oneskies-live-chat-widget 0.1.4 → 0.1.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAYA,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,YAAY,CAAC;AAE3D,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAqEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAS,EAAE,SAAc,EAAE,EAAE,wBAAwB,2CA4EhG;AA+GD,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAA4B,EAC5B,YAA+B,GAChC,EAAE,mBAAmB,2CAsgCrB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAYA,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,YAAY,CAAC;AAE3D,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAmFF,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAS,EAAE,SAAc,EAAE,EAAE,wBAAwB,2CA4EhG;AA8ID,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAA4B,EAC5B,YAA+B,GAChC,EAAE,mBAAmB,2CAqwCrB"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const TYPING_BROADCAST_INTERVAL_MS = 120;
9
9
  const WS_RECONNECT_BASE_DELAY_MS = 650;
10
10
  const WS_RECONNECT_MAX_DELAY_MS = 12000;
11
11
  const WS_MAX_MESSAGE_CHARS = 128 * 1024;
12
+ const LIVE_CHAT_BLOCKED_STATUS = 423;
12
13
  function toTitleCase(name) {
13
14
  return name.replace(/\S+/g, (word) => word
14
15
  .split(/(['-])/)
@@ -124,6 +125,37 @@ function apiHeaders(apiKey) {
124
125
  headers.set("Content-Type", "application/json");
125
126
  return headers;
126
127
  }
128
+ function liveChatQueryParam(name) {
129
+ try {
130
+ return new URL(window.location.href).searchParams.get(name)?.trim() ?? "";
131
+ }
132
+ catch {
133
+ return "";
134
+ }
135
+ }
136
+ function liveChatCurrentUrl() {
137
+ try {
138
+ return window.location.href;
139
+ }
140
+ catch {
141
+ return "";
142
+ }
143
+ }
144
+ function liveChatTrackedFieldKey(element) {
145
+ if (!(element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement)) {
146
+ return "";
147
+ }
148
+ if (element.closest(".os-live-chat"))
149
+ return "";
150
+ if (element instanceof HTMLInputElement && ["hidden", "submit", "button", "reset", "image"].includes(element.type)) {
151
+ return "";
152
+ }
153
+ const form = element.closest("form");
154
+ const formKey = form?.getAttribute("id") || form?.getAttribute("name") || form?.getAttribute("aria-label") || "";
155
+ const placeholder = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element.placeholder : "";
156
+ const fieldKey = element.name || element.id || element.getAttribute("aria-label") || placeholder || element.type || element.tagName;
157
+ return `${formKey}:${fieldKey}`.slice(0, 220);
158
+ }
127
159
  function messageTime(value) {
128
160
  const date = new Date(value);
129
161
  if (Number.isNaN(date.getTime()))
@@ -199,6 +231,7 @@ async function buildDeviceSignature(visitorId) {
199
231
  return sha256(parts.join("|"));
200
232
  }
201
233
  export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, accentColor = DEFAULT_ACCENT, supportLabel = "Travel support", }) {
234
+ const initialBlockedKey = `${STORAGE_PREFIX}:${leadSource}:blocked`;
202
235
  const [visible, setVisible] = useState(false);
203
236
  const [open, setOpen] = useState(false);
204
237
  const [session, setSession] = useState(null);
@@ -210,6 +243,15 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
210
243
  const [loading, setLoading] = useState(false);
211
244
  const [sending, setSending] = useState(false);
212
245
  const [error, setError] = useState("");
246
+ const [blocked, setBlocked] = useState(() => {
247
+ try {
248
+ return window.localStorage.getItem(initialBlockedKey) === "1";
249
+ }
250
+ catch {
251
+ return false;
252
+ }
253
+ });
254
+ const [blockedRetryNonce, setBlockedRetryNonce] = useState(0);
213
255
  const [staffTypingName, setStaffTypingName] = useState("");
214
256
  const [unreadCount, setUnreadCount] = useState(0);
215
257
  const [unreadPreview, setUnreadPreview] = useState("");
@@ -223,11 +265,75 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
223
265
  const typingSentAtRef = useRef(0);
224
266
  const typingTimeoutRef = useRef(null);
225
267
  const unreadPreviewTimeoutRef = useRef(null);
268
+ const pageUrlsRef = useRef(new Set());
269
+ const touchedFieldsRef = useRef(new Set());
270
+ const stateBroadcastTimeoutRef = useRef(null);
271
+ const banReloadedRef = useRef(false);
226
272
  const baseUrl = useMemo(() => normalizedBaseUrl(apiBaseUrl), [apiBaseUrl]);
227
273
  const storageKey = `${STORAGE_PREFIX}:${leadSource}`;
228
274
  const profileKey = `${storageKey}:profile`;
275
+ const blockedKey = `${storageKey}:blocked`;
229
276
  const visitorName = formatGuestName(profile?.name);
230
277
  const needsProfile = profileReady && !visitorName;
278
+ function buildStatePayload() {
279
+ const currentUrl = liveChatCurrentUrl();
280
+ if (currentUrl)
281
+ pageUrlsRef.current.add(currentUrl);
282
+ return {
283
+ type: "state",
284
+ open: openRef.current,
285
+ page_url: currentUrl,
286
+ referrer_url: document.referrer || "",
287
+ utm_source: liveChatQueryParam("utm_source"),
288
+ utm_medium: liveChatQueryParam("utm_medium"),
289
+ utm_campaign: liveChatQueryParam("utm_campaign"),
290
+ page_view_count: Math.max(1, pageUrlsRef.current.size),
291
+ fields_touched_count: touchedFieldsRef.current.size,
292
+ };
293
+ }
294
+ function sendStateNow() {
295
+ const socket = wsRef.current;
296
+ if (socket?.readyState !== WebSocket.OPEN)
297
+ return;
298
+ socket.send(JSON.stringify(buildStatePayload()));
299
+ }
300
+ function scheduleStateBroadcast(delay = 250) {
301
+ if (stateBroadcastTimeoutRef.current)
302
+ return;
303
+ stateBroadcastTimeoutRef.current = window.setTimeout(() => {
304
+ stateBroadcastTimeoutRef.current = null;
305
+ sendStateNow();
306
+ }, delay);
307
+ }
308
+ function enterBlockedState({ reload = false } = {}) {
309
+ try {
310
+ window.localStorage.setItem(blockedKey, "1");
311
+ }
312
+ catch {
313
+ // Ignore storage failures; the in-memory state still blocks the current page.
314
+ }
315
+ setBlocked(true);
316
+ setVisible(true);
317
+ setOpen(false);
318
+ setSession(null);
319
+ setMessages([]);
320
+ setDraft("");
321
+ setError("");
322
+ setLoading(true);
323
+ if (reload && !banReloadedRef.current) {
324
+ banReloadedRef.current = true;
325
+ window.setTimeout(() => window.location.reload(), 120);
326
+ }
327
+ }
328
+ function leaveBlockedState() {
329
+ try {
330
+ window.localStorage.removeItem(blockedKey);
331
+ }
332
+ catch {
333
+ // Ignore storage failures; clearing React state is enough for this tab.
334
+ }
335
+ setBlocked(false);
336
+ }
231
337
  useEffect(() => {
232
338
  openRef.current = open;
233
339
  if (open) {
@@ -239,7 +345,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
239
345
  }
240
346
  const socket = wsRef.current;
241
347
  if (socket?.readyState === WebSocket.OPEN) {
242
- socket.send(JSON.stringify({ type: "state", open }));
348
+ sendStateNow();
243
349
  }
244
350
  }, [open]);
245
351
  useEffect(() => {
@@ -261,9 +367,9 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
261
367
  return () => window.removeEventListener("keydown", onKeyDown);
262
368
  }, []);
263
369
  useEffect(() => {
264
- setTidioTemporarilyHidden(visible);
370
+ setTidioTemporarilyHidden(visible || blocked);
265
371
  return () => setTidioTemporarilyHidden(false);
266
- }, [visible]);
372
+ }, [blocked, visible]);
267
373
  useEffect(() => {
268
374
  if (profileReady)
269
375
  return;
@@ -310,6 +416,63 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
310
416
  return;
311
417
  messagesEndRef.current?.scrollIntoView({ block: "end" });
312
418
  }, [messages.length, staffTypingName, open, visible]);
419
+ useEffect(() => {
420
+ if (!session)
421
+ return;
422
+ pageUrlsRef.current.add(liveChatCurrentUrl());
423
+ let lastUrl = liveChatCurrentUrl();
424
+ function recordPageView() {
425
+ const currentUrl = liveChatCurrentUrl();
426
+ if (!currentUrl || currentUrl === lastUrl)
427
+ return;
428
+ lastUrl = currentUrl;
429
+ pageUrlsRef.current.add(currentUrl);
430
+ scheduleStateBroadcast(120);
431
+ }
432
+ function recordFieldTouch(event) {
433
+ const target = event.target;
434
+ if (!(target instanceof Element))
435
+ return;
436
+ const key = liveChatTrackedFieldKey(target);
437
+ if (!key || touchedFieldsRef.current.has(key))
438
+ return;
439
+ touchedFieldsRef.current.add(key);
440
+ scheduleStateBroadcast(220);
441
+ }
442
+ const originalPushState = window.history.pushState;
443
+ const originalReplaceState = window.history.replaceState;
444
+ window.history.pushState = function patchedPushState(...args) {
445
+ const result = originalPushState.apply(this, args);
446
+ window.setTimeout(recordPageView, 0);
447
+ return result;
448
+ };
449
+ window.history.replaceState = function patchedReplaceState(...args) {
450
+ const result = originalReplaceState.apply(this, args);
451
+ window.setTimeout(recordPageView, 0);
452
+ return result;
453
+ };
454
+ const interval = window.setInterval(recordPageView, 1000);
455
+ window.addEventListener("popstate", recordPageView);
456
+ window.addEventListener("hashchange", recordPageView);
457
+ document.addEventListener("focusin", recordFieldTouch, true);
458
+ document.addEventListener("input", recordFieldTouch, true);
459
+ document.addEventListener("change", recordFieldTouch, true);
460
+ scheduleStateBroadcast(350);
461
+ return () => {
462
+ window.history.pushState = originalPushState;
463
+ window.history.replaceState = originalReplaceState;
464
+ window.clearInterval(interval);
465
+ window.removeEventListener("popstate", recordPageView);
466
+ window.removeEventListener("hashchange", recordPageView);
467
+ document.removeEventListener("focusin", recordFieldTouch, true);
468
+ document.removeEventListener("input", recordFieldTouch, true);
469
+ document.removeEventListener("change", recordFieldTouch, true);
470
+ if (stateBroadcastTimeoutRef.current) {
471
+ window.clearTimeout(stateBroadcastTimeoutRef.current);
472
+ stateBroadcastTimeoutRef.current = null;
473
+ }
474
+ };
475
+ }, [session]);
313
476
  useEffect(() => {
314
477
  if (!profileReady || session || !profile)
315
478
  return;
@@ -318,6 +481,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
318
481
  const currentProfile = profile;
319
482
  let cancelled = false;
320
483
  async function startSession() {
484
+ let keepLoading = false;
321
485
  if (visible && open)
322
486
  setLoading(true);
323
487
  setError("");
@@ -331,12 +495,24 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
331
495
  lead_source: leadSource,
332
496
  guest_name: formatGuestName(currentProfile.name),
333
497
  page_url: window.location.href,
498
+ referrer_url: document.referrer || "",
499
+ utm_source: liveChatQueryParam("utm_source"),
500
+ utm_medium: liveChatQueryParam("utm_medium"),
501
+ utm_campaign: liveChatQueryParam("utm_campaign"),
334
502
  device_signature: signature,
335
503
  }),
336
504
  });
505
+ if (response.status === LIVE_CHAT_BLOCKED_STATUS) {
506
+ keepLoading = true;
507
+ if (!cancelled) {
508
+ enterBlockedState();
509
+ }
510
+ return;
511
+ }
337
512
  const envelope = await readJson(response);
338
513
  if (cancelled)
339
514
  return;
515
+ leaveBlockedState();
340
516
  window.localStorage.setItem(storageKey, JSON.stringify(envelope.data));
341
517
  setSession(envelope.data);
342
518
  }
@@ -345,7 +521,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
345
521
  setError(err instanceof Error ? err.message : "Chat is unavailable.");
346
522
  }
