@xapp/chat-widget 1.96.3 → 1.97.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.es.js CHANGED
@@ -78,7 +78,9 @@ color.invertColorHEX = invertColorHEX;
78
78
  color.invertColorRgb = invertColorRgb;
79
79
  color.invertColor = invertColor;
80
80
  color.isSupportedColor = isSupportedColor;
81
+ color.usableColorOr = usableColorOr;
81
82
  color.readableTextOn = readableTextOn;
83
+ color.darkenColor = darkenColor;
82
84
  /**
83
85
  * Colour maths for building a widget theme from a business's own branding.
84
86
  *
@@ -187,6 +189,19 @@ function isSupportedColor(color) {
187
189
  }
188
190
  return parseRgb(color) !== null;
189
191
  }
192
+ /**
193
+ * The colour to theme with: the one given, or the fallback when we cannot work
194
+ * out what text reads on it.
195
+ *
196
+ * Every builder needs this and each was doing it by hand, which is how the
197
+ * chat widget ended up guarded and the other two not. A background whose
198
+ * contrast cannot be computed is one we must not use — the foreground would be
199
+ * a guess paired with an unknown, and on a dark unparseable colour that is
200
+ * invisible text.
201
+ */
202
+ function usableColorOr(color, fallback) {
203
+ return isSupportedColor(color) ? color : fallback;
204
+ }
190
205
  /**
191
206
  * The text colour that will read on this background: black or white, never
192
207
  * anything else.
@@ -226,6 +241,77 @@ function hexToRgb(hex) {
226
241
  b: parseInt(value.slice(4, 6), 16)
227
242
  };
228
243
  }
244
+ /**
245
+ * A shade of the input colour, darkened by `amount` percent (negative
246
+ * lightens). Ported alongside the rest so the form widget's theme does not
247
+ * need its own copy of it.
248
+ */
249
+ function darkenColor(colorIn, amount) {
250
+ if (!colorIn) {
251
+ return colorIn;
252
+ }
253
+ let r, g, b, a;
254
+ let colorOut;
255
+ try {
256
+ // Parse the color format
257
+ if (colorIn.startsWith('#')) {
258
+ // Hex format
259
+ const hexMatch = colorIn.match(/\w\w/g);
260
+ if (hexMatch) {
261
+ [r, g, b] = hexMatch.map(x => parseInt(x, 16));
262
+ }
263
+ }
264
+ else if (colorIn.startsWith('rgb')) {
265
+ // RGB or RGBA format
266
+ const rgba = parseRgb(colorIn);
267
+ if (rgba) {
268
+ ({ r, g, b, a } = rgba);
269
+ }
270
+ }
271
+ if (r === undefined || g === undefined || b === undefined) {
272
+ console.error(`Unsupported color format: ${colorIn}`);
273
+ return colorIn;
274
+ }
275
+ if (a !== undefined) {
276
+ // Adjust luminance for RGBA
277
+ // Luminance is on the 0-255 scale the channels are, so the clamp
278
+ // has to be too. Bounding it to 0-1 collapsed it to 1 for every
279
+ // real colour, which made the ratio tiny and crushed the result to
280
+ // near-black instead of darkening it: rgba(20, 52, 204, 0.8) at 20%
281
+ // came back rgba(0, 1, 3, 1).
282
+ const luminance = 0.3 * r + 0.59 * g + 0.11 * b;
283
+ let newLuminance = luminance * (1 - amount / 100);
284
+ newLuminance = Math.max(0, Math.min(255, newLuminance));
285
+ const ratio = newLuminance / (luminance || 1); // no division by zero
286
+ r = Math.round(r * ratio);
287
+ g = Math.round(g * ratio);
288
+ b = Math.round(b * ratio);
289
+ // Adjust alpha
290
+ a = a + amount / 100;
291
+ a = Math.max(0, Math.min(1, a)); // within 0 and 1
292
+ colorOut = `rgba(${r}, ${g}, ${b}, ${a})`;
293
+ }
294
+ else {
295
+ // Adjust the color for RGB or Hex
296
+ if (amount > 0) {
297
+ r = Math.max(0, r - amount);
298
+ g = Math.max(0, g - amount);
299
+ b = Math.max(0, b - amount);
300
+ }
301
+ else {
302
+ amount = Math.abs(amount); // positive for lightening
303
+ r = Math.min(255, r + amount);
304
+ g = Math.min(255, g + amount);
305
+ b = Math.min(255, b + amount);
306
+ }
307
+ colorOut = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
308
+ }
309
+ }
310
+ catch (e) {
311
+ console.error(`Unsupported color format: ${colorIn} - Error: ${e}`);
312
+ }
313
+ return colorOut;
314
+ }
229
315
 
