oneskies-live-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.
package/dist/index.js ADDED
@@ -0,0 +1,1013 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useRef, useState, } from "react";
4
+ import { Headphones, Loader2, MessageCircle, SendHorizontal, X } from "lucide-react";
5
+ const STORAGE_PREFIX = "oneskies-live-chat";
6
+ const DEFAULT_ACCENT = "#2563eb";
7
+ const TIDIO_HIDE_STYLE_ID = "oneskies-live-chat-hide-tidio";
8
+ const TYPING_BROADCAST_INTERVAL_MS = 120;
9
+ export function LiveChatTypingGhost({ name, body = "", className = "" }) {
10
+ const preview = body.trim();
11
+ return (_jsxs("div", { className: `os-live-chat-typing-ghost ${className}`, "aria-live": "polite", children: [_jsxs("div", { className: "os-live-chat-typing-ghost__meta", children: [_jsxs("span", { children: [name || "Guest", " is typing"] }), _jsx("i", {}), _jsx("i", {}), _jsx("i", {})] }), preview ? _jsx("p", { children: preview }) : null, _jsx("style", { children: `
12
+ .os-live-chat-typing-ghost {
13
+ width: min(34rem, 82%);
14
+ border: 1px solid rgba(148, 163, 184, 0.28);
15
+ border-radius: 18px;
16
+ padding: 10px 12px;
17
+ color: #64748b;
18
+ background: rgba(255, 255, 255, 0.68);
19
+ box-shadow: 0 10px 26px rgba(15, 23, 42, 0.07);
20
+ animation: os-live-chat-ghost-in 180ms ease-out both;
21
+ }
22
+ .os-live-chat-typing-ghost__meta {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ gap: 5px;
26
+ font-size: 12px;
27
+ font-weight: 800;
28
+ }
29
+ .os-live-chat-typing-ghost__meta i {
30
+ width: 5px;
31
+ height: 5px;
32
+ border-radius: 999px;
33
+ background: currentColor;
34
+ animation: os-live-chat-ghost-dot 900ms infinite ease-in-out;
35
+ }
36
+ .os-live-chat-typing-ghost__meta i:nth-child(3) {
37
+ animation-delay: 120ms;
38
+ }
39
+ .os-live-chat-typing-ghost__meta i:nth-child(4) {
40
+ animation-delay: 240ms;
41
+ }
42
+ .os-live-chat-typing-ghost p {
43
+ margin: 7px 0 0;
44
+ color: #475569;
45
+ font-size: 13px;
46
+ line-height: 1.45;
47
+ white-space: pre-wrap;
48
+ overflow-wrap: anywhere;
49
+ opacity: 0.62;
50
+ filter: blur(0.15px);
51
+ }
52
+ @keyframes os-live-chat-ghost-in {
53
+ from {
54
+ opacity: 0;
55
+ transform: translateY(7px);
56
+ }
57
+ to {
58
+ opacity: 1;
59
+ transform: translateY(0);
60
+ }
61
+ }
62
+ @keyframes os-live-chat-ghost-dot {
63
+ 0%, 80%, 100% {
64
+ opacity: 0.35;
65
+ transform: translateY(0);
66
+ }
67
+ 40% {
68
+ opacity: 1;
69
+ transform: translateY(-3px);
70
+ }
71
+ }
72
+ ` })] }));
73
+ }
74
+ function normalizedBaseUrl(raw) {
75
+ return raw.replace(/\/+$/, "");
76
+ }
77
+ function wsUrl(apiBaseUrl, sessionUUID, apiKey) {
78
+ const base = normalizedBaseUrl(apiBaseUrl)
79
+ .replace(/^http:\/\//i, "ws://")
80
+ .replace(/^https:\/\//i, "wss://");
81
+ const params = new URLSearchParams({ api_key: apiKey });
82
+ return `${base}/live-chat/sessions/${encodeURIComponent(sessionUUID)}/ws?${params.toString()}`;
83
+ }
84
+ async function readJson(response) {
85
+ const text = await response.text();
86
+ if (!response.ok) {
87
+ try {
88
+ const parsed = JSON.parse(text);
89
+ throw new Error(parsed.error || "Request failed");
90
+ }
91
+ catch (err) {
92
+ if (err instanceof Error && err.message !== "Unexpected end of JSON input")
93
+ throw err;
94
+ throw new Error(text || "Request failed");
95
+ }
96
+ }
97
+ return JSON.parse(text);
98
+ }
99
+ function apiHeaders(apiKey) {
100
+ const headers = new Headers();
101
+ headers.set("X-Api-Key", apiKey);
102
+ headers.set("Content-Type", "application/json");
103
+ return headers;
104
+ }
105
+ function messageTime(value) {
106
+ const date = new Date(value);
107
+ if (Number.isNaN(date.getTime()))
108
+ return "";
109
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
110
+ }
111
+ function randomId() {
112
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
113
+ return crypto.randomUUID();
114
+ }
115
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
116
+ }
117
+ function getStoredProfile(key) {
118
+ try {
119
+ const raw = window.localStorage.getItem(key);
120
+ if (!raw)
121
+ return null;
122
+ const parsed = JSON.parse(raw);
123
+ if (!parsed.visitorId)
124
+ return null;
125
+ return {
126
+ visitorId: parsed.visitorId,
127
+ name: typeof parsed.name === "string" ? parsed.name : "",
128
+ };
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ function saveStoredProfile(key, profile) {
135
+ window.localStorage.setItem(key, JSON.stringify(profile));
136
+ }
137
+ function setTidioTemporarilyHidden(hidden) {
138
+ const existing = document.getElementById(TIDIO_HIDE_STYLE_ID);
139
+ if (!hidden) {
140
+ existing?.remove();
141
+ return;
142
+ }
143
+ if (existing)
144
+ return;
145
+ const style = document.createElement("style");
146
+ style.id = TIDIO_HIDE_STYLE_ID;
147
+ style.textContent = `
148
+ #tidio-chat-iframe,
149
+ iframe[src*="tidio"],
150
+ div[id*="tidio"],
151
+ div[class*="tidio"],
152
+ [data-testid*="tidio"] {
153
+ opacity: 0 !important;
154
+ visibility: hidden !important;
155
+ pointer-events: none !important;
156
+ }
157
+ `;
158
+ document.head.appendChild(style);
159
+ }
160
+ async function sha256(value) {
161
+ if (typeof crypto === "undefined" || !crypto.subtle) {
162
+ return value;
163
+ }
164
+ const bytes = new TextEncoder().encode(value);
165
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
166
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
167
+ }
168
+ async function buildDeviceSignature(visitorId) {
169
+ const screenValue = typeof screen === "undefined" ? "unknown" : `${screen.width}x${screen.height}x${screen.colorDepth}`;
170
+ const parts = [
171
+ visitorId,
172
+ navigator.userAgent,
173
+ navigator.language,
174
+ Intl.DateTimeFormat().resolvedOptions().timeZone,
175
+ screenValue,
176
+ ];
177
+ return sha256(parts.join("|"));
178
+ }
179
+ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, accentColor = DEFAULT_ACCENT, supportLabel = "Travel support", }) {
180
+ const [visible, setVisible] = useState(false);
181
+ const [open, setOpen] = useState(false);
182
+ const [session, setSession] = useState(null);
183
+ const [messages, setMessages] = useState([]);
184
+ const [draft, setDraft] = useState("");
185
+ const [nameDraft, setNameDraft] = useState("");
186
+ const [profile, setProfile] = useState(null);
187
+ const [profileReady, setProfileReady] = useState(false);
188
+ const [loading, setLoading] = useState(false);
189
+ const [sending, setSending] = useState(false);
190
+ const [error, setError] = useState("");
191
+ const [staffTypingName, setStaffTypingName] = useState("");
192
+ const [unreadCount, setUnreadCount] = useState(0);
193
+ const [unreadPreview, setUnreadPreview] = useState("");
194
+ const [previewVisible, setPreviewVisible] = useState(false);
195
+ const [onlineAgents, setOnlineAgents] = useState([]);
196
+ const wsRef = useRef(null);
197
+ const openRef = useRef(false);
198
+ const messagesEndRef = useRef(null);
199
+ const typingSentAtRef = useRef(0);
200
+ const typingTimeoutRef = useRef(null);
201
+ const unreadPreviewTimeoutRef = useRef(null);
202
+ const baseUrl = useMemo(() => normalizedBaseUrl(apiBaseUrl), [apiBaseUrl]);
203
+ const storageKey = `${STORAGE_PREFIX}:${leadSource}`;
204
+ const profileKey = `${storageKey}:profile`;
205
+ const visitorName = profile?.name.trim() ?? "";
206
+ const needsProfile = profileReady && !visitorName;
207
+ useEffect(() => {
208
+ openRef.current = open;
209
+ if (open) {
210
+ setUnreadCount(0);
211
+ setUnreadPreview("");
212
+ setPreviewVisible(false);
213
+ if (unreadPreviewTimeoutRef.current)
214
+ clearTimeout(unreadPreviewTimeoutRef.current);
215
+ }
216
+ const socket = wsRef.current;
217
+ if (socket?.readyState === WebSocket.OPEN) {
218
+ socket.send(JSON.stringify({ type: "state", open }));
219
+ }
220
+ }, [open]);
221
+ useEffect(() => {
222
+ function onKeyDown(event) {
223
+ if (!event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
224
+ return;
225
+ const isShortcutKey = event.code === "KeyJ" || event.key.toLowerCase() === "j";
226
+ if (!isShortcutKey)
227
+ return;
228
+ event.preventDefault();
229
+ setVisible((current) => {
230
+ const next = !current;
231
+ if (!next)
232
+ setOpen(false);
233
+ return next;
234
+ });
235
+ }
236
+ window.addEventListener("keydown", onKeyDown);
237
+ return () => window.removeEventListener("keydown", onKeyDown);
238
+ }, []);
239
+ useEffect(() => {
240
+ setTidioTemporarilyHidden(visible);
241
+ return () => setTidioTemporarilyHidden(false);
242
+ }, [visible]);
243
+ useEffect(() => {
244
+ if (profileReady)
245
+ return;
246
+ const stored = getStoredProfile(profileKey);
247
+ if (stored) {
248
+ setProfile(stored);
249
+ setNameDraft(stored.name);
250
+ }
251
+ else {
252
+ const nextProfile = { visitorId: randomId(), name: "" };
253
+ saveStoredProfile(profileKey, nextProfile);
254
+ setProfile(nextProfile);
255
+ }
256
+ setProfileReady(true);
257
+ }, [profileKey, profileReady]);
258
+ useEffect(() => {
259
+ if (!visible || !open)
260
+ return;
261
+ let cancelled = false;
262
+ async function loadOnlineAgents() {
263
+ try {
264
+ const response = await fetch(`${baseUrl}/live-chat/agents/online`, {
265
+ headers: apiHeaders(apiKey),
266
+ credentials: "omit",
267
+ });
268
+ const envelope = await readJson(response);
269
+ if (!cancelled)
270
+ setOnlineAgents(Array.isArray(envelope.data) ? envelope.data.slice(0, 5) : []);
271
+ }
272
+ catch {
273
+ if (!cancelled)
274
+ setOnlineAgents([]);
275
+ }
276
+ }
277
+ void loadOnlineAgents();
278
+ const interval = window.setInterval(loadOnlineAgents, 30000);
279
+ return () => {
280
+ cancelled = true;
281
+ window.clearInterval(interval);
282
+ };
283
+ }, [apiKey, baseUrl, open, visible]);
284
+ useEffect(() => {
285
+ if (!visible || !open)
286
+ return;
287
+ messagesEndRef.current?.scrollIntoView({ block: "end" });
288
+ }, [messages.length, staffTypingName, open, visible]);
289
+ useEffect(() => {
290
+ if (!profileReady || session || !profile)
291
+ return;
292
+ const currentProfile = profile;
293
+ let cancelled = false;
294
+ async function startSession() {
295
+ if (visible && open)
296
+ setLoading(true);
297
+ setError("");
298
+ try {
299
+ const stored = window.localStorage.getItem(storageKey);
300
+ if (stored) {
301
+ const parsed = JSON.parse(stored);
302
+ if (parsed.uuid && parsed.guest_name === currentProfile.name) {
303
+ setSession(parsed);
304
+ return;
305
+ }
306
+ }
307
+ const signature = await buildDeviceSignature(currentProfile.visitorId);
308
+ const response = await fetch(`${baseUrl}/live-chat/sessions`, {
309
+ method: "POST",
310
+ headers: apiHeaders(apiKey),
311
+ credentials: "omit",
312
+ body: JSON.stringify({
313
+ lead_source: leadSource,
314
+ guest_name: currentProfile.name,
315
+ page_url: window.location.href,
316
+ device_signature: signature,
317
+ }),
318
+ });
319
+ const envelope = await readJson(response);
320
+ if (cancelled)
321
+ return;
322
+ window.localStorage.setItem(storageKey, JSON.stringify(envelope.data));
323
+ setSession(envelope.data);
324
+ }
325
+ catch (err) {
326
+ if (!cancelled && visible && open)
327
+ setError(err instanceof Error ? err.message : "Chat is unavailable.");
328
+ }
329
+ finally {
330
+ if (!cancelled)
331
+ setLoading(false);
332
+ }
333
+ }
334
+ void startSession();
335
+ return () => {
336
+ cancelled = true;
337
+ };
338
+ }, [apiKey, baseUrl, leadSource, open, profile, profileReady, session, storageKey, visible]);
339
+ useEffect(() => {
340
+ if (!visible || !open || !session)
341
+ return;
342
+ const sessionUUID = session.uuid;
343
+ let cancelled = false;
344
+ async function loadMessages() {
345
+ try {
346
+ const response = await fetch(`${baseUrl}/live-chat/sessions/${sessionUUID}/messages`, {
347
+ headers: apiHeaders(apiKey),
348
+ credentials: "omit",
349
+ });
350
+ const envelope = await readJson(response);
351
+ if (!cancelled)
352
+ setMessages(envelope.data);
353
+ }
354
+ catch {
355
+ if (!cancelled)
356
+ setError("Could not load the chat history.");
357
+ }
358
+ }
359
+ void loadMessages();
360
+ return () => {
361
+ cancelled = true;
362
+ };
363
+ }, [apiKey, baseUrl, open, session, visible]);
364
+ useEffect(() => {
365
+ if (!session)
366
+ return;
367
+ const socket = new WebSocket(wsUrl(baseUrl, session.uuid, apiKey));
368
+ wsRef.current = socket;
369
+ socket.onopen = () => {
370
+ socket.send(JSON.stringify({ type: "state", open: openRef.current }));
371
+ };
372
+ socket.onmessage = (event) => {
373
+ if (typeof event.data !== "string")
374
+ return;
375
+ try {
376
+ const payload = JSON.parse(event.data);
377
+ if (payload.action === "typing" && payload.sender === "staff") {
378
+ setStaffTypingName(payload.name?.trim() || brandName);
379
+ if (typingTimeoutRef.current)
380
+ clearTimeout(typingTimeoutRef.current);
381
+ typingTimeoutRef.current = setTimeout(() => setStaffTypingName(""), 2200);
382
+ return;
383
+ }
384
+ if (payload.action === "session_removed") {
385
+ window.localStorage.removeItem(storageKey);
386
+ window.localStorage.removeItem(profileKey);
387
+ setSession(null);
388
+ setMessages([]);
389
+ setDraft("");
390
+ setNameDraft("");
391
+ setProfile(null);
392
+ setProfileReady(false);
393
+ setStaffTypingName("");
394
+ setUnreadCount(0);
395
+ setUnreadPreview("");
396
+ setPreviewVisible(false);
397
+ socket.close();
398
+ return;
399
+ }
400
+ if (payload.action !== "message_created" || !payload.message)
401
+ return;
402
+ setStaffTypingName("");
403
+ if (payload.message.sender === "staff" && !openRef.current) {
404
+ setUnreadCount((count) => Math.min(count + 1, 99));
405
+ setUnreadPreview(payload.message.body);
406
+ setPreviewVisible(true);
407
+ if (unreadPreviewTimeoutRef.current)
408
+ clearTimeout(unreadPreviewTimeoutRef.current);
409
+ unreadPreviewTimeoutRef.current = setTimeout(() => setPreviewVisible(false), 5000);
410
+ }
411
+ setMessages((current) => {
412
+ if (current.some((item) => item.id === payload.message.id))
413
+ return current;
414
+ return [...current, payload.message];
415
+ });
416
+ }
417
+ catch {
418
+ // Ignore malformed realtime payloads.
419
+ }
420
+ };
421
+ return () => {
422
+ if (socket.readyState === WebSocket.OPEN) {
423
+ socket.send(JSON.stringify({ type: "state", open: false }));
424
+ }
425
+ socket.close();
426
+ if (wsRef.current === socket)
427
+ wsRef.current = null;
428
+ if (typingTimeoutRef.current)
429
+ clearTimeout(typingTimeoutRef.current);
430
+ if (unreadPreviewTimeoutRef.current)
431
+ clearTimeout(unreadPreviewTimeoutRef.current);
432
+ };
433
+ }, [apiKey, baseUrl, brandName, profileKey, session, storageKey]);
434
+ function saveName(event) {
435
+ event.preventDefault();
436
+ const name = nameDraft.trim();
437
+ if (!name || !profile)
438
+ return;
439
+ const nextProfile = { ...profile, name };
440
+ saveStoredProfile(profileKey, nextProfile);
441
+ setProfile(nextProfile);
442
+ setSession(null);
443
+ }
444
+ function announceTyping(nextDraft) {
445
+ const now = Date.now();
446
+ if (now - typingSentAtRef.current < TYPING_BROADCAST_INTERVAL_MS)
447
+ return;
448
+ typingSentAtRef.current = now;
449
+ const socket = wsRef.current;
450
+ if (socket?.readyState === WebSocket.OPEN) {
451
+ socket.send(JSON.stringify({ type: "typing", name: visitorName || "Guest", body: nextDraft.slice(0, 500) }));
452
+ }
453
+ }
454
+ async function sendMessage(event) {
455
+ event.preventDefault();
456
+ const body = draft.trim();
457
+ if (!body || !session || sending)
458
+ return;
459
+ setSending(true);
460
+ setError("");
461
+ setDraft("");
462
+ try {
463
+ const socket = wsRef.current;
464
+ if (socket?.readyState === WebSocket.OPEN) {
465
+ socket.send(JSON.stringify({ type: "message", body }));
466
+ }
467
+ else {
468
+ const response = await fetch(`${baseUrl}/live-chat/sessions/${session.uuid}/messages`, {
469
+ method: "POST",
470
+ headers: apiHeaders(apiKey),
471
+ credentials: "omit",
472
+ body: JSON.stringify({ body }),
473
+ });
474
+ const envelope = await readJson(response);
475
+ setMessages((current) => [...current, envelope.data]);
476
+ }
477
+ }
478
+ catch (err) {
479
+ setDraft(body);
480
+ setError(err instanceof Error ? err.message : "Message was not sent.");
481
+ }
482
+ finally {
483
+ setSending(false);
484
+ }
485
+ }
486
+ const rootStyle = {
487
+ "--os-chat-accent": accentColor,
488
+ };
489
+ return (_jsxs("div", { className: `os-live-chat ${visible ? "is-visible" : "is-hidden"}`, style: rootStyle, children: [open ? (_jsxs("section", { className: "os-live-chat-panel", "aria-label": `${brandName} live chat`, children: [_jsxs("header", { className: "os-live-chat-header", children: [_jsx("span", { className: "os-live-chat-mark", children: _jsx("span", { className: "os-live-chat-mark-icon", children: _jsx(Headphones, { size: 22, "aria-hidden": true }) }) }), _jsxs("div", { children: [_jsx("p", { children: supportLabel }), _jsx("span", { children: visitorName ? `${visitorName}, ${brandName} team is here` : `${brandName} team` })] }), _jsx("button", { type: "button", "aria-label": "Close chat", onClick: () => setOpen(false), children: _jsx(X, { size: 20, "aria-hidden": true }) })] }), needsProfile ? (_jsxs("form", { className: "os-live-chat-intro", onSubmit: saveName, children: [_jsx("strong", { children: "Welcome. What should we call you?" }), onlineAgents.length > 0 ? (_jsxs("div", { className: "os-live-chat-online", "aria-label": `${onlineAgents.length} support agents online`, children: [_jsx("div", { className: "os-live-chat-online-avatars", children: onlineAgents.map((agent, index) => (_jsx("span", { className: "os-live-chat-online-avatar", style: { "--os-agent-color": agent.color || accentColor }, children: agent.avatar_url ? _jsx("img", { src: agent.avatar_url, alt: "" }) : null }, `${agent.avatar_url || agent.color || "agent"}-${index}`))) }), _jsx("span", { children: "Support is online now" })] })) : null, _jsx("input", { autoFocus: true, value: nameDraft, onChange: (event) => setNameDraft(event.target.value), placeholder: "Your name", maxLength: 80 }), _jsx("button", { type: "submit", disabled: !nameDraft.trim(), children: "Start chat" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "os-live-chat-body", children: [loading ? (_jsxs("div", { className: "os-live-chat-empty", children: [_jsx(Loader2, { size: 18, className: "os-live-chat-spin", "aria-hidden": true }), "Connecting..."] })) : messages.length === 0 ? (_jsxs("div", { className: "os-live-chat-empty", children: [_jsx("strong", { children: "How can we help?" }), onlineAgents.length > 0 ? (_jsxs("div", { className: "os-live-chat-online", "aria-label": `${onlineAgents.length} support agents online`, children: [_jsx("div", { className: "os-live-chat-online-avatars", children: onlineAgents.map((agent, index) => (_jsx("span", { className: "os-live-chat-online-avatar", style: { "--os-agent-color": agent.color || accentColor }, children: agent.avatar_url ? _jsx("img", { src: agent.avatar_url, alt: "" }) : null }, `${agent.avatar_url || agent.color || "agent"}-${index}`))) }), _jsx("span", { children: "Support is online now" })] })) : null, _jsx("span", { children: "Send a message and our team will reply here." })] })) : (messages.map((message) => (_jsxs("article", { className: `os-live-chat-message ${message.sender === "guest" ? "is-guest" : "is-staff"}`, children: [_jsx("p", { children: message.body }), _jsxs("span", { children: [message.sender === "guest" ? "You" : brandName, " \u00B7 ", messageTime(message.created_at)] })] }, message.id)))), staffTypingName ? (_jsxs("div", { className: "os-live-chat-typing", "aria-live": "polite", children: [_jsx("span", { children: "Support is typing" }), _jsx("i", {}), _jsx("i", {}), _jsx("i", {})] })) : null, _jsx("div", { ref: messagesEndRef })] }), error ? _jsx("p", { className: "os-live-chat-error", children: error }) : null, _jsxs("form", { className: "os-live-chat-form", onSubmit: sendMessage, children: [_jsx("textarea", { value: draft, onChange: (event) => {
490
+ const nextDraft = event.target.value;
491
+ setDraft(nextDraft);
492
+ announceTyping(nextDraft);
493
+ }, placeholder: "Type your message...", rows: 2, onKeyDown: (event) => {
494
+ if (event.key === "Enter" && !event.shiftKey) {
495
+ event.preventDefault();
496
+ event.currentTarget.form?.requestSubmit();
497
+ }
498
+ } }), _jsx("button", { type: "submit", disabled: !draft.trim() || !session || sending, "aria-label": "Send message", children: sending ? (_jsx(Loader2, { size: 19, className: "os-live-chat-spin", "aria-hidden": true })) : (_jsx(SendHorizontal, { size: 20, "aria-hidden": true })) })] })] }))] })) : null, visible ? (_jsxs("button", { className: "os-live-chat-bubble", type: "button", onClick: () => setOpen((value) => !value), "aria-label": "Open live chat", children: [_jsx(MessageCircle, { size: 25, "aria-hidden": true }), unreadCount > 0 ? _jsx("strong", { className: "os-live-chat-badge", children: unreadCount }) : null, _jsx("span", { className: unreadCount > 0 ? `has-unread ${previewVisible ? "is-preview-visible" : "is-preview-hidden"}` : "", children: unreadCount > 0 && previewVisible ? (_jsxs(_Fragment, { children: [_jsx("b", { children: "New message" }), _jsx("small", { children: unreadPreview })] })) : ("Need live assistance?") })] })) : null, _jsx("style", { children: `
499
+ .os-live-chat {
500
+ position: fixed;
501
+ right: max(18px, env(safe-area-inset-right));
502
+ bottom: max(18px, env(safe-area-inset-bottom));
503
+ z-index: 80;
504
+ font-family: var(--font-manrope), var(--font-inter), ui-sans-serif, system-ui, sans-serif;
505
+ }
506
+ .os-live-chat.is-hidden {
507
+ pointer-events: none;
508
+ }
509
+ .os-live-chat-bubble {
510
+ position: relative;
511
+ display: grid;
512
+ width: 62px;
513
+ height: 62px;
514
+ place-items: center;
515
+ border: 0;
516
+ border-radius: 999px;
517
+ color: white;
518
+ background: radial-gradient(circle at 32% 24%, color-mix(in srgb, var(--os-chat-accent) 72%, white), var(--os-chat-accent) 62%, color-mix(in srgb, var(--os-chat-accent) 72%, #111827));
519
+ box-shadow: 0 18px 44px color-mix(in srgb, var(--os-chat-accent) 38%, rgba(15, 23, 42, 0.28));
520
+ cursor: pointer;
521
+ animation: os-chat-bubble-in 220ms cubic-bezier(.2,.8,.2,1) both;
522
+ transition: transform 180ms ease, box-shadow 180ms ease;
523
+ }
524
+ .os-live-chat-bubble span {
525
+ position: absolute;
526
+ right: 48px;
527
+ bottom: 10px;
528
+ width: max-content;
529
+ max-width: 230px;
530
+ border: 1px solid rgba(148, 163, 184, 0.28);
531
+ border-radius: 999px;
532
+ padding: 10px 14px;
533
+ color: #0f172a;
534
+ background: rgba(255, 255, 255, 0.96);
535
+ box-shadow: 0 14px 34px rgba(15, 23, 42, 0.14);
536
+ font-size: 14px;
537
+ font-weight: 800;
538
+ pointer-events: none;
539
+ animation: os-chat-nudge 420ms ease-out 350ms both;
540
+ }
541
+ .os-live-chat-bubble span.has-unread {
542
+ display: grid;
543
+ gap: 2px;
544
+ width: min(260px, calc(100vw - 106px));
545
+ max-width: 260px;
546
+ border-radius: 18px;
547
+ padding: 10px 13px;
548
+ text-align: left;
549
+ }
550
+ .os-live-chat-bubble span.has-unread.is-preview-visible {
551
+ animation: os-chat-preview-in 220ms cubic-bezier(.2,.8,.2,1) both;
552
+ }
553
+ .os-live-chat-bubble span.has-unread.is-preview-hidden {
554
+ opacity: 0;
555
+ transform: translateX(10px) scale(0.98);
556
+ visibility: hidden;
557
+ transition: opacity 220ms ease, transform 220ms ease, visibility 0s linear 220ms;
558
+ }
559
+ .os-live-chat-bubble span.has-unread b,
560
+ .os-live-chat-bubble span.has-unread small {
561
+ display: block;
562
+ min-width: 0;
563
+ overflow: hidden;
564
+ text-overflow: ellipsis;
565
+ white-space: nowrap;
566
+ }
567
+ .os-live-chat-bubble span.has-unread b {
568
+ color: #0f172a;
569
+ font-size: 13px;
570
+ line-height: 1.15;
571
+ }
572
+ .os-live-chat-bubble span.has-unread small {
573
+ color: #64748b;
574
+ font-size: 12px;
575
+ font-weight: 700;
576
+ line-height: 1.25;
577
+ }
578
+ .os-live-chat-badge {
579
+ position: absolute;
580
+ top: -5px;
581
+ right: -5px;
582
+ display: grid;
583
+ min-width: 22px;
584
+ height: 22px;
585
+ place-items: center;
586
+ border: 2px solid white;
587
+ border-radius: 999px;
588
+ padding: 0 6px;
589
+ color: white;
590
+ background: #ef4444;
591
+ box-shadow: 0 10px 22px rgba(239, 68, 68, 0.35);
592
+ font-size: 12px;
593
+ font-weight: 900;
594
+ line-height: 1;
595
+ }
596
+ .os-live-chat-bubble:hover {
597
+ transform: translateY(-2px) scale(1.02);
598
+ box-shadow: 0 22px 54px color-mix(in srgb, var(--os-chat-accent) 42%, rgba(15, 23, 42, 0.31));
599
+ }
600
+ .os-live-chat-panel {
601
+ position: absolute;
602
+ right: 0;
603
+ bottom: 78px;
604
+ display: flex;
605
+ width: min(388px, calc(100vw - 28px));
606
+ max-height: min(660px, calc(100vh - 112px));
607
+ overflow: hidden;
608
+ flex-direction: column;
609
+ border: 1px solid rgba(148, 163, 184, 0.36);
610
+ border-radius: 28px;
611
+ background: rgba(255, 255, 255, 0.98);
612
+ box-shadow: 0 28px 80px rgba(15, 23, 42, 0.24);
613
+ animation: os-chat-in 210ms cubic-bezier(.2,.8,.2,1);
614
+ transform-origin: bottom right;
615
+ }
616
+ .os-live-chat-header {
617
+ display: flex;
618
+ align-items: center;
619
+ gap: 13px;
620
+ padding: 16px;
621
+ color: #0f172a;
622
+ border-bottom: 1px solid rgba(148, 163, 184, 0.2);
623
+ background: linear-gradient(180deg, rgba(248, 250, 252, 0.98), rgba(255, 255, 255, 0.96));
624
+ }
625
+ .os-live-chat-header p {
626
+ margin: 0;
627
+ font-size: 16px;
628
+ font-weight: 900;
629
+ letter-spacing: 0;
630
+ }
631
+ .os-live-chat-header span {
632
+ display: block;
633
+ margin-top: 2px;
634
+ font-size: 13px;
635
+ color: #64748b;
636
+ }
637
+ .os-live-chat-mark {
638
+ position: relative;
639
+ display: grid;
640
+ width: 48px;
641
+ height: 48px;
642
+ flex: 0 0 48px;
643
+ place-items: center;
644
+ border-radius: 18px;
645
+ color: var(--os-chat-accent);
646
+ background: color-mix(in srgb, var(--os-chat-accent) 12%, white);
647
+ line-height: 0;
648
+ }
649
+ .os-live-chat-mark-icon {
650
+ position: absolute;
651
+ top: 50%;
652
+ left: 50%;
653
+ display: grid;
654
+ width: 24px;
655
+ height: 24px;
656
+ place-items: center;
657
+ margin: 0;
658
+ color: inherit;
659
+ line-height: 0;
660
+ transform: translate(-50%, -50%);
661
+ }
662
+ .os-live-chat-mark-icon svg {
663
+ display: block;
664
+ width: 22px !important;
665
+ height: 22px !important;
666
+ margin: 0;
667
+ flex: none;
668
+ transform: none;
669
+ }
670
+ .os-live-chat-header button {
671
+ margin-left: auto;
672
+ display: grid;
673
+ width: 44px;
674
+ height: 44px;
675
+ place-items: center;
676
+ border: 0;
677
+ border-radius: 999px;
678
+ color: #475569;
679
+ background: rgba(15, 23, 42, 0.05);
680
+ cursor: pointer;
681
+ transition: background 160ms ease, transform 160ms ease;
682
+ }
683
+ .os-live-chat-header button:hover {
684
+ background: rgba(15, 23, 42, 0.09);
685
+ transform: rotate(3deg);
686
+ }
687
+ .os-live-chat-intro {
688
+ display: grid;
689
+ gap: 14px;
690
+ padding: 22px 20px 20px;
691
+ background: linear-gradient(180deg, #ffffff, #f8fafc);
692
+ }
693
+ .os-live-chat-intro strong {
694
+ color: #0f172a;
695
+ font-size: 19px;
696
+ line-height: 1.18;
697
+ }
698
+ .os-live-chat-intro p {
699
+ margin: -6px 0 2px;
700
+ color: #64748b;
701
+ font-size: 13px;
702
+ line-height: 1.5;
703
+ }
704
+ .os-live-chat-online {
705
+ display: inline-flex;
706
+ align-items: center;
707
+ gap: 8px;
708
+ width: fit-content;
709
+ border: 1px solid rgba(148, 163, 184, 0.28);
710
+ border-radius: 999px;
711
+ padding: 5px 10px 5px 5px;
712
+ color: #475569;
713
+ background: rgba(255, 255, 255, 0.78);
714
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
715
+ font-size: 11px;
716
+ font-weight: 850;
717
+ animation: os-chat-nudge 260ms ease-out both;
718
+ }
719
+ .os-live-chat-empty .os-live-chat-online {
720
+ margin: 2px auto 0;
721
+ }
722
+ .os-live-chat-online-avatars {
723
+ display: flex;
724
+ align-items: center;
725
+ }
726
+ .os-live-chat-online-avatar {
727
+ position: relative;
728
+ display: grid;
729
+ width: 24px;
730
+ height: 24px;
731
+ place-items: center;
732
+ border: 2px solid white;
733
+ border-radius: 999px;
734
+ background: var(--os-agent-color);
735
+ box-shadow: 0 5px 12px rgba(15, 23, 42, 0.16);
736
+ }
737
+ .os-live-chat-online-avatar + .os-live-chat-online-avatar {
738
+ margin-left: -8px;
739
+ }
740
+ .os-live-chat-online-avatar::after {
741
+ position: absolute;
742
+ right: -1px;
743
+ bottom: -1px;
744
+ width: 7px;
745
+ height: 7px;
746
+ border: 1.5px solid white;
747
+ border-radius: 999px;
748
+ background: #16a34a;
749
+ content: "";
750
+ }
751
+ .os-live-chat-online-avatar img {
752
+ width: 100%;
753
+ height: 100%;
754
+ overflow: hidden;
755
+ border-radius: inherit;
756
+ object-fit: cover;
757
+ }
758
+ .os-live-chat-intro input {
759
+ height: 48px;
760
+ border: 1px solid rgba(148, 163, 184, 0.42);
761
+ border-radius: 16px;
762
+ padding: 0 14px;
763
+ color: #0f172a;
764
+ background: #ffffff;
765
+ font: inherit;
766
+ outline: none;
767
+ transition: border-color 160ms ease, box-shadow 160ms ease;
768
+ }
769
+ .os-live-chat-intro input:focus {
770
+ border-color: color-mix(in srgb, var(--os-chat-accent) 62%, white);
771
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--os-chat-accent) 14%, transparent);
772
+ }
773
+ .os-live-chat-intro button {
774
+ height: 48px;
775
+ border: 0;
776
+ border-radius: 16px;
777
+ color: white;
778
+ background: var(--os-chat-accent);
779
+ font: inherit;
780
+ font-weight: 900;
781
+ cursor: pointer;
782
+ transition: transform 160ms ease, opacity 160ms ease;
783
+ }
784
+ .os-live-chat-intro button:hover:not(:disabled) {
785
+ transform: translateY(-1px);
786
+ }
787
+ .os-live-chat-intro button:disabled {
788
+ opacity: 0.45;
789
+ cursor: not-allowed;
790
+ }
791
+ .os-live-chat-body {
792
+ display: flex;
793
+ min-height: 300px;
794
+ flex: 1;
795
+ flex-direction: column;
796
+ gap: 10px;
797
+ overflow-y: auto;
798
+ padding: 16px;
799
+ background:
800
+ radial-gradient(circle at 22% 0%, color-mix(in srgb, var(--os-chat-accent) 8%, transparent), transparent 32%),
801
+ #f8fafc;
802
+ }
803
+ .os-live-chat-empty {
804
+ display: grid;
805
+ min-height: 210px;
806
+ place-items: center;
807
+ align-content: center;
808
+ gap: 8px;
809
+ text-align: center;
810
+ color: #64748b;
811
+ font-size: 13px;
812
+ }
813
+ .os-live-chat-empty strong {
814
+ color: #0f172a;
815
+ font-size: 16px;
816
+ }
817
+ .os-live-chat-message {
818
+ max-width: 86%;
819
+ border-radius: 20px;
820
+ padding: 11px 13px;
821
+ font-size: 14px;
822
+ line-height: 1.45;
823
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
824
+ animation: os-message-in 180ms ease-out both;
825
+ }
826
+ .os-live-chat-message p {
827
+ margin: 0;
828
+ white-space: pre-wrap;
829
+ overflow-wrap: anywhere;
830
+ }
831
+ .os-live-chat-message span {
832
+ display: block;
833
+ margin-top: 6px;
834
+ font-size: 11px;
835
+ opacity: 0.72;
836
+ }
837
+ .os-live-chat-message.is-guest {
838
+ align-self: flex-end;
839
+ color: white;
840
+ background: linear-gradient(145deg, color-mix(in srgb, var(--os-chat-accent) 88%, white), var(--os-chat-accent));
841
+ border-bottom-right-radius: 8px;
842
+ }
843
+ .os-live-chat-message.is-staff,
844
+ .os-live-chat-message:not(.is-guest) {
845
+ align-self: flex-start;
846
+ color: #0f172a;
847
+ background: white;
848
+ border: 1px solid rgba(148, 163, 184, 0.22);
849
+ border-bottom-left-radius: 8px;
850
+ }
851
+ .os-live-chat-typing {
852
+ align-self: flex-start;
853
+ display: inline-flex;
854
+ align-items: center;
855
+ gap: 5px;
856
+ border: 1px solid rgba(148, 163, 184, 0.22);
857
+ border-radius: 999px;
858
+ padding: 9px 12px;
859
+ color: #64748b;
860
+ background: white;
861
+ font-size: 12px;
862
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
863
+ animation: os-message-in 180ms ease-out both;
864
+ }
865
+ .os-live-chat-typing i {
866
+ width: 5px;
867
+ height: 5px;
868
+ border-radius: 999px;
869
+ background: currentColor;
870
+ animation: os-typing-dot 900ms infinite ease-in-out;
871
+ }
872
+ .os-live-chat-typing i:nth-child(3) {
873
+ animation-delay: 120ms;
874
+ }
875
+ .os-live-chat-typing i:nth-child(4) {
876
+ animation-delay: 240ms;
877
+ }
878
+ .os-live-chat-error {
879
+ margin: 0;
880
+ padding: 8px 14px 0;
881
+ color: #b91c1c;
882
+ font-size: 12px;
883
+ background: white;
884
+ }
885
+ .os-live-chat-form {
886
+ display: grid;
887
+ grid-template-columns: 1fr 50px;
888
+ gap: 10px;
889
+ padding: 13px;
890
+ background: white;
891
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
892
+ }
893
+ .os-live-chat-form textarea {
894
+ min-height: 52px;
895
+ max-height: 124px;
896
+ resize: vertical;
897
+ border: 1px solid rgba(148, 163, 184, 0.4);
898
+ border-radius: 18px;
899
+ padding: 13px 14px;
900
+ color: #0f172a;
901
+ background: #f8fafc;
902
+ font: inherit;
903
+ outline: none;
904
+ transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
905
+ }
906
+ .os-live-chat-form textarea:focus {
907
+ border-color: color-mix(in srgb, var(--os-chat-accent) 60%, white);
908
+ background: white;
909
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--os-chat-accent) 14%, transparent);
910
+ }
911
+ .os-live-chat-form button {
912
+ display: grid;
913
+ place-items: center;
914
+ border: 0;
915
+ border-radius: 18px;
916
+ color: white;
917
+ background: var(--os-chat-accent);
918
+ cursor: pointer;
919
+ transition: transform 160ms ease, opacity 160ms ease;
920
+ }
921
+ .os-live-chat-form button:hover:not(:disabled) {
922
+ transform: translateY(-1px);
923
+ }
924
+ .os-live-chat-form button:disabled {
925
+ cursor: not-allowed;
926
+ opacity: 0.45;
927
+ }
928
+ .os-live-chat-spin {
929
+ animation: os-chat-spin 900ms linear infinite;
930
+ }
931
+ @keyframes os-chat-in {
932
+ from {
933
+ opacity: 0;
934
+ transform: translateY(12px) scale(0.97);
935
+ }
936
+ to {
937
+ opacity: 1;
938
+ transform: translateY(0) scale(1);
939
+ }
940
+ }
941
+ @keyframes os-chat-bubble-in {
942
+ from {
943
+ opacity: 0;
944
+ transform: translateY(14px) scale(0.88);
945
+ }
946
+ to {
947
+ opacity: 1;
948
+ transform: translateY(0) scale(1);
949
+ }
950
+ }
951
+ @keyframes os-chat-nudge {
952
+ from {
953
+ opacity: 0;
954
+ transform: translateX(8px);
955
+ }
956
+ to {
957
+ opacity: 1;
958
+ transform: translateX(0);
959
+ }
960
+ }
961
+ @keyframes os-chat-preview-in {
962
+ from {
963
+ opacity: 0;
964
+ transform: translateX(10px) scale(0.98);
965
+ }
966
+ to {
967
+ opacity: 1;
968
+ transform: translateX(0) scale(1);
969
+ }
970
+ }
971
+ @keyframes os-message-in {
972
+ from {
973
+ opacity: 0;
974
+ transform: translateY(7px) scale(0.98);
975
+ }
976
+ to {
977
+ opacity: 1;
978
+ transform: translateY(0) scale(1);
979
+ }
980
+ }
981
+ @keyframes os-typing-dot {
982
+ 0%, 80%, 100% {
983
+ opacity: 0.35;
984
+ transform: translateY(0);
985
+ }
986
+ 40% {
987
+ opacity: 1;
988
+ transform: translateY(-3px);
989
+ }
990
+ }
991
+ @keyframes os-chat-spin {
992
+ to {
993
+ transform: rotate(360deg);
994
+ }
995
+ }
996
+ @media (max-width: 520px) {
997
+ .os-live-chat {
998
+ right: 12px;
999
+ bottom: 12px;
1000
+ }
1001
+ .os-live-chat-panel {
1002
+ bottom: 72px;
1003
+ width: calc(100vw - 24px);
1004
+ max-height: calc(100vh - 96px);
1005
+ border-radius: 24px;
1006
+ }
1007
+ .os-live-chat-bubble span:not(.has-unread) {
1008
+ display: none;
1009
+ }
1010
+ }
1011
+ ` })] }));
1012
+ }
1013
+ //# sourceMappingURL=index.js.map