347
523
  finally {
348
- if (!cancelled)
524
+ if (!cancelled && !keepLoading)
349
525
  setLoading(false);
350
526
  if (startSessionRef.current === startPromise)
351
527
  startSessionRef.current = null;
@@ -356,7 +532,15 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
356
532
  return () => {
357
533
  cancelled = true;
358
534
  };
359
- }, [apiKey, baseUrl, leadSource, open, profile, profileReady, session, storageKey, visible]);
535
+ }, [apiKey, baseUrl, blockedRetryNonce, leadSource, open, profile, profileReady, session, storageKey, visible]);
536
+ useEffect(() => {
537
+ if (!blocked || session || !profileReady || !profile)
538
+ return;
539
+ const timer = window.setTimeout(() => {
540
+ setBlockedRetryNonce((value) => value + 1);
541
+ }, 15000);
542
+ return () => window.clearTimeout(timer);
543
+ }, [blocked, profile, profileReady, session]);
360
544
  useEffect(() => {
361
545
  if (!visible || !open || !session)
362
546
  return;
@@ -401,7 +585,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
401
585
  if (cancelled || activeSocket !== socket)
402
586
  return;
403
587
  reconnectAttempts = 0;
404
- socket.send(JSON.stringify({ type: "state", open: openRef.current }));
588
+ sendStateNow();
405
589
  };
406
590
  socket.onmessage = (event) => {
407
591
  if (cancelled || activeSocket !== socket)
@@ -419,19 +603,26 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
419
603
  typingTimeoutRef.current = setTimeout(() => setStaffTypingName(""), 2200);
420
604
  return;
421
605
  }
422
- if (payload.action === "session_removed") {
606
+ if (payload.action === "session_removed" || payload.action === "session_banned") {
423
607
  window.localStorage.removeItem(storageKey);
424
- window.localStorage.removeItem(profileKey);
608
+ if (payload.action === "session_removed" && payload.clear_profile !== false) {
609
+ window.localStorage.removeItem(profileKey);
610
+ }
425
611
  setSession(null);
426
612
  setMessages([]);
427
613
  setDraft("");
428
- setNameDraft("");
429
- setProfile(null);
430
- setProfileReady(false);
614
+ if (payload.action === "session_removed" && payload.clear_profile !== false) {
615
+ setNameDraft("");
616
+ setProfile(null);
617
+ setProfileReady(false);
618
+ }
431
619
  setStaffTypingName("");
432
620
  setUnreadCount(0);
433
621
  setUnreadPreview("");
434
622
  setPreviewVisible(false);
623
+ if (payload.action === "session_banned") {
624
+ enterBlockedState({ reload: true });
625
+ }
435
626
  socket.close();
436
627
  return;
437
628
  }
@@ -498,7 +689,8 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
498
689
  if (reconnectTimer)
499
690
  clearTimeout(reconnectTimer);
500
691
  if (activeSocket?.readyState === WebSocket.OPEN) {
501
- activeSocket.send(JSON.stringify({ type: "state", open: false }));
692
+ openRef.current = false;
693
+ activeSocket.send(JSON.stringify(buildStatePayload()));
502
694
  }
503
695
  endPresence();
504
696
  activeSocket?.close();
@@ -565,7 +757,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
565
757
  const rootStyle = {
566
758
  "--os-chat-accent": accentColor,
567
759
  };
568
- 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: [_jsxs("strong", { children: ["Hi there ", _jsx("span", { "aria-hidden": true, children: "\uD83D\uDC4B" })] }), 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: "How may we help you today?" })] })) : (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) => {
760
+ return (_jsxs("div", { className: `os-live-chat ${blocked || visible ? "is-visible" : "is-hidden"} ${blocked ? "is-blocked" : ""}`, style: rootStyle, children: [blocked ? (_jsx("div", { className: "os-live-chat-blocker", "aria-live": "polite", "aria-busy": "true", children: _jsx("div", { className: "os-live-chat-blocker-card", "aria-label": "Loading", children: _jsx(Loader2, { size: 32, className: "os-live-chat-spin", "aria-hidden": true }) }) })) : null, !blocked && 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: [_jsxs("strong", { children: ["Hi there ", _jsx("span", { "aria-hidden": true, children: "\uD83D\uDC4B" })] }), 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: "How may we help you today?" })] })) : (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) => {
569
761
  const nextDraft = event.target.value;
570
762
  setDraft(nextDraft);
571
763
  announceTyping(nextDraft);
@@ -574,7 +766,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
574
766
  event.preventDefault();
575
767
  event.currentTarget.form?.requestSubmit();
576
768
  }
