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