@xapp/chat-widget 1.96.4 → 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
@@ -932,9 +932,39 @@ var MOBILE_MEDIA_QUERY = "(max-width: 640px), (max-height: 480px) and (pointer:
932
932
  * Two concurrent bars would fight over it, and either one unmounting would clear it.
933
933
  */
934
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
+ }
935
965
  var ActionBar = function (_a) {
936
966
  var _b, _c;
937
- 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;
938
968
  var _e = useState(false), isMobile = _e[0], setIsMobile = _e[1];
939
969
  // An explicit override can only be a width; without one we ask the same question the
940
970
  // stylesheets ask, so the two cannot disagree. A survey of production on 2026-08-19
@@ -1005,10 +1035,9 @@ var ActionBar = function (_a) {
1005
1035
  document.documentElement.style.removeProperty(ACTION_BAR_HEIGHT_VAR);
1006
1036
  };
1007
1037
  }, [isMobile]);
1008
- // Filter out chat buttons if chat is disabled
1009
- var filteredButtons = chatDisabled
1010
- ? config.buttons.filter(function (button) { return !lib.isActionBarChatButton(button); })
1011
- : 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 });
1012
1041
  // Build class names - ActionBar stays visible even when chat is open
1013
1042
  var classNames = [
1014
1043
  "xapp-action-bar",
@@ -1021,7 +1050,7 @@ var ActionBar = function (_a) {
1021
1050
  // - Must have CTA config with a message
1022
1051
  // - Chat must not be visible (user hasn't opened it)
1023
1052
  // - Chat must not be disabled
1024
- var hasChatButton = config.buttons.some(lib.isActionBarChatButton);
1053
+ var hasChatButton = filteredButtons.some(lib.isActionBarChatButton);
1025
1054
  var showCta = ((_c = config.cta) === null || _c === void 0 ? void 0 : _c.message) && !visible && !chatDisabled && hasChatButton;
1026
1055
  var handleCtaClick = function () {
1027
1056
  onChatClick();
@@ -1029,6 +1058,13 @@ var ActionBar = function (_a) {
1029
1058
  var handleCtaDismiss = function () {
1030
1059
  onCtaDismiss === null || onCtaDismiss === void 0 ? void 0 : onCtaDismiss();
1031
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
+ }
1032
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)); }) })] }));
1033
1069
  };
1034
1070
 
@@ -12797,6 +12833,64 @@ function getSrcId(url) {
12797
12833
  return res;
12798
12834
  }
12799
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
+
12800
12894
  function useChatServerVisitorId() {
12801
12895
  var dispatch = useChatServerDispatch();
12802
12896
  var clientVisitorId = useSelector(function (state) {
@@ -37041,6 +37135,21 @@ var ChatWidgetUI = function (props) {
37041
37135
  log("Rendering - accountStatus: \"".concat(chatState.accountStatus, "\", connectionStatus: \"").concat(chatState.connection.connectionStatus, "\""));
37042
37136
  var config = props.config, onConnectionStatusChange = props.onConnectionStatusChange;
37043
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;
37044
37153
  // Disable greeting in preview mode
37045
37154
  useGreeting(!isOffline && !props.preChatFormEnabled && visible && !previewMode);
37046
37155
  var connectionStatus = chatState.connection.connectionStatus;
@@ -37050,23 +37159,31 @@ var ChatWidgetUI = function (props) {
37050
37159
  }
37051
37160
  }, [connectionStatus, onConnectionStatusChange]);
37052
37161
  useExternalScript((_t = props.config) === null || _t === void 0 ? void 0 : _t.middlewareUrl);
37053
- // 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));
37054
37171
  useEffect(function () {
37055
- if (config === null || config === void 0 ? void 0 : config.formWidgetKey) {
37172
+ if ((config === null || config === void 0 ? void 0 : config.formWidgetKey) && formWidgetNeeded) {
37056
37173
  preloadFormWidgetScript(config.formWidgetKey);
37057
37174
  }
37058
- }, [config === null || config === void 0 ? void 0 : config.formWidgetKey]);
37175
+ }, [config === null || config === void 0 ? void 0 : config.formWidgetKey, formWidgetNeeded]);
37059
37176
  // Set body attribute when actionBar is enabled (used by CSS to hide form-widget side button on mobile)
37060
37177
  // Uses reference counting to handle multiple widget instances
37061
37178
  useEffect(function () {
37062
- if (actionBarEnabled) {
37179
+ if (actionBarVisible) {
37063
37180
  // Increment reference count
37064
37181
  var currentCount = parseInt(document.body.getAttribute("data-xapp-action-bar-count") || "0", 10);
37065
37182
  document.body.setAttribute("data-xapp-action-bar-count", String(currentCount + 1));
37066
37183
  document.body.setAttribute("data-xapp-action-bar-enabled", "true");
37067
37184
  }
37068
37185
  return function () {
37069
- if (actionBarEnabled) {
37186
+ if (actionBarVisible) {
37070
37187
  // Decrement reference count
37071
37188
  var currentCount = parseInt(document.body.getAttribute("data-xapp-action-bar-count") || "0", 10);
37072
37189
  var newCount = Math.max(0, currentCount - 1);
@@ -37079,7 +37196,7 @@ var ChatWidgetUI = function (props) {
37079
37196
  }
37080
37197
  }
37081
37198
  };
37082
- }, [actionBarEnabled]);
37199
+ }, [actionBarVisible]);
37083
37200
  // This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
37084
37201
  var widgetAgent = ((_u = chatState.agents["agent:robot"]) === null || _u === void 0 ? void 0 : _u.user) ||
37085
37202
  (config === null || config === void 0 ? void 0 : config.agent) || {
@@ -37087,9 +37204,9 @@ var ChatWidgetUI = function (props) {
37087
37204
  avatarPath: config.avatarUrl,
37088
37205
  display_name: "Agent",
37089
37206
  };
37090
- 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), {
37091
37208
  // Use actionBar.cta if set, otherwise fall back to widget-level cta
37092
- 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 })] }));
37093
37210
  };
37094
37211
  /**
37095
37212
  * Top-level wrapper that dispatches between preview mode and connected mode.