577
- } }), _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, !open ? (_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 })] })) : (_jsxs(_Fragment, { children: ["Need live assistance? ", _jsx("small", { className: "os-live-chat-wave", "aria-hidden": true, children: "\uD83D\uDC4B" })] })) })) : null] })) : null, _jsx("style", { children: `
769
+ } }), _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, !blocked && 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, !open ? (_jsx("span", { className: unreadCount > 0 ? `has-unread ${previewVisible ? "is-preview-visible" : "is-preview-hidden"}` : "", "aria-live": unreadCount > 0 ? "polite" : undefined, children: unreadCount > 0 && previewVisible ? (_jsxs(_Fragment, { children: [_jsx("b", { children: "New message" }), _jsx("small", { children: unreadPreview })] })) : (_jsxs(_Fragment, { children: ["Need live assistance? ", _jsx("small", { className: "os-live-chat-wave", "aria-hidden": true, children: "\uD83D\uDC4B" })] })) }, unreadCount > 0 ? `preview-${unreadPreview}` : "assist-prompt")) : null] })) : null, _jsx("style", { children: `
578
770
  .os-live-chat {
579
771
  position: fixed;
580
772
  right: max(18px, env(safe-area-inset-right));
@@ -585,6 +777,34 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
585
777
  .os-live-chat.is-hidden {
586
778
  pointer-events: none;
587
779
  }
780
+ .os-live-chat.is-blocked {
781
+ pointer-events: auto;
782
+ }
783
+ .os-live-chat-blocker {
784
+ position: fixed;
785
+ inset: 0;
786
+ z-index: 2147483000;
787
+ display: grid;
788
+ place-items: center;
789
+ padding: 24px;
790
+ background:
791
+ radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--os-chat-accent) 14%, transparent), transparent 34%),
792
+ rgba(248, 250, 252, 0.98);
793
+ backdrop-filter: blur(18px);
794
+ -webkit-backdrop-filter: blur(18px);
795
+ animation: os-chat-blocker-in 320ms cubic-bezier(.16,1,.3,1) both;
796
+ }
797
+ .os-live-chat-blocker-card {
798
+ display: grid;
799
+ width: 84px;
800
+ height: 84px;
801
+ place-items: center;
802
+ border: 1px solid rgba(148, 163, 184, 0.24);
803
+ border-radius: 28px;
804
+ color: var(--os-chat-accent);
805
+ background: rgba(255, 255, 255, 0.86);
806
+ box-shadow: 0 24px 80px rgba(15, 23, 42, 0.16);
807
+ }
588
808
  .os-live-chat-bubble {
589
809
  position: relative;
590
810
  display: grid;
@@ -602,8 +822,8 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
602
822
  }