230
316
  Object.defineProperty(buildChatWidgetChannel$1, "__esModule", { value: true });
231
317
  buildChatWidgetChannel$1.buildChatWidgetChannel = buildChatWidgetChannel;
@@ -238,7 +324,7 @@ function buildChatWidgetChannel(props) {
238
324
  // Only a colour whose contrast can actually be computed, so the background
239
325
  // and the text on it are always a matched pair rather than one scraped
240
326
  // value and one guess.
241
- const primaryColor = (0, color_1.isSupportedColor)(props.primaryColor) ? props.primaryColor : DEFAULT_PRIMARY;
327
+ const primaryColor = (0, color_1.usableColorOr)(props.primaryColor, DEFAULT_PRIMARY);
242
328
  return {
243
329
  type: "chat-widget",
244
330
  // MCP by default. The widget runs on a public page, so homeowner mode
@@ -252,7 +338,7 @@ function buildChatWidgetChannel(props) {
252
338
  mode: "homeowner"
253
339
  }
254
340
  },
255
- theme: buildChatTheme(primaryColor),
341
+ theme: buildChatTheme(primaryColor, props.secondaryColor),
256
342
  actionBar: buildActionBar({ apiUrl, formWidgetKey, phoneNumber })
257
343
  };
258
344
  }
@@ -303,19 +389,49 @@ function buildActionBar(props) {
303
389
  };
304
390
  }
305
391
  /**
306
- * A theme derived from the business's own colour.
392
+ * The theme a new chat widget starts with, from the business's own colour.
393
+ *
394
+ * A faithful port of stentor-client's buildDefaultChatTheme, deliberately: the
395
+ * point of moving it is that onboarding can call this instead of its own copy,
396
+ * and that migration should not restyle every widget created afterwards. A
397
+ * thinner theme would have quietly dropped the CTA, message, input, footer and
398
+ * button styling that new widgets have today.
307
399
  *
308
- * Only the text colour is computed rather than chosen: which of black or white
309
- * reads on a given background is arithmetic, and getting it wrong ships a
310
- * widget nobody can read.
400
+ * The one change is how the contrast colour is derived — see below.
311
401
  */