603
823
  .os-live-chat-bubble span {
604
824
  position: absolute;
605
- right: 48px;
606
- bottom: 10px;
825
+ right: 68px;
826
+ bottom: 11px;
607
827
  width: max-content;
608
828
  max-width: 230px;
609
829
  border: 1px solid rgba(148, 163, 184, 0.28);
@@ -615,25 +835,27 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
615
835
  font-size: 14px;
616
836
  font-weight: 800;
617
837
  pointer-events: none;
618
- animation: os-chat-nudge 420ms ease-out 350ms both;
838
+ transform-origin: bottom right;
839
+ animation: os-chat-nudge 520ms cubic-bezier(.16,1,.3,1) 350ms both;
619
840
  }
620
841
  .os-live-chat-bubble span.has-unread {
621
842
  display: grid;
622
843
  gap: 2px;
623
- width: min(260px, calc(100vw - 106px));
844
+ width: min(260px, calc(100vw - 130px));
624
845
  max-width: 260px;
625
- border-radius: 18px;
626
- padding: 10px 13px;
846
+ border-radius: 20px;
847
+ padding: 11px 14px;
627
848
  text-align: left;
849
+ box-shadow: 0 18px 44px rgba(15, 23, 42, 0.16);
628
850
  }
629
851
  .os-live-chat-bubble span.has-unread.is-preview-visible {
630
- animation: os-chat-preview-in 220ms cubic-bezier(.2,.8,.2,1) both;
852
+ animation: os-chat-preview-in 420ms cubic-bezier(.16,1,.3,1) both;
631
853
  }
632
854
  .os-live-chat-bubble span.has-unread.is-preview-hidden {
633
855
  opacity: 0;
634
- transform: translateX(10px) scale(0.98);
635
856
  visibility: hidden;
636
- transition: opacity 220ms ease, transform 220ms ease, visibility 0s linear 220ms;
857
+ animation: os-chat-preview-out 360ms cubic-bezier(.4,0,.2,1) both;
858
+ transition: visibility 0s linear 360ms;
637
859
  }
638
860
  .os-live-chat-bubble span.has-unread b,
639
861
  .os-live-chat-bubble span.has-unread small {
@@ -697,7 +919,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
697
919
  border-radius: 28px;
698
920
  background: rgba(255, 255, 255, 0.98);
699
921
  box-shadow: 0 28px 80px rgba(15, 23, 42, 0.24);
700
- animation: os-chat-in 210ms cubic-bezier(.2,.8,.2,1);
922
+ animation: os-chat-in 360ms cubic-bezier(.16,1,.3,1);
701
923
  transform-origin: bottom right;
702
924
  }
703
925
  .os-live-chat-header {
@@ -883,6 +1105,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
883
1105
  gap: 10px;
884
1106
  overflow-y: auto;
885
1107
  padding: 16px;
1108
+ scroll-behavior: smooth;
886
1109
  background:
887
1110
  radial-gradient(circle at 22% 0%, color-mix(in srgb, var(--os-chat-accent) 8%, transparent), transparent 32%),
888
1111
  #f8fafc;
@@ -908,7 +1131,13 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
908
1131
  font-size: 14px;
909
1132
  line-height: 1.45;
910
1133
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
911
- animation: os-message-in 180ms ease-out both;
1134
+ will-change: transform, opacity;
1135
+ animation: os-message-in 340ms cubic-bezier(.16,1,.3,1) both;
1136
+ transition: transform 260ms cubic-bezier(.16,1,.3,1), box-shadow 260ms ease, opacity 260ms ease;
1137
+ }
1138
+ .os-live-chat-message:hover {
1139
+ transform: translateY(-1px);
1140
+ box-shadow: 0 11px 26px rgba(15, 23, 42, 0.09);
912
1141
  }