312
- function buildChatTheme(primaryColor) {
313
- const onPrimary = (0, color_1.readableTextOn)(primaryColor);
402
+ function buildChatTheme(primaryColor, secondaryColor) {
403
+ const fontFamily = "RobotoRegular";
404
+ // secondaryColor is the colour of text sitting ON primaryColor — header
405
+ // title, subtitle, footer branding. Left unset it is computed, which is
406
+ // what makes the theme readable against whatever colour was scraped off
407
+ // the business's own site.
408
+ if (!secondaryColor) {
409
+ secondaryColor = (0, color_1.readableTextOn)(primaryColor);
410
+ }
314
411
  return {
315
412
  zIndex: "10000",
316
413
  border: {
317
414
  color: primaryColor,
318
- radius: "10px"
415
+ radius: "10px",
416
+ },
417
+ carousel: {
418
+ button: {
419
+ color: primaryColor,
420
+ },
421
+ subtitle: {
422
+ color: "#979797",
423
+ fontFamily,
424
+ fontSize: "12px",
425
+ fontStyle: "normal",
426
+ fontWeight: "normal",
427
+ },
428
+ title: {
429
+ color: "#000",
430
+ fontFamily,
431
+ fontSize: "14px",
432
+ fontStyle: "normal",
433
+ fontWeight: "bold",
434
+ },
319
435
  },
320
436
  chatButton: {
321
437
  background: primaryColor,
@@ -323,18 +439,147 @@ function buildChatTheme(primaryColor) {
323
439
  bottom: "10px",
324
440
  left: "10px",
325
441
  right: "10px",
326
- top: "10px"
327
- }
442
+ top: "10px",
443
+ },
328
444
  },
329
445
  content: {
330
- background: "#ffffff"
446
+ background: "#ffffff",
447
+ },
448
+ cta: {
449
+ background: "#e1e2e1",
450
+ text: {
451
+ // Use a dark color that contrasts well with the light grey background
452
+ // instead of primaryColor which may not have sufficient contrast
453
+ color: "#333333",
454
+ fontFamily,
455
+ fontSize: "14px",
456
+ },
457
+ },
458
+ footer: {
459
+ background: primaryColor,
460
+ border: {
461
+ color: primaryColor,
462
+ radius: "0px",
463
+ width: "0px",
464
+ },
465
+ branding: {
466
+ text: {
467
+ color: "#ffffff",
468
+ fontFamily,
469
+ fontSize: "14px",
470
+ fontWeight: "normal",
471
+ fontStyle: "normal",
472
+ },
473
+ },
331
474
  },
332
475
  header: {
333
476
  background: primaryColor,
477
+ avatarBackground: "transparent",
478
+ border: {
479
+ color: primaryColor,
480
+ radius: "0px",
481
+ width: "0px",
482
+ },
334
483
  text: {
335
- color: onPrimary
336
- }
337
- }
484
+ color: secondaryColor,
485
+ fontFamily,
486
+ fontSize: "16px",
487
+ fontWeight: "normal",
488
+ fontStyle: "normal",
489
+ },
490
+ subtitle: {
491
+ color: secondaryColor,
492
+ fontFamily,
493
+ fontSize: "10px",
494
+ fontWeight: "normal",
495
+ fontStyle: "normal",
496
+ },
497
+ },
498
+ input: {
499
+ background: "#ffffff",
500
+ border: {
501
+ radius: "20px",
502
+ },
503
+ text: {
504
+ color: "#000000",
505
+ fontFamily,
506
+ fontSize: "14px",
507
+ },
508
+ placeholder: {
509
+ color: "#e1e2e1",
510
+ fontFamily,
511
+ fontSize: "14px",
512
+ fontWeight: "normal",
513
+ fontStyle: "normal",
514
+ },
515
+ },
516
+ textTypingStatus: {
517
+ text: {
518
+ color: "#bfbfbf",
519
+ fontFamily,
520
+ fontSize: "14px",
521
+ fontWeight: "normal",
522
+ fontStyle: "normal",
523
+ },
524
+ },
525
+ menu: {
526
+ item: {
527
+ height: "24px",
528
+ text: {
529
+ fontSize: "14px",
530
+ },
531
+ },
532
+ },
533
+ menuButton: {
534
+ color: "#ffffff",
535
+ },
536
+ messages: {
537
+ maxWidth: "400px",
538
+ mine: {
539
+ bubbleColor: "#f2f2f2",
540
+ text: {
541
+ color: "#454545",
542
+ fontFamily,
543
+ fontSize: "18px",
544
+ fontStyle: "normal",
545
+ fontWeight: "normal",
546
+ },
547
+ },
548
+ others: {
549
+ bubbleColor: "#e1e2e1",
550
+ text: {
551
+ color: "#262726",
552
+ fontFamily,
553
+ fontSize: "18px",
554
+ fontStyle: "normal",
555
+ fontWeight: "normal",
556
+ },
557
+ },
558
+ padding: {
559
+ bottom: "0px",
560
+ left: "20px",
561
+ right: "16px",
562
+ top: "16px",
563
+ },
564
+ },
565
+ refreshButton: {
566
+ color: secondaryColor,
567
+ },
568
+ minimizeButton: {
569
+ color: secondaryColor,
570
+ },
571
+ cancelButton: {
572
+ color: secondaryColor,
573
+ },
574
+ primaryColor: primaryColor,
575
+ sendButton: {
576
+ color: "#505050",
577
+ width: "20px",
578
+ },
579
+ size: {
580
+ width: "500px",
581
+ height: "700px",
582
+ },
338
583
  };