913
1142
  .os-live-chat-message p {
914
1143
  margin: 0;
@@ -947,7 +1176,7 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
947
1176
  background: white;
948
1177
  font-size: 12px;
949
1178
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
950
- animation: os-message-in 180ms ease-out both;
1179
+ animation: os-message-in 340ms cubic-bezier(.16,1,.3,1) both;
951
1180
  }
952
1181
  .os-live-chat-typing i {
953
1182
  width: 5px;
@@ -1018,11 +1247,13 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
1018
1247
  @keyframes os-chat-in {
1019
1248
  from {
1020
1249
  opacity: 0;
1021
- transform: translateY(12px) scale(0.97);
1250
+ transform: translateY(18px) scale(0.965);
1251
+ filter: blur(5px);
1022
1252
  }
1023
1253
  to {
1024
1254
  opacity: 1;
1025
1255
  transform: translateY(0) scale(1);
1256
+ filter: blur(0);
1026
1257
  }
1027
1258
  }
1028
1259
  @keyframes os-chat-bubble-in {
@@ -1035,6 +1266,16 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
1035
1266
  transform: translateY(0) scale(1);
1036
1267
  }
1037
1268
  }
1269
+ @keyframes os-chat-blocker-in {
1270
+ from {
1271
+ opacity: 0;
1272
+ filter: blur(6px);
1273
+ }
1274
+ to {
1275
+ opacity: 1;
1276
+ filter: blur(0);
1277
+ }
1278
+ }
1038
1279
  @keyframes os-chat-nudge {
1039
1280
  from {
1040
1281
  opacity: 0;
@@ -1059,21 +1300,37 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
1059
1300
  @keyframes os-chat-preview-in {
1060
1301
  from {
1061
1302
  opacity: 0;
1062
- transform: translateX(10px) scale(0.98);
1303
+ transform: translateX(14px) translateY(4px) scale(0.96);
1304
+ filter: blur(4px);
1063
1305
  }
1064
1306
  to {
1065
1307
  opacity: 1;
1066
1308
  transform: translateX(0) scale(1);
1309
+ filter: blur(0);
1310
+ }
1311
+ }
1312
+ @keyframes os-chat-preview-out {
1313
+ from {
1314
+ opacity: 1;
1315
+ transform: translateX(0) scale(1);
1316
+ filter: blur(0);
1317
+ }
1318
+ to {
1319
+ opacity: 0;
1320
+ transform: translateX(10px) translateY(3px) scale(0.96);
1321
+ filter: blur(4px);
1067
1322
  }
1068
1323
  }
1069
1324
  @keyframes os-message-in {
1070
1325
  from {
1071
1326
  opacity: 0;
1072
- transform: translateY(7px) scale(0.98);
1327
+ transform: translateY(10px) scale(0.975);
1328
+ filter: blur(3px);
1073
1329
  }
1074
1330
  to {
1075
1331
  opacity: 1;
1076
1332
  transform: translateY(0) scale(1);
1333
+ filter: blur(0);
1077
1334
  }
1078
1335
  }
1079
1336
  @keyframes os-typing-dot {
@@ -1105,6 +1362,26 @@ export function LiveChatWidget({ apiBaseUrl, apiKey, brandName, leadSource, acce
1105
1362
  .os-live-chat-bubble span:not(.has-unread) {
1106
1363
  display: none;
1107
1364
  }
1365
+ .os-live-chat-bubble span.has-unread {
1366
+ right: 68px;
1367
+ width: min(242px, calc(100vw - 116px));
1368
+ }
1369
+ }
1370
+ @media (prefers-reduced-motion: reduce) {
1371
+ .os-live-chat-bubble,
1372
+ .os-live-chat-bubble span,
1373
+ .os-live-chat-bubble span.has-unread.is-preview-visible,
1374
+ .os-live-chat-bubble span.has-unread.is-preview-hidden,
1375
+ .os-live-chat-panel,
1376
+ .os-live-chat-message,
1377
+ .os-live-chat-typing,
1378
+ .os-live-chat-wave {
1379
+ animation: none !important;
1380
+ transition: none !important;
1381
+ }
1382
+ .os-live-chat-body {
1383
+ scroll-behavior: auto;
1384
+ }
1108
1385
  }
1109
1386
  ` })] }));
1110
1387
  }