339
584
  }
340
585
 
@@ -687,9 +932,39 @@ var MOBILE_MEDIA_QUERY = "(max-width: 640px), (max-height: 480px) and (pointer:
687
932
  * Two concurrent bars would fight over it, and either one unmounting would clear it.
688
933
  */
689
934
  var ACTION_BAR_HEIGHT_VAR = "--xapp-action-bar-height";
935
+ /**
936
+ * Whether pressing this button ends up in the form widget.
937
+ *
938
+ * `schedule` is the same form-widget infrastructure with a calendar icon, so it is the
939
+ * same button as far as this question goes. XappMedia/chat-widget#1626.
940
+ */
941
+ function opensFormWidget(button) {
942
+ return lib.isActionBarFormButton(button) || lib.isActionBarScheduleButton(button);
943
+ }
944
+ /**
945
+ * Which of a bar's configured buttons would actually render.
946
+ *
947
+ * Exported because the decision is needed in two places: here, to render, and in
948
+ * `ChatWidget`, to know whether the bar has anything left to show and whether anything on
949
+ * the page can still open the form widget. Two copies of this predicate would drift.
950
+ */
951
+ function visibleActionBarButtons(buttons, options) {
952
+ if (buttons === void 0) { buttons = []; }
953
+ if (options === void 0) { options = {}; }
954
+ var chatDisabled = options.chatDisabled, formDisabled = options.formDisabled;
955
+ return buttons.filter(function (button) {
956
+ if (chatDisabled && lib.isActionBarChatButton(button)) {
957
+ return false;
958
+ }
959
+ if (formDisabled && opensFormWidget(button)) {
960
+ return false;
961
+ }
962
+ return true;
963
+ });
964
+ }
690
965
  var ActionBar = function (_a) {
691
966
  var _b, _c;
692
- var config = _a.config, visible = _a.visible, chatDisabled = _a.chatDisabled, _d = _a.hasUserInteracted, hasUserInteracted = _d === void 0 ? false : _d, onChatClick = _a.onChatClick, onChatMinimize = _a.onChatMinimize, onFormClick = _a.onFormClick, onCtaDismiss = _a.onCtaDismiss;
967
+ var config = _a.config, visible = _a.visible, chatDisabled = _a.chatDisabled, formDisabled = _a.formDisabled, _d = _a.hasUserInteracted, hasUserInteracted = _d === void 0 ? false : _d, onChatClick = _a.onChatClick, onChatMinimize = _a.onChatMinimize, onFormClick = _a.onFormClick, onCtaDismiss = _a.onCtaDismiss;
693
968
  var _e = useState(false), isMobile = _e[0], setIsMobile = _e[1];
694
969
  // An explicit override can only be a width; without one we ask the same question the
695
970
  // stylesheets ask, so the two cannot disagree. A survey of production on 2026-08-19
@@ -760,10 +1035,9 @@ var ActionBar = function (_a) {
760
1035
  document.documentElement.style.removeProperty(ACTION_BAR_HEIGHT_VAR);
761
1036
  };
762
1037
  }, [isMobile]);
763
- // Filter out chat buttons if chat is disabled
764
- var filteredButtons = chatDisabled
765
- ? config.buttons.filter(function (button) { return !lib.isActionBarChatButton(button); })
766
- : config.buttons;
1038
+ // Drop the buttons this page has no use for - chat when chat is disabled, form/schedule
1039
+ // when the form already is the page.
1040
+ var filteredButtons = visibleActionBarButtons(config.buttons, { chatDisabled: chatDisabled, formDisabled: formDisabled });
767
1041
  // Build class names - ActionBar stays visible even when chat is open
768
1042
  var classNames = [
769
1043
  "xapp-action-bar",
@@ -776,7 +1050,7 @@ var ActionBar = function (_a) {
776
1050
  // - Must have CTA config with a message
777
1051
  // - Chat must not be visible (user hasn't opened it)
778
1052
  // - Chat must not be disabled
779
- var hasChatButton = config.buttons.some(lib.isActionBarChatButton);
1053
+ var hasChatButton = filteredButtons.some(lib.isActionBarChatButton);
780
1054
  var showCta = ((_c = config.cta) === null || _c === void 0 ? void 0 : _c.message) && !visible && !chatDisabled && hasChatButton;
781
1055
  var handleCtaClick = function () {
782
1056
  onChatClick();
@@ -784,6 +1058,13 @@ var ActionBar = function (_a) {
784
1058
  var handleCtaDismiss = function () {
785
1059
  onCtaDismiss === null || onCtaDismiss === void 0 ? void 0 : onCtaDismiss();
786
1060
  };
1061
+ // A bar with nothing in it is a bare strip of chrome the visitor cannot use. The bar
1062
+ // renders on `actionBar.enabled` alone, so an App configured with only Form + Schedule
1063
+ // filters down to zero on a form page - render nothing rather than an empty shell.
1064
+ // This is after the hooks on purpose; bailing earlier would change the hook order.
1065
+ if (filteredButtons.length === 0) {
1066
+ return null;
1067
+ }
787
1068
  return (jsxs("div", { ref: barRef, className: classNames, role: "toolbar", "aria-label": "Chat actions", children: [showCta && (jsx("div", { className: "xapp-action-bar-cta-wrapper", children: jsx(ActionBarCta, { config: config.cta, isMobile: isMobile, onClick: handleCtaClick, onDismiss: handleCtaDismiss, hasUserInteracted: hasUserInteracted }) })), jsx("div", { className: "xapp-action-bar__buttons", children: filteredButtons.map(function (button) { return (jsx(ActionBarButton, { button: button, chatActive: visible, onChatClick: onChatClick, onChatMinimize: onChatMinimize, onFormClick: onFormClick, isMobile: isMobile }, button.id)); }) })] }));
788
1069
  };
789
1070
 
@@ -12552,6 +12833,64 @@ function getSrcId(url) {
12552
12833
  return res;
12553
12834
  }
12554
12835
 
12836
+ /*! Copyright (c) 2026, XAPPmedia */
12837
+ /**
12838
+ * Body attribute a host page sets to declare "a form widget already *is* this page".
12839
+ *
12840
+ * The standalone contact/booking page (contact.xapp.ai, book.xapp.ai, sche.dule.pro)
12841
+ * renders a docked form-widget as its whole content and then injects the chat widget on
12842
+ * top of it. Without a signal, the injected widget cheerfully offers two more ways into
12843
+ * the same form - an ActionBar Contact/Book button and the form-widget's own side button.
12844
+ *
12845
+ * This is a declaration, not a detection: the page knows what it renders, the widget does
12846
+ * not. Sniffing the DOM for a docked form would be guesswork, and it would also fire on
12847
+ * pages that legitimately embed a form *and* want a separate one from the bar.
12848
+ *
12849
+ * The writer is `@xapp/form-widget`'s `useFormPageMarker` (used by `@xapp/contact-app`).
12850
+ * The two packages ship independently, so this string is the contract between them -
12851
+ * the same arrangement `[data-xapp-form-side-button]` already uses. Do not rename it on
12852
+ * one side only. XappMedia/chat-widget#1626.
12853
+ */
12854
+ var FORM_PAGE_ATTRIBUTE = "data-xapp-form-page";
12855
+ /**
12856
+ * Whether a form widget is already the page, per the marker above.
12857
+ */
12858
+ function isFormPage() {
12859
+ if (typeof document === "undefined" || !document.body) {
12860
+ return false;
12861
+ }
12862
+ return document.body.getAttribute(FORM_PAGE_ATTRIBUTE) === "true";
12863
+ }
12864
+ /**
12865
+ * Reactive form of {@link isFormPage}.
12866
+ *
12867
+ * The marker and the chat widget mount independently - the widget arrives from a
12868
+ * `<script>` tag, so it can win or lose the race against the page's own React tree.
12869
+ * Observing the attribute rather than reading it once means the filter settles correctly
12870
+ * whichever order they land in.
12871
+ */
12872
+ function useIsFormPage() {
12873
+ var _a = useState(isFormPage), formPage = _a[0], setFormPage = _a[1];
12874
+ useEffect(function () {
12875
+ if (typeof document === "undefined" || !document.body) {
12876
+ return undefined;
12877
+ }
12878
+ // Re-read on mount: the marker may have appeared between the initial state and here.
12879
+ setFormPage(isFormPage());
12880
+ // jsdom has MutationObserver, but a customer's browser might not - an old browser
12881
+ // that reads the marker once is still better than one that throws on mount.
12882
+ if (typeof MutationObserver === "undefined") {
12883
+ return undefined;
12884
+ }
12885
+ var observer = new MutationObserver(function () {
12886
+ setFormPage(isFormPage());
12887
+ });
12888
+ observer.observe(document.body, { attributes: true, attributeFilter: [FORM_PAGE_ATTRIBUTE] });
12889
+ return function () { return observer.disconnect(); };
12890
+ }, []);
12891
+ return formPage;
12892
+ }
12893
+
12555
12894
  function useChatServerVisitorId() {
12556
12895
  var dispatch = useChatServerDispatch();
12557
12896
  var clientVisitorId = useSelector(function (state) {
@@ -36796,6 +37135,21 @@ var ChatWidgetUI = function (props) {
36796
37135
  log("Rendering - accountStatus: \"".concat(chatState.accountStatus, "\", connectionStatus: \"").concat(chatState.connection.connectionStatus, "\""));
36797
37136
  var config = props.config, onConnectionStatusChange = props.onConnectionStatusChange;
36798
37137
  var actionBarEnabled = ((_s = config === null || config === void 0 ? void 0 : config.actionBar) === null || _s === void 0 ? void 0 : _s.enabled) === true;
37138
+ // The host page declares this; the widget cannot work it out. On the standalone
37139
+ // contact/booking page the form already *is* the page, which makes every button that
37140
+ // only opens one redundant. XappMedia/chat-widget#1626.
37141
+ var isFormPage = useIsFormPage();
37142
+ var actionBarButtons = useMemo(function () {
37143
+ return actionBarEnabled && (config === null || config === void 0 ? void 0 : config.actionBar)
37144
+ ? visibleActionBarButtons(config.actionBar.buttons, {
37145
+ chatDisabled: config.disabled,
37146
+ formDisabled: isFormPage,
37147
+ })
37148
+ : [];
37149
+ }, [actionBarEnabled, config === null || config === void 0 ? void 0 : config.actionBar, config === null || config === void 0 ? void 0 : config.disabled, isFormPage]);
37150
+ // A bar that filters down to nothing is a bare strip of chrome. Treat it as if the bar
37151
+ // were off, so the visitor still gets the plain chat button rather than an empty shell.
37152
+ var actionBarVisible = actionBarEnabled && actionBarButtons.length > 0;
36799
37153
  // Disable greeting in preview mode
36800
37154
  useGreeting(!isOffline && !props.preChatFormEnabled && visible && !previewMode);
36801
37155
  var connectionStatus = chatState.connection.connectionStatus;
@@ -36805,23 +37159,31 @@ var ChatWidgetUI = function (props) {
36805
37159
  }
36806
37160
  }, [connectionStatus, onConnectionStatusChange]);
36807
37161
  useExternalScript((_t = props.config) === null || _t === void 0 ? void 0 : _t.middlewareUrl);
36808
- // Pre-load form-widget script if formWidgetKey is provided
37162
+ // Only reach for form.xapp.ai when something on this page can still open the form.
37163
+ //
37164
+ // Loading it is not free: the script mounts a second form-widget instance whose wrapper
37165
+ // renders its own sticky "Contact Us" side button. On a page that already is the form
37166
+ // that button is a third way into the same thing, so not loading the script at all is
37167
+ // what actually removes it - hiding it in CSS only papers over the second instance.
37168
+ //
37169
+ // With no bar configured the side button IS the entry point, so that case still loads.
37170
+ var formWidgetNeeded = !isFormPage && (!actionBarEnabled || actionBarButtons.some(opensFormWidget));
36809
37171
  useEffect(function () {
36810
- if (config === null || config === void 0 ? void 0 : config.formWidgetKey) {
37172
+ if ((config === null || config === void 0 ? void 0 : config.formWidgetKey) && formWidgetNeeded) {
36811
37173
  preloadFormWidgetScript(config.formWidgetKey);
36812
37174
  }
36813
- }, [config === null || config === void 0 ? void 0 : config.formWidgetKey]);
37175
+ }, [config === null || config === void 0 ? void 0 : config.formWidgetKey, formWidgetNeeded]);
36814
37176
  // Set body attribute when actionBar is enabled (used by CSS to hide form-widget side button on mobile)
36815
37177
  // Uses reference counting to handle multiple widget instances
36816
37178
  useEffect(function () {
36817
- if (actionBarEnabled) {
37179
+ if (actionBarVisible) {
36818
37180
  // Increment reference count
36819
37181
  var currentCount = parseInt(document.body.getAttribute("data-xapp-action-bar-count") || "0", 10);
36820
37182
  document.body.setAttribute("data-xapp-action-bar-count", String(currentCount + 1));
36821
37183
  document.body.setAttribute("data-xapp-action-bar-enabled", "true");
36822
37184
  }
36823
37185
  return function () {
36824
- if (actionBarEnabled) {
37186
+ if (actionBarVisible) {
36825
37187
  // Decrement reference count
36826
37188
  var currentCount = parseInt(document.body.getAttribute("data-xapp-action-bar-count") || "0", 10);
36827
37189
  var newCount = Math.max(0, currentCount - 1);
@@ -36834,7 +37196,7 @@ var ChatWidgetUI = function (props) {
36834
37196
  }
36835
37197
  }
36836
37198
  };
36837
- }, [actionBarEnabled]);
37199
+ }, [actionBarVisible]);
36838
37200
  // This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
36839
37201
  var widgetAgent = ((_u = chatState.agents["agent:robot"]) === null || _u === void 0 ? void 0 : _u.user) ||
36840
37202
  (config === null || config === void 0 ? void 0 : config.agent) || {
@@ -36842,9 +37204,9 @@ var ChatWidgetUI = function (props) {
36842
37204
  avatarPath: config.avatarUrl,
36843
37205
  display_name: "Agent",
36844
37206
  };
36845
- return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarEnabled ? " widget-container--with-action-bar" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_x = (_w = (_v = props.config) === null || _v === void 0 ? void 0 : _v.connection) === null || _w === void 0 ? void 0 : _w.mcp) === null || _x === void 0 ? void 0 : _x.debugMode, onSubmit: handleOnSubmit }), jsx(MessageList, { visible: visible, queuePosition: chatState.queuePosition, isChatting: chatState.isChatting, isOffline: isOffline, messages: messages, agents: chatState.agents, agent: config === null || config === void 0 ? void 0 : config.agent, lastRatingRequestTimestamp: chatState.lastRatingRequestTimestamp, hasRating: chatState.hasRating, visitorId: chatState.visitorId, hasWsButton: !!chatState.wsButton, messageMiddleware: props.messageMiddleware, textTypingStatusEnabled: (_z = (_y = props.config) === null || _y === void 0 ? void 0 : _y.typingStatus) === null || _z === void 0 ? void 0 : _z.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && jsx(ServerOffline, {}), chatState.wsButton && visible && (jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_0 = config === null || config === void 0 ? void 0 : config.input) === null || _0 === void 0 ? void 0 : _0.placeholder, sendButtonIcon: (_2 = (_1 = config === null || config === void 0 ? void 0 : config.footer) === null || _1 === void 0 ? void 0 : _1.sendButton) === null || _2 === void 0 ? void 0 : _2.icon, sendButtonIconHover: (_4 = (_3 = config === null || config === void 0 ? void 0 : config.footer) === null || _3 === void 0 ? void 0 : _3.sendButton) === null || _4 === void 0 ? void 0 : _4.iconHover, sendButtonIconDisabled: (_6 = (_5 = config === null || config === void 0 ? void 0 : config.footer) === null || _5 === void 0 ? void 0 : _5.sendButton) === null || _6 === void 0 ? void 0 : _6.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: (_7 = props.config) === null || _7 === void 0 ? void 0 : _7.menu, footerConfig: (_8 = props.config) === null || _8 === void 0 ? void 0 : _8.footer, inputConfig: (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarEnabled && config.actionBar ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign(__assign({}, config.actionBar), {
37207
+ return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_x = (_w = (_v = props.config) === null || _v === void 0 ? void 0 : _v.connection) === null || _w === void 0 ? void 0 : _w.mcp) === null || _x === void 0 ? void 0 : _x.debugMode, onSubmit: handleOnSubmit }), jsx(MessageList, { visible: visible, queuePosition: chatState.queuePosition, isChatting: chatState.isChatting, isOffline: isOffline, messages: messages, agents: chatState.agents, agent: config === null || config === void 0 ? void 0 : config.agent, lastRatingRequestTimestamp: chatState.lastRatingRequestTimestamp, hasRating: chatState.hasRating, visitorId: chatState.visitorId, hasWsButton: !!chatState.wsButton, messageMiddleware: props.messageMiddleware, textTypingStatusEnabled: (_z = (_y = props.config) === null || _y === void 0 ? void 0 : _y.typingStatus) === null || _z === void 0 ? void 0 : _z.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && jsx(ServerOffline, {}), chatState.wsButton && visible && (jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_0 = config === null || config === void 0 ? void 0 : config.input) === null || _0 === void 0 ? void 0 : _0.placeholder, sendButtonIcon: (_2 = (_1 = config === null || config === void 0 ? void 0 : config.footer) === null || _1 === void 0 ? void 0 : _1.sendButton) === null || _2 === void 0 ? void 0 : _2.icon, sendButtonIconHover: (_4 = (_3 = config === null || config === void 0 ? void 0 : config.footer) === null || _3 === void 0 ? void 0 : _3.sendButton) === null || _4 === void 0 ? void 0 : _4.iconHover, sendButtonIconDisabled: (_6 = (_5 = config === null || config === void 0 ? void 0 : config.footer) === null || _5 === void 0 ? void 0 : _5.sendButton) === null || _6 === void 0 ? void 0 : _6.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: (_7 = props.config) === null || _7 === void 0 ? void 0 : _7.menu, footerConfig: (_8 = props.config) === null || _8 === void 0 ? void 0 : _8.footer, inputConfig: (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign(__assign({}, config.actionBar), {
36846
37208
  // Use actionBar.cta if set, otherwise fall back to widget-level cta
36847
- cta: (_10 = config.actionBar.cta) !== null && _10 !== void 0 ? _10 : config.cta }), visible: visible, chatDisabled: config.disabled, hasUserInteracted: (_11 = chatState.visuals) === null || _11 === void 0 ? void 0 : _11.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: config === null || config === void 0 ? void 0 : config.cta, imageUrl: (_12 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _12 === void 0 ? void 0 : _12.imageUrl, visible: visible, hasInteracted: (_13 = chatState.visuals) === null || _13 === void 0 ? void 0 : _13.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37209
+ cta: (_10 = config.actionBar.cta) !== null && _10 !== void 0 ? _10 : config.cta }), visible: visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_11 = chatState.visuals) === null || _11 === void 0 ? void 0 : _11.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: config === null || config === void 0 ? void 0 : config.cta, imageUrl: (_12 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _12 === void 0 ? void 0 : _12.imageUrl, visible: visible, hasInteracted: (_13 = chatState.visuals) === null || _13 === void 0 ? void 0 : _13.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
36848
37210
  };
36849
37211
  /**
36850
37212
  * Top-level wrapper that dispatches between preview mode and connected mode.