@cognigy/webchat 3.51.0 → 3.52.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.
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * webchat.esm.js v3.51.0
3
- * https://github.com/Cognigy/Webchat/tree/v3.51.0
4
- * https://github.com/Cognigy/Webchat/tree/v3.51.0/OSS_LICENSES.txt
2
+ * webchat.esm.js v3.52.0
3
+ * https://github.com/Cognigy/Webchat/tree/v3.52.0
4
+ * https://github.com/Cognigy/Webchat/tree/v3.52.0/OSS_LICENSES.txt
5
5
  */
6
6
  import * as __WEBPACK_EXTERNAL_MODULE_react__ from "react";
7
7
  import * as __WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee__ from "react-dom";
@@ -35778,21 +35778,22 @@ const Modal = ({
35778
35778
  /* harmony import */ var react_transition_group__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(66492);
35779
35779
  /* harmony import */ var _presentational_TeaserMessage__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(49888);
35780
35780
  /* harmony import */ var _presentational_TeaserMessageAnnouncer__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(59359);
35781
- /* harmony import */ var _functional_xapp_overlay_XAppOverlay__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(12381);
35782
- /* harmony import */ var _utils_sourceMapping__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(25091);
35783
- /* harmony import */ var _plugins_speech_output__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(83679);
35784
- /* harmony import */ var _utils_filter_out_control_commands__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(70562);
35785
- /* harmony import */ var _utils_isMobile__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(40794);
35786
- /* harmony import */ var _webchat_helper_handleMarkdown__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(71621);
35787
- /* harmony import */ var _presentational_previous_conversations_DeleteAllConversations__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(5196);
35788
- /* harmony import */ var _presentational_ScreenReaderLiveRegion__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(41426);
35789
- /* harmony import */ var _presentational_StatusLiveRegion__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(63813);
35790
- /* harmony import */ var _presentational_FreezeOnExit__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(97475);
35791
- /* harmony import */ var _presentational_HomeScreenAnnouncer__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(55115);
35792
- /* harmony import */ var _utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(39191);
35793
- /* harmony import */ var _utils_screen_visibility__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(48245);
35794
- /* harmony import */ var classnames__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(14176);
35795
- /* harmony import */ var classnames__WEBPACK_IMPORTED_MODULE_57___default = /*#__PURE__*/__webpack_require__.n(classnames__WEBPACK_IMPORTED_MODULE_57__);
35781
+ /* harmony import */ var _presentational_UnreadMessagesAnnouncer__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(1715);
35782
+ /* harmony import */ var _functional_xapp_overlay_XAppOverlay__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(12381);
35783
+ /* harmony import */ var _utils_sourceMapping__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(25091);
35784
+ /* harmony import */ var _plugins_speech_output__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(83679);
35785
+ /* harmony import */ var _utils_filter_out_control_commands__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(70562);
35786
+ /* harmony import */ var _utils_isMobile__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(40794);
35787
+ /* harmony import */ var _webchat_helper_handleMarkdown__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(71621);
35788
+ /* harmony import */ var _presentational_previous_conversations_DeleteAllConversations__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(5196);
35789
+ /* harmony import */ var _presentational_ScreenReaderLiveRegion__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(41426);
35790
+ /* harmony import */ var _presentational_StatusLiveRegion__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(63813);
35791
+ /* harmony import */ var _presentational_FreezeOnExit__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(97475);
35792
+ /* harmony import */ var _presentational_HomeScreenAnnouncer__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(55115);
35793
+ /* harmony import */ var _utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(39191);
35794
+ /* harmony import */ var _utils_screen_visibility__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(48245);
35795
+ /* harmony import */ var classnames__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(14176);
35796
+ /* harmony import */ var classnames__WEBPACK_IMPORTED_MODULE_58___default = /*#__PURE__*/__webpack_require__.n(classnames__WEBPACK_IMPORTED_MODULE_58__);
35796
35797
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
35797
35798
 
35798
35799
 
@@ -35850,6 +35851,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
35850
35851
 
35851
35852
 
35852
35853
 
35854
+
35853
35855
 
35854
35856
 
35855
35857
  const stylisPlugins = [(0,_utils_css__WEBPACK_IMPORTED_MODULE_8__/* .isolate */ .az)("[data-cognigy-webchat-root]")];
@@ -35954,7 +35956,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
35954
35956
  deleteConversationsModalState: false,
35955
35957
  liveContent: {},
35956
35958
  isMobile: false,
35957
- noticeSession: _utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_55__/* .INITIAL_NOTICE_SESSION */ .j_
35959
+ noticeSession: _utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_56__/* .INITIAL_NOTICE_SESSION */ .j_
35958
35960
  };
35959
35961
  unreadTitleIndicatorInterval = null;
35960
35962
  originalTitle = window.document.title;
@@ -35965,6 +35967,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
35965
35967
  engagementMessageTimeout = null;
35966
35968
  ratingFocusTimeout = null;
35967
35969
  homeScreenExitFocusTimeout = null;
35970
+ xAppOverlayCloseFocusTimeout = null;
35968
35971
 
35969
35972
  // Disarms the pending home-screen exit focus fallback. Also registered as
35970
35973
  // a capture-phase document pointerdown listener while the fallback is
@@ -36002,9 +36005,9 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36002
36005
  const secondaryColor = props?.config?.settings?.colors?.secondaryColor;
36003
36006
  const chatInterfaceColor = props?.config?.settings?.colors?.chatInterfaceColor;
36004
36007
  const sourceColorMapping = props?.config?.settings?.widgetSettings?.sourceColorMapping;
36005
- const overrideBotMessageColor = (0,_utils_sourceMapping__WEBPACK_IMPORTED_MODULE_45__/* .getSourceBackgroundColor */ .N)(sourceColorMapping?.bot, props?.config?.settings?.colors);
36006
- const overrideUserMessageColor = (0,_utils_sourceMapping__WEBPACK_IMPORTED_MODULE_45__/* .getSourceBackgroundColor */ .N)(sourceColorMapping?.user, props?.config?.settings?.colors);
36007
- const overrideAgentMessageColor = (0,_utils_sourceMapping__WEBPACK_IMPORTED_MODULE_45__/* .getSourceBackgroundColor */ .N)(sourceColorMapping?.agent, props?.config?.settings?.colors);
36008
+ const overrideBotMessageColor = (0,_utils_sourceMapping__WEBPACK_IMPORTED_MODULE_46__/* .getSourceBackgroundColor */ .N)(sourceColorMapping?.bot, props?.config?.settings?.colors);
36009
+ const overrideUserMessageColor = (0,_utils_sourceMapping__WEBPACK_IMPORTED_MODULE_46__/* .getSourceBackgroundColor */ .N)(sourceColorMapping?.user, props?.config?.settings?.colors);
36010
+ const overrideAgentMessageColor = (0,_utils_sourceMapping__WEBPACK_IMPORTED_MODULE_46__/* .getSourceBackgroundColor */ .N)(sourceColorMapping?.agent, props?.config?.settings?.colors);
36008
36011
  const overrideBotMessageBorderColor = sourceColorMapping?.bot === "user" ? "transparent" : null;
36009
36012
  const overrideUserMessageBorderColor = sourceColorMapping?.user === "bot" ? state.theme.black80 : null;
36010
36013
  const overrideAgentMessageBorderColor = sourceColorMapping?.agent === "user" ? "transparent" : null;
@@ -36108,14 +36111,14 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36108
36111
  evaluateNoticeSession(prevConversationsSnapshot) {
36109
36112
  const id = this.props.currentSession || "";
36110
36113
  this.setState(prev => ({
36111
- noticeSession: (0,_utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_55__/* .computeNoticeSession */ .Kr)(prev.noticeSession, id, prevConversationsSnapshot, this.props.hasRestoredPersistedHistory)
36114
+ noticeSession: (0,_utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_56__/* .computeNoticeSession */ .Kr)(prev.noticeSession, id, prevConversationsSnapshot, this.props.hasRestoredPersistedHistory, this.props.willRestorePersistedConversation)
36112
36115
  }));
36113
36116
  }
36114
36117
  handleNoticeIntroAnnounced = introKey => {
36115
36118
  this.announcedNoticeKeys.add(introKey);
36116
36119
  };
36117
36120
  getNoticeIntro() {
36118
- const text = (0,_utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_55__/* .getAIAgentNoticeIntroText */ .bu)({
36121
+ const text = (0,_utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_56__/* .getAIAgentNoticeIntroText */ .bu)({
36119
36122
  behavior: this.props.config.settings.behavior,
36120
36123
  showDisconnectOverlay: this.showDisconnectOverlay,
36121
36124
  noticeSession: this.state.noticeSession,
@@ -36131,12 +36134,12 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36131
36134
  componentDidMount() {
36132
36135
  const defaultMessagePlugins = [];
36133
36136
  if (this.props.ttsActive) {
36134
- defaultMessagePlugins.push(_plugins_speech_output__WEBPACK_IMPORTED_MODULE_46__/* ["default"] */ .A);
36137
+ defaultMessagePlugins.push(_plugins_speech_output__WEBPACK_IMPORTED_MODULE_47__/* ["default"] */ .A);
36135
36138
  }
36136
36139
  this.setState({
36137
36140
  inputPlugins: [...(this.props.inputPlugins || []), _plugins_input_base__WEBPACK_IMPORTED_MODULE_11__/* ["default"] */ .A],
36138
36141
  messagePlugins: [...(this.props.messagePlugins || []), ...defaultMessagePlugins],
36139
- isMobile: (0,_utils_isMobile__WEBPACK_IMPORTED_MODULE_48__/* .isMobileViewport */ .$)()
36142
+ isMobile: (0,_utils_isMobile__WEBPACK_IMPORTED_MODULE_49__/* .isMobileViewport */ .$)()
36140
36143
  });
36141
36144
  this.setupIconAnimationInterval();
36142
36145
 
@@ -36208,13 +36211,53 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36208
36211
  }
36209
36212
  }, 450);
36210
36213
  }
36214
+
36215
+ // Closing the xApp overlay unmounts the dialog together with whatever
36216
+ // was focused in it (its close button, or the frame), and the screen it
36217
+ // covered re-mounts — so focus cannot be "restored" to a surviving
36218
+ // element and would drop to document.body (SC 2.4.3). The chat screen
36219
+ // re-focuses itself via the message input's 200ms autofocus; when that
36220
+ // is disabled (`disableInputAutofocus`), or for any other view, this
36221
+ // late check moves focus to the re-mounted header title, mirroring the
36222
+ // home-screen exit fallback above. It never overrides focus that
36223
+ // landed anywhere else.
36224
+ if (prevProps.isXAppOverlayOpen && !this.props.isXAppOverlayOpen) {
36225
+ if (this.xAppOverlayCloseFocusTimeout) clearTimeout(this.xAppOverlayCloseFocusTimeout);
36226
+ this.xAppOverlayCloseFocusTimeout = setTimeout(() => {
36227
+ this.xAppOverlayCloseFocusTimeout = null;
36228
+ const webchatWindowEl = this.webchatWindowRef?.current;
36229
+ if (!webchatWindowEl) return;
36230
+ const active = document.activeElement;
36231
+ if (active === document.body || active === null) {
36232
+ (webchatWindowEl.querySelector(".webchat-header-bar .webchat-header-title") ?? (0,_utils_find_focusable__WEBPACK_IMPORTED_MODULE_23__/* ["default"] */ .A)(webchatWindowEl).firstFocusable)?.focus();
36233
+ }
36234
+ }, 450);
36235
+ }
36211
36236
  if (prevProps.currentSession !== this.props.currentSession) {
36212
36237
  this.evaluateNoticeSession(prevProps.prevConversations);
36213
36238
  }
36239
+
36240
+ // The persisted-conversation prediction is read from storage under a
36241
+ // key that contains the endpoint config's URLToken, so it only
36242
+ // becomes meaningful once that config has loaded — after this
36243
+ // component has already mounted. Re-evaluating on the flip is in
36244
+ // time for every case the notice can observe, and normally with
36245
+ // room to spare: `open()` waits for the config before showing
36246
+ // anything, and even if it gives up waiting (~1s by default, then
36247
+ // it opens regardless) the flip still lands within the intro's own
36248
+ // 600ms delay and its effect cleanup cancels the pending timer. A
36249
+ // config slower than roughly the sum of the two would let the
36250
+ // notice announce before the prediction arrives — the remaining
36251
+ // gap, and the reason the notice is not gated on the connect on
36252
+ // top of this. No session id exists yet at this point, so current
36253
+ // props are the pre-change snapshot.
36254
+ if (prevProps.willRestorePersistedConversation !== this.props.willRestorePersistedConversation && !this.props.currentSession) {
36255
+ this.evaluateNoticeSession(this.props.prevConversations);
36256
+ }
36214
36257
  if (prevProps.ttsActive !== this.props.ttsActive || prevProps.inputPlugins !== this.props.inputPlugins || prevProps.messagePlugins !== this.props.messagePlugins) {
36215
36258
  const defaultMessagePlugins = [];
36216
36259
  if (this.props.ttsActive) {
36217
- defaultMessagePlugins.push(_plugins_speech_output__WEBPACK_IMPORTED_MODULE_46__/* ["default"] */ .A);
36260
+ defaultMessagePlugins.push(_plugins_speech_output__WEBPACK_IMPORTED_MODULE_47__/* ["default"] */ .A);
36218
36261
  }
36219
36262
  this.setState({
36220
36263
  inputPlugins: [...(this.props.inputPlugins || []), _plugins_input_base__WEBPACK_IMPORTED_MODULE_11__/* ["default"] */ .A],
@@ -36264,7 +36307,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36264
36307
  // TODO: Add markdown rendering if there is valid markdown and renderMarkdown is enabled
36265
36308
  // adding react-markdown is blocked by updating React to v18
36266
36309
  if (!this.props.config.settings.widgetSettings.disableTeaserMarkdownRemoval) {
36267
- lastUnseenMessageText = (0,_webchat_helper_handleMarkdown__WEBPACK_IMPORTED_MODULE_49__/* .removeMarkdownChars */ .$)(lastUnseenMessageText);
36310
+ lastUnseenMessageText = (0,_webchat_helper_handleMarkdown__WEBPACK_IMPORTED_MODULE_50__/* .removeMarkdownChars */ .$)(lastUnseenMessageText);
36268
36311
  }
36269
36312
  this.setState({
36270
36313
  lastUnseenMessageText
@@ -36324,6 +36367,10 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36324
36367
 
36325
36368
  // also removes the fallback's document pointerdown listener
36326
36369
  this.cancelHomeScreenExitFocusFallback();
36370
+ if (this.xAppOverlayCloseFocusTimeout) {
36371
+ clearTimeout(this.xAppOverlayCloseFocusTimeout);
36372
+ this.xAppOverlayCloseFocusTimeout = null;
36373
+ }
36327
36374
 
36328
36375
  // Teardown icon animation interval
36329
36376
  if (this.iconAnimationIntervalHandle) {
@@ -36333,7 +36380,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36333
36380
  window.removeEventListener("resize", this.handleResize);
36334
36381
  }
36335
36382
  handleResize() {
36336
- const isMobile = (0,_utils_isMobile__WEBPACK_IMPORTED_MODULE_48__/* .isMobileViewport */ .$)();
36383
+ const isMobile = (0,_utils_isMobile__WEBPACK_IMPORTED_MODULE_49__/* .isMobileViewport */ .$)();
36337
36384
  if (isMobile !== this.state.isMobile) {
36338
36385
  this.setState({
36339
36386
  isMobile
@@ -36467,7 +36514,10 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36467
36514
  // behind it is inert, so this trap would agree with it only by virtue
36468
36515
  // of getKeyboardFocusableElements skipping inert subtrees. Excluded
36469
36516
  // explicitly instead of relying on that coupling.
36470
- !this.showDisconnectOverlay) {
36517
+ !this.showDisconnectOverlay &&
36518
+ // The xApp overlay dialog traps focus with its own focus guards
36519
+ // (Tab presses inside its cross-origin frame are invisible here).
36520
+ !this.props.isXAppOverlayOpen) {
36471
36521
  // Get the first and last focusable elements within the webchat window and add focus
36472
36522
  const webchatWindowEl = this.webchatWindowRef?.current;
36473
36523
  const {
@@ -36743,7 +36793,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36743
36793
  // while the webchat is open, so no `open` check is needed here.
36744
36794
  const {
36745
36795
  showHomeScreenView
36746
- } = (0,_utils_screen_visibility__WEBPACK_IMPORTED_MODULE_56__/* .getScreenVisibility */ .p)(this.props, isInforming);
36796
+ } = (0,_utils_screen_visibility__WEBPACK_IMPORTED_MODULE_57__/* .getScreenVisibility */ .p)(this.props, isInforming);
36747
36797
  const openChatAriaLabel = () => {
36748
36798
  if (open) return config.settings.customTranslations?.ariaLabels?.closeChat ?? "Close chat";
36749
36799
  const openChat = config.settings.customTranslations?.ariaLabels?.openChat ?? "Open chat";
@@ -36818,7 +36868,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36818
36868
  el.removeAttribute("inert");
36819
36869
  }
36820
36870
  }
36821
- }, !fullscreenMessage ? this.renderRegularLayout(isInforming) : this.renderFullscreenMessageLayout()), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_StatusLiveRegion__WEBPACK_IMPORTED_MODULE_52__/* .StatusLiveRegion */ .t, null), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_HomeScreenAnnouncer__WEBPACK_IMPORTED_MODULE_54__/* ["default"] */ .A, {
36871
+ }, !fullscreenMessage ? this.renderRegularLayout(isInforming) : this.renderFullscreenMessageLayout()), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_StatusLiveRegion__WEBPACK_IMPORTED_MODULE_53__/* .StatusLiveRegion */ .t, null), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_HomeScreenAnnouncer__WEBPACK_IMPORTED_MODULE_55__/* ["default"] */ .A, {
36822
36872
  active: showHomeScreenView,
36823
36873
  label: config.settings.customTranslations?.ariaLabels?.homeScreen ?? "Chat window home screen"
36824
36874
  }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_DisconnectOverlay__WEBPACK_IMPORTED_MODULE_20__/* ["default"] */ .A, {
@@ -36828,7 +36878,12 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36828
36878
  isConnecting: !!this.props.connecting,
36829
36879
  onClose: handleClose,
36830
36880
  config: config
36831
- })), !disableToggleButton && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_TeaserMessageAnnouncer__WEBPACK_IMPORTED_MODULE_43__/* ["default"] */ .A, {
36881
+ })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_UnreadMessagesAnnouncer__WEBPACK_IMPORTED_MODULE_44__/* ["default"] */ .A, {
36882
+ active: config.settings.unreadMessages.enableBadge || config.settings.unreadMessages.enableIndicator,
36883
+ count: unseenMessages.length,
36884
+ singularText: config.settings.customTranslations?.ariaLabels?.unreadMessageSingularText ?? "One unread message in chat.",
36885
+ pluralText: config.settings.customTranslations?.ariaLabels?.unreadMessagePluralText ?? "unread messages in chat."
36886
+ }), !disableToggleButton && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_TeaserMessageAnnouncer__WEBPACK_IMPORTED_MODULE_43__/* ["default"] */ .A, {
36832
36887
  active: !!lastUnseenMessageText && !open,
36833
36888
  text: lastUnseenMessageText,
36834
36889
  label: config.settings.customTranslations?.ariaLabels?.newMessagePreview ?? "New message preview"
@@ -36862,7 +36917,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36862
36917
  onClick: this.handleFabClick
36863
36918
  }, webchatToggleProps, {
36864
36919
  type: "button",
36865
- className: classnames__WEBPACK_IMPORTED_MODULE_57___default()("webchat-toggle-button burst", config.settings?.layout?.iconAnimation),
36920
+ className: classnames__WEBPACK_IMPORTED_MODULE_58___default()("webchat-toggle-button burst", config.settings?.layout?.iconAnimation),
36866
36921
  id: "webchatWindowToggleButton",
36867
36922
  "aria-label": openChatAriaLabel(),
36868
36923
  ref: this.chatToggleButtonRef,
@@ -36871,10 +36926,17 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
36871
36926
  }
36872
36927
  }), open ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_assets_collapse_20px_svg__WEBPACK_IMPORTED_MODULE_19__/* ["default"] */ .A, null) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_ChatIcon__WEBPACK_IMPORTED_MODULE_18__/* ["default"] */ .A, {
36873
36928
  config: config
36874
- }), config.settings.unreadMessages.enableBadge ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_Badge__WEBPACK_IMPORTED_MODULE_21__/* ["default"] */ .A, {
36929
+ }), config.settings.unreadMessages.enableBadge ?
36930
+ /*#__PURE__*/
36931
+ // The count is already in the button's
36932
+ // accessible name (and announced live by
36933
+ // UnreadMessagesAnnouncer); the badge is a
36934
+ // visual duplicate. aria-label is prohibited
36935
+ // on a generic <span>.
36936
+ react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_Badge__WEBPACK_IMPORTED_MODULE_21__/* ["default"] */ .A, {
36875
36937
  _content: unseenMessages.length,
36876
36938
  className: "webchat-unread-message-badge",
36877
- "aria-label": `${unseenMessages.length} ${config.settings.customTranslations?.ariaLabels?.unreadMessages ?? "unread messages"}`
36939
+ "aria-hidden": "true"
36878
36940
  }) : null))))))));
36879
36941
  }
36880
36942
  renderRegularLayout(isInforming) {
@@ -37000,7 +37062,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
37000
37062
  onAcceptTerms: handleAcceptTerms,
37001
37063
  isHomeScreenEnabled: isHomeScreenEnabled
37002
37064
  });
37003
- if (isXAppOverlayOpen) return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_functional_xapp_overlay_XAppOverlay__WEBPACK_IMPORTED_MODULE_44__/* ["default"] */ .A, null);
37065
+ if (isXAppOverlayOpen) return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_functional_xapp_overlay_XAppOverlay__WEBPACK_IMPORTED_MODULE_45__/* ["default"] */ .A, null);
37004
37066
  if (showPrevConversations) return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_previous_conversations_ConversationsList__WEBPACK_IMPORTED_MODULE_33__/* .PrevConversationsList */ .K, {
37005
37067
  conversations: this.props.prevConversations,
37006
37068
  onSetShowPrevConversations: onSetShowPrevConversations,
@@ -37039,7 +37101,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
37039
37101
  }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement("h3", {
37040
37102
  className: "sr-only",
37041
37103
  id: "webchatChatHistoryHeading"
37042
- }, config.settings.customTranslations?.ariaLabels?.chatHistory ?? "Chat history"), this.renderHistory()), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_ScreenReaderLiveRegion__WEBPACK_IMPORTED_MODULE_51__/* ["default"] */ .A, {
37104
+ }, config.settings.customTranslations?.ariaLabels?.chatHistory ?? "Chat history"), this.renderHistory()), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_ScreenReaderLiveRegion__WEBPACK_IMPORTED_MODULE_52__/* ["default"] */ .A, {
37043
37105
  liveContent: this.state.liveContent,
37044
37106
  intro: this.getNoticeIntro(),
37045
37107
  onIntroAnnounced: this.handleNoticeIntroAnnounced
@@ -37070,7 +37132,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
37070
37132
  const {
37071
37133
  showEnabledHomeScreen,
37072
37134
  showChatScreen
37073
- } = (0,_utils_screen_visibility__WEBPACK_IMPORTED_MODULE_56__/* .getScreenVisibility */ .p)(this.props, isInforming);
37135
+ } = (0,_utils_screen_visibility__WEBPACK_IMPORTED_MODULE_57__/* .getScreenVisibility */ .p)(this.props, isInforming);
37074
37136
  const isChatOptionsButtonVisible = config.settings.chatOptions.enabled && showChatScreen;
37075
37137
  const hideBackButton = showChatScreen && !isHomeScreenEnabled;
37076
37138
  const showDeleteAllConversationButton = !!((config.settings.privacyNotice.enabled && this.props.hasAcceptedTerms || !config.settings.privacyNotice.enabled) && config.settings.homeScreen.previousConversations.enableDeleteAllConversations && showPrevConversations && Object.keys(this.props.prevConversations).length);
@@ -37142,9 +37204,9 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
37142
37204
  className: "webchat-regular-layout-content",
37143
37205
  "aria-hidden": isRegularLayoutActiveView ? undefined : true,
37144
37206
  inert: isRegularLayoutActiveView ? undefined : ""
37145
- }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_FreezeOnExit__WEBPACK_IMPORTED_MODULE_53__/* ["default"] */ .A, {
37207
+ }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_FreezeOnExit__WEBPACK_IMPORTED_MODULE_54__/* ["default"] */ .A, {
37146
37208
  active: isRegularLayoutActiveView
37147
- }, getRegularLayoutContent()), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_previous_conversations_DeleteAllConversations__WEBPACK_IMPORTED_MODULE_50__/* ["default"] */ .A, {
37209
+ }, getRegularLayoutContent()), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_presentational_previous_conversations_DeleteAllConversations__WEBPACK_IMPORTED_MODULE_51__/* ["default"] */ .A, {
37148
37210
  config: config,
37149
37211
  isOpen: this.state.showDeleteAllConversationsModal,
37150
37212
  onOpenChange: (open, confirmDelete) => {
@@ -37207,7 +37269,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
37207
37269
 
37208
37270
  // Find privacy message and remove it from the messages list (these message types are not displayed in the chat log).
37209
37271
  // If we do not remove, it will cause the collatation of the first user message.
37210
- const messagesExcludingPrivacyMessage = (0,_utils_filter_out_control_commands__WEBPACK_IMPORTED_MODULE_47__/* ["default"] */ .A)(messages, ["acceptPrivacyPolicy"]);
37272
+ const messagesExcludingPrivacyMessage = (0,_utils_filter_out_control_commands__WEBPACK_IMPORTED_MODULE_48__/* ["default"] */ .A)(messages, ["acceptPrivacyPolicy"]);
37211
37273
 
37212
37274
  // Filter messages based on progressive rendering settings
37213
37275
  const visibleMessages = progressiveMessageRendering ? messagesExcludingPrivacyMessage.filter(message => {
@@ -37228,7 +37290,7 @@ class WebchatUI extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
37228
37290
  variant: "body-regular",
37229
37291
  component: "div",
37230
37292
  className: "webchat-log-ai-agent-notice-text"
37231
- }, AIAgentNoticeText || _utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_55__/* .DEFAULT_AI_AGENT_NOTICE_TEXT */ .S5), visibleMessages.map((message, index) => {
37293
+ }, AIAgentNoticeText || _utils_ai_agent_notice__WEBPACK_IMPORTED_MODULE_56__/* .DEFAULT_AI_AGENT_NOTICE_TEXT */ .S5), visibleMessages.map((message, index) => {
37232
37294
  // Lookahead if there is a user reply that includes text or attachments
37233
37295
  const hasReply = visibleMessages.slice(index + 1).some(message => {
37234
37296
  const isUser = message.source === "user";
@@ -37421,6 +37483,7 @@ const MobileScrollLock = ({
37421
37483
  /* harmony import */ var _webchat_helper_useSelector__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3862);
37422
37484
  /* harmony import */ var _presentational_Header__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(55922);
37423
37485
  /* harmony import */ var _webchat_store_messages_message_middleware__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(20570);
37486
+ /* harmony import */ var _utils_find_focusable__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(41098);
37424
37487
 
37425
37488
 
37426
37489
 
@@ -37430,12 +37493,18 @@ const MobileScrollLock = ({
37430
37493
 
37431
37494
 
37432
37495
 
37496
+
37497
+ const TITLE_ID = "webchatXAppOverlayTitle";
37498
+ const FOCUS_GUARD_ATTRIBUTE = "data-xapp-overlay-focus-guard";
37433
37499
  const Root = _emotion_styled__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A.div(() => ({
37434
37500
  display: "flex",
37435
37501
  flexDirection: "column",
37436
37502
  height: "100%",
37437
37503
  width: "100%",
37438
37504
  animation: "fade-in-xapp .25s ease-out",
37505
+ "@media (prefers-reduced-motion: reduce)": {
37506
+ animation: "none"
37507
+ },
37439
37508
  "@keyframes fade-in-xapp": {
37440
37509
  from: {
37441
37510
  transform: "scale(.875)",
@@ -37453,6 +37522,9 @@ const Iframe = _emotion_styled__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A.
37453
37522
  height: "100%",
37454
37523
  width: "100%",
37455
37524
  animation: "fade-in-xapp-2 .325s ease-in",
37525
+ "@media (prefers-reduced-motion: reduce)": {
37526
+ animation: "none"
37527
+ },
37456
37528
  "@keyframes fade-in-xapp-2": {
37457
37529
  from: {
37458
37530
  opacity: 0.87,
@@ -37466,6 +37538,21 @@ const Iframe = _emotion_styled__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A.
37466
37538
  }
37467
37539
  }
37468
37540
  }));
37541
+
37542
+ // Visually hidden but reachable by sequential focus navigation. Not
37543
+ // `aria-hidden`: a focusable aria-hidden node is an axe violation
37544
+ // (aria-hidden-focus) — and focus never rests here anyway, the onFocus
37545
+ // handler redirects it immediately.
37546
+ const FocusGuard = _emotion_styled__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A.div(() => ({
37547
+ position: "absolute",
37548
+ width: 1,
37549
+ height: 1,
37550
+ margin: -1,
37551
+ padding: 0,
37552
+ overflow: "hidden",
37553
+ clip: "rect(0, 0, 0, 0)",
37554
+ outline: "none"
37555
+ }));
37469
37556
  const xAppOverlay = () => {
37470
37557
  const url = (0,_webchat_helper_useSelector__WEBPACK_IMPORTED_MODULE_6__/* .useSelector */ .d)(state => state.xAppOverlay.currentUrl) || "";
37471
37558
  const {
@@ -37475,7 +37562,17 @@ const xAppOverlay = () => {
37475
37562
  sendEventOnCloseIconClick,
37476
37563
  showCloseIcon
37477
37564
  } = (0,_webchat_store_xapp_overlay_hooks__WEBPACK_IMPORTED_MODULE_5__/* .useOverlaySettingsByUrl */ .Z)(url);
37565
+ const ariaLabels = (0,_webchat_helper_useSelector__WEBPACK_IMPORTED_MODULE_6__/* .useSelector */ .d)(state => state.config.settings.customTranslations?.ariaLabels);
37566
+ // A whitespace-only screen title has no accessible text: it would render
37567
+ // an empty heading and name the dialog after it. Treat it as absent.
37568
+ const title = screenTitle?.trim() ?? "";
37569
+ // Names the dialog and its frame when the xApp has no screen title.
37570
+ const fallbackName = ariaLabels?.xAppOverlay ?? "Embedded app";
37571
+ const closeButtonAriaLabel = ariaLabels?.closeDialog ?? "Close dialog";
37478
37572
  const dispatch = (0,react_redux__WEBPACK_IMPORTED_MODULE_0__/* .useDispatch */ .wA)();
37573
+ const rootRef = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
37574
+ const closeButtonRef = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
37575
+ const iframeRef = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
37479
37576
  const handleClose = () => {
37480
37577
  if (closeOnSubmit) {
37481
37578
  dispatch((0,_webchat_store_xapp_overlay_slice__WEBPACK_IMPORTED_MODULE_4__/* .closeOverlay */ .L9)());
@@ -37492,7 +37589,20 @@ const xAppOverlay = () => {
37492
37589
  dispatch((0,_webchat_store_xapp_overlay_slice__WEBPACK_IMPORTED_MODULE_4__/* .closeOverlay */ .L9)());
37493
37590
  };
37494
37591
  const handleSubmit = event => {
37495
- if (url.startsWith(event.origin) === false) {
37592
+ // WCH-SI10-004: compare canonical origins, not raw URL strings.
37593
+ // The previous url.startsWith(event.origin) check was semantically backwards —
37594
+ // a domain that is a string-prefix of url (e.g. "https://xapp.cognigy.a" for
37595
+ // "https://xapp.cognigy.ai/form") could pass the check despite being a different
37596
+ // origin. new URL(url).origin extracts the canonical scheme+host+port for an
37597
+ // exact match, which is the correct cross-origin security boundary.
37598
+ let urlOrigin;
37599
+ try {
37600
+ urlOrigin = new URL(url).origin;
37601
+ } catch {
37602
+ // url is empty or not a valid absolute URL — reject all postMessages.
37603
+ return;
37604
+ }
37605
+ if (urlOrigin !== event.origin) {
37496
37606
  return;
37497
37607
  }
37498
37608
  if (event.data.type !== "x-app-submit") {
@@ -37518,8 +37628,12 @@ const xAppOverlay = () => {
37518
37628
  handleClose();
37519
37629
  };
37520
37630
 
37521
- // We revieve a MessageEvent from the xApp iframe when submission happens
37631
+ // We receive a MessageEvent from the xApp iframe when submission happens.
37522
37632
  // https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
37633
+ // url and feedbackMessage are included in deps so the handler is re-registered
37634
+ // whenever either changes — both affect the handleSubmit closure and can change
37635
+ // independently (e.g. a subsequent RECEIVE_MESSAGE can update overlay settings
37636
+ // for the same URL, which would change feedbackMessage without changing url).
37523
37637
  (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(() => {
37524
37638
  function unsubscribe() {
37525
37639
  window.removeEventListener("message", handleSubmit);
@@ -37528,19 +37642,88 @@ const xAppOverlay = () => {
37528
37642
  return () => {
37529
37643
  unsubscribe();
37530
37644
  };
37531
- }, [closeOnSubmit]);
37532
- const showHeader = screenTitle || showCloseIcon;
37645
+ }, [closeOnSubmit, url, feedbackMessage]);
37646
+
37647
+ // APG modal dialog: move focus into the dialog once on open — to the
37648
+ // close button (the first control, as Modal does), else the frame itself.
37649
+ // Deliberately NOT the title: the dialog is named by that heading, so
37650
+ // focusing it makes NVDA read the same text as dialog name, as focused
37651
+ // heading, and again on its browse-mode pass. Deferred like
37652
+ // Modal.initialFocusRef: focusing in the same task that inserted the
37653
+ // subtree is not announced by VoiceOver (CGY-3274).
37654
+ (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(() => {
37655
+ const focusTimer = window.setTimeout(() => {
37656
+ const root = rootRef.current;
37657
+ if (!root || root.closest("[inert]")) return;
37658
+ (closeButtonRef.current ?? iframeRef.current)?.focus();
37659
+ }, 200);
37660
+ return () => window.clearTimeout(focusTimer);
37661
+ }, []);
37662
+
37663
+ // Escape dismisses the dialog exactly like the close icon (same side
37664
+ // effects), and only when the xApp is dismissable — without a close icon
37665
+ // the overlay is a blocking step that ends on submission inside the app.
37666
+ // Keys pressed inside the cross-origin frame never reach this document.
37667
+ (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(() => {
37668
+ if (!showCloseIcon) return;
37669
+ const handleKeyDown = event => {
37670
+ if (event.key !== "Escape") return;
37671
+ // Another modal surface (e.g. the disconnect overlay) is stacked on
37672
+ // top — leave Esc to that surface.
37673
+ if (rootRef.current?.closest("[inert]")) return;
37674
+ handleCloseIconClick();
37675
+ };
37676
+ document.addEventListener("keydown", handleKeyDown);
37677
+ return () => document.removeEventListener("keydown", handleKeyDown);
37678
+ }, [showCloseIcon, sendEventOnCloseIconClick]);
37679
+
37680
+ // Focus trap. A keydown-based trap (Modal) cannot see Tab presses inside
37681
+ // the cross-origin frame, so the dialog is bracketed by two focus guards
37682
+ // instead: tabbing out of the frame lands on a guard, which sends focus
37683
+ // back to the opposite edge of the dialog. Arriving at a guard from
37684
+ // outside the dialog (e.g. Tab from the host page) enters at the nearest
37685
+ // edge instead of wrapping. `relatedTarget` is null when focus comes from
37686
+ // the frame's document, which is the wrap case.
37687
+ const handleFocusGuard = edge => event => {
37688
+ const root = rootRef.current;
37689
+ if (!root) return;
37690
+ const focusable = (0,_utils_find_focusable__WEBPACK_IMPORTED_MODULE_9__/* ["default"] */ .A)(root).focusable.filter(el => !el.hasAttribute(FOCUS_GUARD_ATTRIBUTE));
37691
+ const first = focusable[0];
37692
+ const last = focusable[focusable.length - 1];
37693
+ const relatedTarget = event.relatedTarget;
37694
+ const cameFromInside = !relatedTarget || root.contains(relatedTarget);
37695
+ const target = edge === "start" ? cameFromInside ? last : first : cameFromInside ? first : last;
37696
+ target?.focus();
37697
+ };
37698
+ const showHeader = title || showCloseIcon;
37533
37699
  return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1__["default"].createElement(Root, {
37534
- tabIndex: 0,
37700
+ ref: rootRef,
37535
37701
  role: "dialog",
37536
- "aria-modal": "true"
37537
- }, showHeader && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1__["default"].createElement(_presentational_Header__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A, {
37538
- title: screenTitle,
37702
+ "aria-modal": "true",
37703
+ "aria-labelledby": title ? TITLE_ID : undefined,
37704
+ "aria-label": title ? undefined : fallbackName,
37705
+ className: "webchat-xapp-overlay-root",
37706
+ "data-xapp-overlay": true
37707
+ }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1__["default"].createElement(FocusGuard, {
37708
+ tabIndex: 0,
37709
+ onFocus: handleFocusGuard("start"),
37710
+ [FOCUS_GUARD_ATTRIBUTE]: "start"
37711
+ }), showHeader && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1__["default"].createElement(_presentational_Header__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .A, {
37712
+ title: title,
37713
+ titleId: TITLE_ID,
37539
37714
  hideBackButton: true,
37540
- onClose: showCloseIcon ? handleCloseIconClick : undefined
37715
+ onClose: showCloseIcon ? handleCloseIconClick : undefined,
37716
+ closeButtonAriaLabel: closeButtonAriaLabel,
37717
+ closeButtonRef: closeButtonRef
37541
37718
  }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1__["default"].createElement(Iframe, {
37719
+ ref: iframeRef,
37542
37720
  src: url,
37543
- allow: " accelerometer; ambient-light-sensor; autoplay; battery; bluetooth; camera; cross-origin-isolated; display-capture; document-domain; encrypted-media; execution-while-not-rendered; execution-while-out-of-viewport; fullscreen; gamepad; geolocation; gyroscope; hid; idle-detection; interest-cohort; local-fonts; magnetometer; microphone; midi; otp-credentials; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; speaker-selection; usb; web-share; xr-spatial-tracking"
37721
+ title: title || fallbackName,
37722
+ allow: " accelerometer; ambient-light-sensor; autoplay; battery; bluetooth; camera; cross-origin-isolated; display-capture; document-domain; encrypted-media; execution-while-not-rendered; execution-while-out-of-viewport; fullscreen; gamepad; geolocation; gyroscope; hid; idle-detection; interest-cohort; local-fonts; magnetometer; microphone; midi; otp-credentials; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; speaker-selection; usb; web-share; xr-spatial-tracking"
37723
+ }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1__["default"].createElement(FocusGuard, {
37724
+ tabIndex: 0,
37725
+ onFocus: handleFocusGuard("end"),
37726
+ [FOCUS_GUARD_ATTRIBUTE]: "end"
37544
37727
  }));
37545
37728
  };
37546
37729
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (xAppOverlay);
@@ -38165,6 +38348,10 @@ const InputPluginRenderer = ({
38165
38348
  /* harmony import */ var react_responsive__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(react_responsive__WEBPACK_IMPORTED_MODULE_9__);
38166
38349
  /* harmony import */ var _menu_PersistentMenu__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(71117);
38167
38350
  /* harmony import */ var _FloatingLabel__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(93277);
38351
+ /* harmony import */ var _presentational_Notifications__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(82833);
38352
+ /* harmony import */ var _utils_speech_recognition__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(44120);
38353
+
38354
+
38168
38355
 
38169
38356
 
38170
38357
 
@@ -38314,46 +38501,66 @@ const InputContainer = _emotion_styled__WEBPACK_IMPORTED_MODULE_1__/* ["default"
38314
38501
  const Label = (0,_emotion_styled__WEBPACK_IMPORTED_MODULE_1__/* ["default"] */ .A)(_FloatingLabel__WEBPACK_IMPORTED_MODULE_11__/* ["default"] */ .A)({
38315
38502
  padding: "8px 2px"
38316
38503
  });
38317
- const getSpeechRecognition = () => {
38318
- try {
38319
- return new SpeechRecognition();
38320
- } catch (e) {}
38321
- try {
38322
- return new webkitSpeechRecognition();
38323
- } catch (e) {}
38324
- return null;
38325
- };
38326
38504
  const combineStrings = (str1, str2) => {
38327
38505
  if (!str1) return str2;
38328
38506
  if (!str2) return str1;
38329
38507
  return str1 + " " + str2;
38330
38508
  };
38331
- let webchatSpeechTimeoutRef = null;
38332
38509
  class BaseInput extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureComponent {
38333
38510
  constructor(props) {
38334
38511
  super(props);
38335
- const speechRecognition = getSpeechRecognition();
38336
- if (speechRecognition) {
38337
- speechRecognition.continuous = true;
38338
- speechRecognition.interimResults = true;
38339
- speechRecognition.onresult = this.handleSpeechResult;
38340
- if (props.config.settings.widgetSettings.STTLanguage) {
38341
- speechRecognition.lang = props.config.settings.widgetSettings.STTLanguage;
38342
- }
38343
- }
38344
38512
  this.state = {
38345
38513
  text: "",
38346
- speechRecognition,
38347
38514
  speechResult: "",
38348
- isFinalResult: false,
38349
38515
  isMenuOpen: false,
38350
38516
  selectionStart: 0,
38351
38517
  selectionEnd: 0
38352
38518
  };
38519
+ this.configureSpeechRecognition();
38353
38520
  }
38354
38521
  inputRef = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createRef();
38355
38522
  menuRef = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createRef();
38356
38523
  fileInputRef = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createRef();
38524
+
38525
+ /**
38526
+ * The live recognizer, or null where the browser has no Web Speech API.
38527
+ *
38528
+ * Deliberately NOT component state: it is a mutable host object whose
38529
+ * identity never changes, and keeping it in state invited
38530
+ * `setState({ speechRecognition: { ...recognizer, lang } })` to apply the
38531
+ * language. Native properties live on the prototype, so that spread
38532
+ * copied nothing but `lang` — `start`, `stop` and every event handler
38533
+ * disappeared, and the next click threw "start is not a function"
38534
+ * (CGY-37417).
38535
+ */
38536
+ speechRecognition = (0,_utils_speech_recognition__WEBPACK_IMPORTED_MODULE_13__/* .createSpeechRecognition */ .oC)();
38537
+
38538
+ /** The warm-up or idle stop timer, whichever phase recognition is in. */
38539
+ speechTimeout = null;
38540
+
38541
+ /** Whether the engine has produced any transcript since `start()`. */
38542
+ hasTranscript = false;
38543
+
38544
+ /**
38545
+ * Set while we are the ones ending recognition, so `onend` can tell an
38546
+ * engine-initiated end (which has to reconcile the UI) from our own.
38547
+ */
38548
+ endingDeliberately = false;
38549
+
38550
+ /**
38551
+ * Set on the submit path, where a transcript arriving afterwards belongs
38552
+ * to the message that was already sent. `abort()` should prevent one, but
38553
+ * a result already dispatched can still be delivered, so the handler
38554
+ * checks this too.
38555
+ */
38556
+ discardResults = false;
38557
+
38558
+ /**
38559
+ * A start() the engine rejected because a previous stop() had not reached
38560
+ * `onend` yet. Retried once from `onend`, so a stop-then-start inside that
38561
+ * window isn't silently dropped.
38562
+ */
38563
+ restartPending = false;
38357
38564
  componentDidMount() {
38358
38565
  // Global handler to modify the input text
38359
38566
  window.WebChatInputTextCallback = text => {
@@ -38362,98 +38569,311 @@ class BaseInput extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38362
38569
  });
38363
38570
  };
38364
38571
  setTimeout(() => {
38572
+ // Don't pull focus away from a modal dialog the widget has opened in
38573
+ // the meantime (date picker, xApp overlay, Modal). Scoped to the
38574
+ // widget root: a dialog elsewhere on the host page (cookie banner, a
38575
+ // modal that embeds the launcher) must not suppress the autofocus.
38576
+ const root = this.inputRef.current?.closest("[data-cognigy-webchat-root]");
38577
+ const hasOpenDialog = !!root?.querySelector('[role="dialog"][aria-modal="true"], dialog[open]');
38578
+ if (hasOpenDialog) return;
38365
38579
  if (!this.props.config.settings.widgetSettings.disableInputAutofocus) {
38366
38580
  this.inputRef.current?.focus?.();
38367
38581
  }
38368
38582
  }, 200);
38369
38583
  }
38370
38584
  componentDidUpdate() {
38585
+ this.applySTTLanguage();
38586
+ }
38587
+ componentWillUnmount() {
38588
+ this.clearSpeechTimeout();
38589
+
38590
+ // The input unmounts on every screen change (back to the home screen,
38591
+ // into the previous-conversations list). Without releasing the engine
38592
+ // here the microphone stays open — browser recording indicator and
38593
+ // all — with nothing left to receive the transcript.
38594
+ if (this.speechRecognition) {
38595
+ this.endingDeliberately = true;
38596
+ this.speechRecognition.onresult = null;
38597
+ this.speechRecognition.onerror = null;
38598
+ this.speechRecognition.onend = null;
38599
+ try {
38600
+ // abort(), not stop(): a final transcript can no longer be
38601
+ // delivered anywhere, so don't wait for one.
38602
+ this.speechRecognition.abort();
38603
+ } catch {
38604
+ // Never started, or already finished.
38605
+ }
38606
+ }
38607
+
38608
+ // `sttActive` lives in the store, which outlives this component —
38609
+ // leaving it set would show an active mic button on the next visit to
38610
+ // the chat screen with nothing actually listening.
38611
+ if (this.props.sttActive) this.props.onSetSTTActive(false);
38612
+ }
38613
+ configureSpeechRecognition() {
38614
+ const recognition = this.speechRecognition;
38615
+ if (!recognition) return;
38616
+ recognition.continuous = true;
38617
+ recognition.interimResults = true;
38618
+ recognition.onresult = this.handleSpeechResult;
38619
+ recognition.onerror = this.handleSpeechError;
38620
+ recognition.onend = this.handleSpeechEnd;
38621
+ this.applySTTLanguage();
38622
+ }
38623
+
38624
+ /**
38625
+ * Applies the configured STT language to the live recognizer.
38626
+ *
38627
+ * Runs on every update because the endpoint config is fetched
38628
+ * asynchronously, so `STTLanguage` is typically absent when this
38629
+ * component first mounts. While it is unset we leave the engine's own
38630
+ * default (which follows the page's `lang`) rather than forcing a locale,
38631
+ * so an unconfigured German page keeps transcribing German.
38632
+ */
38633
+ applySTTLanguage() {
38371
38634
  const sttLanguage = this.props.config.settings.widgetSettings.STTLanguage;
38372
- if (this.state.speechRecognition && sttLanguage && this.state.speechRecognition.lang !== sttLanguage) {
38373
- this.setState({
38374
- speechRecognition: {
38375
- ...this.state.speechRecognition,
38376
- lang: sttLanguage
38377
- }
38378
- });
38635
+ if (this.speechRecognition && sttLanguage && this.speechRecognition.lang !== sttLanguage) {
38636
+ this.speechRecognition.lang = sttLanguage;
38379
38637
  }
38380
38638
  }
38381
- componentWillUnmount() {
38382
- if (webchatSpeechTimeoutRef) {
38383
- clearTimeout(webchatSpeechTimeoutRef);
38639
+ clearSpeechTimeout() {
38640
+ if (this.speechTimeout) {
38641
+ clearTimeout(this.speechTimeout);
38642
+ this.speechTimeout = null;
38384
38643
  }
38385
38644
  }
38645
+
38646
+ /**
38647
+ * Arms the single stop timer. `delay` is the warm-up budget before the
38648
+ * first transcript and the idle budget after it — the two differ by an
38649
+ * order of magnitude, see the constants' rationale.
38650
+ */
38651
+ armSpeechTimeout(delay) {
38652
+ this.clearSpeechTimeout();
38653
+ this.speechTimeout = setTimeout(() => {
38654
+ this.speechTimeout = null;
38655
+ const recognizedNothing = !this.hasTranscript;
38656
+
38657
+ // A dictation that produced text ended normally, and the user's
38658
+ // next move is to edit or send it, so focus goes to the input.
38659
+ // Having heard nothing is a failure like any other: leave focus
38660
+ // wherever the user put it (SC 3.2.1).
38661
+ this.endSpeech({
38662
+ restoreFocus: !recognizedNothing
38663
+ });
38664
+
38665
+ // The whole warm-up budget spent without a single word: the
38666
+ // engine never reached its speech service, or the user never
38667
+ // spoke. Either way, say so — this path used to stop the
38668
+ // microphone silently (CGY-37417).
38669
+ if (recognizedNothing) this.notifySpeechFailure("no-transcript");
38670
+ }, delay);
38671
+ }
38672
+ notifySpeechFailure(failure) {
38673
+ (0,_presentational_Notifications__WEBPACK_IMPORTED_MODULE_12__/* .createErrorNotification */ .gi)((0,_utils_speech_recognition__WEBPACK_IMPORTED_MODULE_13__/* .getSpeechRecognitionErrorMessage */ .JO)(failure, this.props.config.settings.customTranslations));
38674
+ }
38386
38675
  handleSpeechResult = e => {
38676
+ // A result that raced the abort() on the submit path. The message it
38677
+ // belongs to has already been sent and `text` cleared, so committing
38678
+ // it would refill the input with what the user just sent — and their
38679
+ // next Enter would send it a second time.
38680
+ if (this.discardResults) return;
38387
38681
  const result = e.results[e.resultIndex];
38388
- const {
38389
- isFinal
38390
- } = result;
38391
38682
  const {
38392
38683
  transcript
38393
38684
  } = result[0];
38394
38685
 
38395
- // Reset the 3s timeout for active speech recognition
38396
- if (webchatSpeechTimeoutRef) {
38397
- clearTimeout(webchatSpeechTimeoutRef);
38686
+ // Edge emits placeholder results — `isFinal` with an empty transcript
38687
+ // — when speech starts and again on stop(); Chrome never does.
38688
+ // Committing one would wipe the interim transcript on screen, and
38689
+ // counting one as "the engine is producing output" would start the
38690
+ // short idle countdown seconds before the first real word arrives.
38691
+ if (!transcript) return;
38692
+ this.hasTranscript = true;
38693
+
38694
+ // Only while listening: stop() lets one last result land afterwards,
38695
+ // and re-arming then would stop recognition and steal focus back to
38696
+ // the input seconds after the user had moved on.
38697
+ if (this.props.sttActive) this.armSpeechTimeout(_utils_speech_recognition__WEBPACK_IMPORTED_MODULE_13__/* .TRANSCRIPT_IDLE_TIMEOUT_MS */ .r0);
38698
+
38699
+ // Interim results are shown next to the typed text but not committed;
38700
+ // only a final result becomes part of the message.
38701
+ if (result.isFinal) {
38702
+ this.setState(({
38703
+ text
38704
+ }) => ({
38705
+ speechResult: "",
38706
+ text: combineStrings(text, transcript)
38707
+ }));
38708
+ return;
38398
38709
  }
38399
- webchatSpeechTimeoutRef = setTimeout(() => {
38400
- this.handleCancelSpeech();
38401
- }, 3000);
38402
38710
  this.setState({
38403
- speechResult: transcript,
38404
- isFinalResult: isFinal
38711
+ speechResult: transcript
38712
+ });
38713
+ };
38714
+ handleSpeechError = event => {
38715
+ // `aborted` is just the engine acknowledging our own stop()/abort().
38716
+ if (event.error === "aborted") return;
38717
+ if (event.error === "language-not-supported" || event.error === "bad-grammar") {
38718
+ // Misconfiguration the end user cannot act on: they get the
38719
+ // generic message, the operator gets the detail.
38720
+ console.warn(`[cognigy-webchat] speech recognition rejected the configured STT language "${this.speechRecognition?.lang}" (${event.error})`);
38721
+ }
38722
+
38723
+ // A failure is not a user-initiated stop: leave focus alone. The
38724
+ // button's `aria-pressed` change and the notification convey it.
38725
+ this.endSpeech({
38726
+ restoreFocus: false
38727
+ });
38728
+ this.notifySpeechFailure(event.error);
38729
+ };
38730
+ handleSpeechEnd = () => {
38731
+ const wasDeliberate = this.endingDeliberately;
38732
+ this.endingDeliberately = false;
38733
+
38734
+ // The engine is finished, so anything still held as an interim never
38735
+ // got a final result. Commit it rather than let the user's visible
38736
+ // words vanish. A real final would already have moved it into `text`
38737
+ // and emptied this, so there is nothing to duplicate.
38738
+ this.commitPendingTranscript();
38739
+
38740
+ // The user pressed the button again while the engine was still
38741
+ // winding down from the previous stop, so `start()` was rejected.
38742
+ // Honour it now that the engine has actually ended — once, because
38743
+ // only a rejected start sets the flag.
38744
+ if (this.restartPending) {
38745
+ this.restartPending = false;
38746
+ this.startSpeech();
38747
+ return;
38748
+ }
38749
+ if (wasDeliberate) return;
38750
+
38751
+ // The engine ended on its own — its service dropped the connection,
38752
+ // or it decided the utterance was over despite `continuous`.
38753
+ // Reconcile, so the button can never claim to be listening when
38754
+ // nothing is (SC 4.1.2).
38755
+ if (!this.props.sttActive) return;
38756
+ const recognizedNothing = !this.hasTranscript;
38757
+ this.endSpeech({
38758
+ restoreFocus: false
38405
38759
  });
38406
38760
 
38407
- // only send messages that are not 'interim'
38408
- if (isFinal) {
38409
- this.setState({
38410
- speechResult: "",
38411
- text: combineStrings(this.state.text, transcript)
38412
- });
38413
- }
38761
+ // Ending with nothing to show for it is exactly the silent failure
38762
+ // this change exists to remove, so say so. `onerror` cannot reach
38763
+ // here — it ends recognition itself, which marks the end deliberate.
38764
+ if (recognizedNothing) this.notifySpeechFailure("no-transcript");
38414
38765
  };
38766
+
38767
+ /**
38768
+ * Moves an un-finalized transcript into the message text. No-op when the
38769
+ * engine already finalized it (or when the submit path discarded it).
38770
+ */
38771
+ commitPendingTranscript() {
38772
+ this.setState(({
38773
+ text,
38774
+ speechResult
38775
+ }) => speechResult ? {
38776
+ text: combineStrings(text, speechResult),
38777
+ speechResult: ""
38778
+ } : null);
38779
+ }
38780
+
38781
+ /** The user pressed the microphone button to stop dictating. */
38415
38782
  handleCancelSpeech = () => {
38416
- const {
38417
- speechRecognition
38418
- } = this.state;
38419
- speechRecognition.stop();
38420
- if (webchatSpeechTimeoutRef) {
38421
- clearTimeout(webchatSpeechTimeoutRef);
38783
+ this.endSpeech({
38784
+ restoreFocus: true
38785
+ });
38786
+ };
38787
+
38788
+ /**
38789
+ * Ends recognition and reconciles the UI.
38790
+ *
38791
+ * `discardPending` is for the submit path only: the message has been sent
38792
+ * and `text` cleared, so a transcript arriving afterwards has to be
38793
+ * thrown away rather than committed. Everywhere else a late final is
38794
+ * welcome — it is the engine's better-punctuated version of the interim
38795
+ * that was on screen, and dropping it would lose the user's words.
38796
+ *
38797
+ * `restoreFocus` is true only when the user themselves stopped: they are
38798
+ * likely to carry on typing. On an error or a timeout the stop was not
38799
+ * requested, and moving focus then would take it from wherever the user
38800
+ * had put it (SC 3.2.1).
38801
+ */
38802
+ endSpeech({
38803
+ discardPending = false,
38804
+ restoreFocus
38805
+ }) {
38806
+ this.clearSpeechTimeout();
38807
+ this.endingDeliberately = true;
38808
+ this.discardResults = discardPending;
38809
+ try {
38810
+ if (discardPending) {
38811
+ // abort() asks the engine not to return a result at all,
38812
+ // unlike stop(), which finalizes what it heard.
38813
+ this.speechRecognition?.abort();
38814
+ } else {
38815
+ this.speechRecognition?.stop();
38816
+ }
38817
+ } catch {
38818
+ // Not currently running.
38422
38819
  }
38423
38820
  this.props.onSetSTTActive(false);
38424
- this.setState({
38425
- speechResult: "",
38426
- isFinalResult: false
38821
+
38822
+ // The interim transcript deliberately stays on screen. The engine may
38823
+ // still upgrade it to a final; if it never does, `onend` commits it.
38824
+ // Clearing it here would lose the words the user watched appear —
38825
+ // Edge answers stop() with an empty placeholder final, which carries
38826
+ // no text to put back. Only the submit path clears it, because that
38827
+ // text is already on its way.
38828
+ if (discardPending) this.setState({
38829
+ speechResult: ""
38427
38830
  });
38428
- if (this.inputRef.current) {
38831
+ if (restoreFocus && this.inputRef.current) {
38429
38832
  this.inputRef.current.focus();
38430
38833
  }
38431
- };
38834
+ }
38432
38835
  isSTTSupported() {
38433
- return !!this.state.speechRecognition;
38836
+ return !!this.speechRecognition;
38434
38837
  }
38435
38838
  toggleSTT = e => {
38436
38839
  e.preventDefault();
38437
- const {
38438
- speechRecognition
38439
- } = this.state;
38440
- const {
38441
- sttActive,
38442
- onSetSTTActive
38443
- } = this.props;
38444
- if (sttActive) {
38840
+ if (this.props.sttActive) {
38445
38841
  this.handleCancelSpeech();
38446
- } else {
38447
- speechRecognition.start();
38448
- if (webchatSpeechTimeoutRef) {
38449
- clearTimeout(webchatSpeechTimeoutRef);
38450
- }
38451
- webchatSpeechTimeoutRef = setTimeout(() => {
38452
- this.handleCancelSpeech();
38453
- }, 3000);
38842
+ return;
38454
38843
  }
38455
- onSetSTTActive(!sttActive);
38844
+ this.startSpeech();
38456
38845
  };
38846
+ startSpeech() {
38847
+ const recognition = this.speechRecognition;
38848
+ if (!recognition) return;
38849
+ try {
38850
+ recognition.start();
38851
+ } catch (error) {
38852
+ if (error?.name === "InvalidStateError") {
38853
+ // The engine is still winding down from a previous stop()
38854
+ // whose `onend` hasn't arrived. Claiming to listen here would
38855
+ // leave the button on with nothing running, so wait for the
38856
+ // end and start then.
38857
+ this.restartPending = true;
38858
+ return;
38859
+ }
38860
+
38861
+ // The engine never started listening, so the button must not
38862
+ // claim that it did.
38863
+ this.notifySpeechFailure("start-failed");
38864
+ return;
38865
+ }
38866
+
38867
+ // Only now that a session is really running: a rejected start above
38868
+ // leaves the previous session's flags in place, so a stale result
38869
+ // still in flight from an aborted submit stays discarded instead of
38870
+ // refilling the input with the message that was just sent.
38871
+ this.hasTranscript = false;
38872
+ this.endingDeliberately = false;
38873
+ this.discardResults = false;
38874
+ this.armSpeechTimeout(_utils_speech_recognition__WEBPACK_IMPORTED_MODULE_13__/* .FIRST_TRANSCRIPT_TIMEOUT_MS */ .rt);
38875
+ this.props.onSetSTTActive(true);
38876
+ }
38457
38877
  handleChangeTextValue = e => {
38458
38878
  this.setState({
38459
38879
  text: e.target.value
@@ -38469,16 +38889,39 @@ class BaseInput extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38469
38889
  } = this.state;
38470
38890
  const {
38471
38891
  sttActive,
38472
- fileList
38892
+ fileList,
38893
+ fileUploadError
38473
38894
  } = this.props;
38474
- let messageText = text;
38895
+
38896
+ // Exactly what the field shows, which is `text` plus any transcript
38897
+ // not yet finalized — including after recognition stopped, while the
38898
+ // interim waits for `onend` to commit it.
38899
+ const messageText = combineStrings(text, speechResult);
38475
38900
  if (sttActive) {
38476
- if (text && speechResult) {
38477
- messageText = text + " " + speechResult;
38478
- }
38479
- this.handleCancelSpeech();
38901
+ // Discard whatever the engine still owes us: it belongs to this
38902
+ // message, which is on its way. handleSubmit restores focus to
38903
+ // the input itself once the message is sent.
38904
+ this.endSpeech({
38905
+ discardPending: true,
38906
+ restoreFocus: false
38907
+ });
38480
38908
  }
38481
- if (!text && !fileList) return;
38909
+
38910
+ /**
38911
+ * Enter reaches `handleSubmit` straight from `handleInputKeyDown`, which does
38912
+ * not consult the Send button — so both of the button's `disabled` conditions
38913
+ * have to be repeated here or Enter bypasses them.
38914
+ *
38915
+ * `fileList` is always an array (it defaults to `[]`), so emptiness has to be
38916
+ * checked by length: `!fileList` would never be true.
38917
+ *
38918
+ * The text half checks `messageText` rather than the button's `state.text`
38919
+ * on purpose: while dictating, the pending transcript is shown in the field
38920
+ * but is not in `text` yet, and a dictation the user never typed a word of
38921
+ * must still submit (CGY-37417).
38922
+ */
38923
+ if (fileUploadError) return;
38924
+ if (!messageText && !fileList?.length) return;
38482
38925
  const attachments = [];
38483
38926
  fileList.forEach(fileItem => {
38484
38927
  if (fileItem.uploadFileMeta) {
@@ -38495,7 +38938,8 @@ class BaseInput extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38495
38938
  }
38496
38939
  this.props.onSetFileList([]);
38497
38940
  this.setState({
38498
- text: ""
38941
+ text: "",
38942
+ speechResult: ""
38499
38943
  }, () => {
38500
38944
  this.props.onSendMessage(messageText, data, {
38501
38945
  collate: true
@@ -38657,8 +39101,15 @@ class BaseInput extends react__WEBPACK_IMPORTED_MODULE_0__["default"].PureCompon
38657
39101
  } : undefined
38658
39102
  }));
38659
39103
  }), props.config.settings.behavior.enableSTT && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(SpeechButton, {
39104
+ type: "button",
38660
39105
  className: classnames__WEBPACK_IMPORTED_MODULE_2___default()("webchat-input-button-speech", sttActive && "webchat-input-button-speech-active"),
38661
- "aria-label": customTranslations?.ariaLabels?.speechToText ?? "Speech to text",
39106
+ "aria-label": customTranslations?.ariaLabels?.speechToText ?? "Speech to text"
39107
+ // APG toggle button: the "listening"
39108
+ // state is otherwise conveyed only by
39109
+ // the animated background, which is
39110
+ // aria-hidden (SC 4.1.2).
39111
+ ,
39112
+ "aria-pressed": sttActive,
38662
39113
  id: "webchatInputMessageSpeechButton",
38663
39114
  onClick: this.toggleSTT,
38664
39115
  disabled: !this.isSTTSupported()
@@ -38999,6 +39450,7 @@ const PreviewUploadedFiles = () => {
38999
39450
  key: index,
39000
39451
  id: `filePreview${index}`
39001
39452
  }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(UploadedFilePreview, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(RemoveFileButton, {
39453
+ type: "button",
39002
39454
  onClick: () => onRemoveFileButtonClick(index),
39003
39455
  onFocus: () => {
39004
39456
  if (removeFileButtonRefs.current[index]) {
@@ -39979,6 +40431,8 @@ const Header = props => {
39979
40431
  showChatScreen,
39980
40432
  autoFocusScreenTitle,
39981
40433
  onDeleteAllConversations,
40434
+ titleId = "webchatHeaderTitle",
40435
+ closeButtonAriaLabel,
39982
40436
  ...rest
39983
40437
  } = props;
39984
40438
  const ariaLabels = (0,_webchat_helper_useSelector__WEBPACK_IMPORTED_MODULE_13__/* .useSelector */ .d)(state => state.config.settings.customTranslations?.ariaLabels);
@@ -40023,10 +40477,10 @@ const Header = props => {
40023
40477
  title: "Cognigy.AI Logo",
40024
40478
  role: "img",
40025
40479
  className: classnames__WEBPACK_IMPORTED_MODULE_8___default()("webchat-header-cognigy-logo")
40026
- })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(HeaderText, {
40480
+ })), title?.trim() && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(HeaderText, {
40027
40481
  ref: headerTextRef,
40028
40482
  variant: "h2-semibold",
40029
- id: "webchatHeaderTitle",
40483
+ id: titleId,
40030
40484
  className: "webchat-header-title",
40031
40485
  margin: 0,
40032
40486
  tabIndex: -1
@@ -40058,7 +40512,7 @@ const Header = props => {
40058
40512
  "data-header-close-button": true,
40059
40513
  onClick: handleCloseClick,
40060
40514
  className: "webchat-header-close-button",
40061
- "aria-label": ariaLabels?.closeChat ?? "Close chat",
40515
+ "aria-label": closeButtonAriaLabel ?? ariaLabels?.closeChat ?? "Close chat",
40062
40516
  ref: closeButtonRef
40063
40517
  }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_assets_close_16px_svg__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A, null)))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_Notifications__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .Ay, null));
40064
40518
  };
@@ -40646,7 +41100,8 @@ MultilineInput.displayName = "MultilineInput";
40646
41100
 
40647
41101
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
40648
41102
  /* harmony export */ Ay: () => (__WEBPACK_DEFAULT_EXPORT__),
40649
- /* harmony export */ UI: () => (/* binding */ createNotification)
41103
+ /* harmony export */ UI: () => (/* binding */ createNotification),
41104
+ /* harmony export */ gi: () => (/* binding */ createErrorNotification)
40650
41105
  /* harmony export */ });
40651
41106
  /* unused harmony export createPersistentNotification */
40652
41107
  /* unused harmony import specifier */ var toast;
@@ -40662,6 +41117,25 @@ const Notifications = () => {
40662
41117
  gutter: 1,
40663
41118
  toastOptions: {
40664
41119
  duration: 1500,
41120
+ // Failures stay up much longer than confirmations: the user has
41121
+ // to read a remedy and act on it, not just register that
41122
+ // something worked. The longest of these ("Microphone access
41123
+ // is blocked. Allow it in your browser settings…") is ~95
41124
+ // characters, which needs well over 5s to notice and read
41125
+ // (SC 2.2.1).
41126
+ // red40 on red10 is 5.27:1, mirroring the vetted green pairing
41127
+ // below (SC 1.4.3).
41128
+ error: {
41129
+ duration: 12000,
41130
+ style: {
41131
+ backgroundColor: theme.red10,
41132
+ color: theme.red40
41133
+ },
41134
+ iconTheme: {
41135
+ primary: theme.red40,
41136
+ secondary: theme.red10
41137
+ }
41138
+ },
40665
41139
  style: {
40666
41140
  backgroundColor: theme.green10,
40667
41141
  borderRadius: 0,
@@ -40713,6 +41187,20 @@ function createNotification(message, options = {}) {
40713
41187
  });
40714
41188
  }
40715
41189
 
41190
+ /**
41191
+ * Same as `createNotification`, styled as a failure and shown for longer.
41192
+ * Use for anything the user needs to act on (e.g. a dictation that could not
41193
+ * be started) — the message is mirrored into <StatusLiveRegion> like every
41194
+ * other toast, so it reaches screen readers too (SC 3.3.1 / 4.1.3).
41195
+ */
41196
+ function createErrorNotification(message, options = {}) {
41197
+ react_hot_toast__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .Ay.error(message, {
41198
+ ...options,
41199
+ className: "webchat-toast-notification webchat-toast-notification-error",
41200
+ ariaProps: silencedAriaProps
41201
+ });
41202
+ }
41203
+
40716
41204
  /**
40717
41205
  * Creates a persistent notification.
40718
41206
  * Can be dismissed by calling the returned function.
@@ -41716,6 +42204,68 @@ const UnreadMessagePreview = _emotion_styled__WEBPACK_IMPORTED_MODULE_0__/* ["de
41716
42204
 
41717
42205
  /***/ },
41718
42206
 
42207
+ /***/ 1715
42208
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
42209
+
42210
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
42211
+ /* harmony export */ A: () => (__WEBPACK_DEFAULT_EXPORT__)
42212
+ /* harmony export */ });
42213
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(51649);
42214
+ /* harmony import */ var _SrOnlyLiveRegion__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(65766);
42215
+
42216
+
42217
+ let unreadAnnouncementCounter = 0;
42218
+
42219
+ /**
42220
+ * Announces the unread-message count when it changes (WCAG 4.1.3 Status
42221
+ * Messages, technique ARIA22, CGY-3163). Sighted users get this count from
42222
+ * the badge on the toggle button and from the page title indicator; neither
42223
+ * reaches a screen reader on its own — the badge only changes the toggle
42224
+ * button's accessible name (read on focus, never live) and browsers do not
42225
+ * announce `document.title` changes at all.
42226
+ *
42227
+ * Unread messages only accumulate while the chat window is closed or the
42228
+ * page is hidden, so <StatusLiveRegion> (inside the open window) cannot
42229
+ * carry this. The region is mounted for the page lifetime — next to the
42230
+ * toggle button, independent of it — so it pre-exists the first count
42231
+ * change (a live region only announces changes to a node already in the
42232
+ * accessibility tree).
42233
+ *
42234
+ * role="status" (polite), not role="alert": a new-message notice is
42235
+ * advisory and must not interrupt what the user is doing on the host page.
42236
+ *
42237
+ * `active` mirrors the visual indicators (badge or title indicator
42238
+ * enabled): when neither is shown, screen-reader users are not notified
42239
+ * either. Clearing the count (chat opened, page visible again) empties the
42240
+ * region silently so users browsing the page later don't find stale text.
42241
+ */
42242
+ const UnreadMessagesAnnouncer = ({
42243
+ active,
42244
+ count,
42245
+ singularText,
42246
+ pluralText
42247
+ }) => {
42248
+ const [message, setMessage] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
42249
+ (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
42250
+ if (!active || count === 0) {
42251
+ setMessage(null);
42252
+ return;
42253
+ }
42254
+ setMessage({
42255
+ id: `webchatUnreadMessages-${++unreadAnnouncementCounter}`,
42256
+ text: count === 1 ? singularText : `${count} ${pluralText}`
42257
+ });
42258
+ }, [active, count, singularText, pluralText]);
42259
+ return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["default"].createElement(_SrOnlyLiveRegion__WEBPACK_IMPORTED_MODULE_1__/* .SrOnlyLiveRegion */ .Y, {
42260
+ id: "webchatUnreadMessagesLiveRegion",
42261
+ role: "status",
42262
+ message: message
42263
+ });
42264
+ };
42265
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (UnreadMessagesAnnouncer);
42266
+
42267
+ /***/ },
42268
+
41719
42269
  /***/ 31961
41720
42270
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
41721
42271
 
@@ -43196,9 +43746,18 @@ const INITIAL_NOTICE_SESSION = {
43196
43746
  * only — later restores (reopening a previous conversation) are already
43197
43747
  * covered by the snapshot lookup, and the flag must not silence a
43198
43748
  * brand-new conversation started afterwards.
43199
- */
43200
- function computeNoticeSession(prev, currentSessionId, prevConversationsSnapshot, hasRestoredPersistedHistory) {
43201
- const isRestoredFirstConnect = prev.id === "" && !!hasRestoredPersistedHistory;
43749
+ *
43750
+ * `willRestorePersistedConversation` is that same page-reload case seen
43751
+ * BEFORE the connect: the restore only happens when the connect
43752
+ * resolves, which can take longer than the notice's 600ms announce
43753
+ * delay, and by then an announcement can no longer be taken back. It is
43754
+ * read from storage through the very helpers the restore uses (see
43755
+ * `hasPersistedConversationForInitialSession`), so it cannot disagree
43756
+ * with the restore that follows. Same first-connect-only scope as the
43757
+ * flag above, for the same reason.
43758
+ */
43759
+ function computeNoticeSession(prev, currentSessionId, prevConversationsSnapshot, hasRestoredPersistedHistory, willRestorePersistedConversation) {
43760
+ const isRestoredFirstConnect = prev.id === "" && (!!hasRestoredPersistedHistory || !!willRestorePersistedConversation);
43202
43761
  const isNew = !prevConversationsSnapshot?.[currentSessionId] && !isRestoredFirstConnect;
43203
43762
  const isFirstConnectOfNewConversation = prev.id === "" && prev.isNew && isNew;
43204
43763
  return {
@@ -43216,6 +43775,12 @@ function computeNoticeSession(prev, currentSessionId, prevConversationsSnapshot,
43216
43775
  * reconnect the socket and open it — its dialog and focus utterances
43217
43776
  * would cancel the notice; on close, the intro becomes pending again
43218
43777
  * and is announced after the "Connection restored" utterances).
43778
+ *
43779
+ * Eligibility only — WHEN an eligible notice is spoken is the live
43780
+ * region's business (it defers the intro past the screen's focus
43781
+ * utterances). That deferral is why the verdict must be final before the
43782
+ * first connect resolves rather than after it, which is what
43783
+ * `willRestorePersistedConversation` in `computeNoticeSession` provides.
43219
43784
  */
43220
43785
  function getAIAgentNoticeIntroText(args) {
43221
43786
  const {
@@ -43343,8 +43908,10 @@ const getMessagesListWithoutControlCommands = (messages, types = EXCLUDE_CC_TYPE
43343
43908
  * @returns {Array}
43344
43909
  */
43345
43910
  const getKeyboardFocusableElements = element => {
43346
- // Get all interactive elements in given element
43347
- const interactiveEls = element?.querySelectorAll('a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])');
43911
+ // Get all interactive elements in given element. An <iframe> is a
43912
+ // sequential-focus stop in every browser (Tab enters the frame's
43913
+ // document), so it counts as focusable — e.g. the xApp overlay's frame.
43914
+ const interactiveEls = element?.querySelectorAll('a[href], button, input, textarea, select, details, iframe, [tabindex]:not([tabindex="-1"])');
43348
43915
  const interactiveElsArray = interactiveEls && Array.from(interactiveEls);
43349
43916
 
43350
43917
  // Two lists are derived from the interactive elements:
@@ -43687,6 +44254,113 @@ function getSourceBackgroundColor(mappedValue, colors) {
43687
44254
 
43688
44255
  /***/ },
43689
44256
 
44257
+ /***/ 44120
44258
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
44259
+
44260
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
44261
+ /* harmony export */ JO: () => (/* binding */ getSpeechRecognitionErrorMessage),
44262
+ /* harmony export */ oC: () => (/* binding */ createSpeechRecognition),
44263
+ /* harmony export */ r0: () => (/* binding */ TRANSCRIPT_IDLE_TIMEOUT_MS),
44264
+ /* harmony export */ rt: () => (/* binding */ FIRST_TRANSCRIPT_TIMEOUT_MS)
44265
+ /* harmony export */ });
44266
+ /**
44267
+ * Web Speech API plumbing for the message input's speech-to-text button.
44268
+ *
44269
+ * The spec says nothing about latency, and engines differ by seconds — which
44270
+ * is what CGY-37417 turned out to be. Measured on macOS with a real
44271
+ * microphone, one spoken sentence, `continuous`/`interimResults` both on:
44272
+ *
44273
+ * | event | Chrome 152 | Edge 152 |
44274
+ * | --------------------------- | ---------- | ------------- |
44275
+ * | `audiostart` after `start()`| 0.55 s | 0.52 s |
44276
+ * | `speechstart` -> 1st result | 0.00 s | 0.32 - 4.25 s |
44277
+ * | `start()` -> 1st transcript | 1.37 s | 5.02 - 6.65 s |
44278
+ *
44279
+ * Chrome streams an interim result in the same tick as `speechstart`; Edge
44280
+ * buffers whole phrases against its cloud service and can take over six
44281
+ * seconds to hand back the first word. Edge also emits *placeholder* results
44282
+ * — `isFinal` with an empty transcript — when speech starts and again on
44283
+ * `stop()`, which Chrome never does. Any timing budget here has to hold for
44284
+ * the slow engine, and any result handler has to tolerate empty transcripts.
44285
+ */
44286
+
44287
+ /**
44288
+ * How long to wait after `start()` for the engine's first transcript before
44289
+ * giving up and telling the user nothing was heard.
44290
+ *
44291
+ * This has to cover the permission prompt (on first use the clock is already
44292
+ * running while the user hunts for the browser's "Allow" button), the
44293
+ * engine's connection to its speech service, the user pausing to collect
44294
+ * their thoughts, and then the engine's own recognition latency — 6.65 s of
44295
+ * it on Edge. The previous budget was 3 s counted from `start()`, so on Edge
44296
+ * recognition was routinely cancelled before it had produced a single word,
44297
+ * and the failure was silent (CGY-37417).
44298
+ */
44299
+ const FIRST_TRANSCRIPT_TIMEOUT_MS = 15000;
44300
+
44301
+ /**
44302
+ * How long to keep listening after the last transcript before auto-stopping,
44303
+ * so the user doesn't have to press the button a second time.
44304
+ *
44305
+ * Only armed once the engine has actually produced a transcript, which is
44306
+ * what makes 3 s safe: while speech is flowing, consecutive results arrive
44307
+ * ~0.2 - 0.7 s apart on both engines. Before the first transcript,
44308
+ * FIRST_TRANSCRIPT_TIMEOUT_MS applies instead.
44309
+ */
44310
+ const TRANSCRIPT_IDLE_TIMEOUT_MS = 3000;
44311
+
44312
+ /** Reasons recognition can fail, beyond the spec's own error codes. */
44313
+
44314
+ /**
44315
+ * Creates a recognizer, or `null` where the browser has no Web Speech API
44316
+ * (Firefox, and WebViews that drop it). Callers treat `null` as
44317
+ * "speech-to-text unsupported" and disable the button.
44318
+ */
44319
+ const createSpeechRecognition = () => {
44320
+ const Recognition = typeof SpeechRecognition !== "undefined" ? SpeechRecognition : typeof webkitSpeechRecognition !== "undefined" ? webkitSpeechRecognition : null;
44321
+ if (!Recognition) return null;
44322
+ try {
44323
+ return new Recognition();
44324
+ } catch {
44325
+ // Constructing can still throw where the API is present but disabled
44326
+ // by policy or an unsupported embedding context.
44327
+ return null;
44328
+ }
44329
+ };
44330
+
44331
+ /**
44332
+ * Maps a failure onto the message shown to the user, so a failed dictation
44333
+ * says what went wrong and what to do instead of failing silently
44334
+ * (SC 3.3.1 Error Identification).
44335
+ */
44336
+ const getSpeechRecognitionErrorMessage = (failure, customTranslations) => {
44337
+ switch (failure) {
44338
+ // The user (or a policy) blocked the microphone. Only they can undo
44339
+ // it, and only in browser settings — the prompt won't come back.
44340
+ // NOT `service-not-allowed`, which per the spec means the recognition
44341
+ // *service* was refused and says nothing about microphone access;
44342
+ // sending those users to their microphone settings would be a wrong
44343
+ // remedy, so it falls through to the generic message below.
44344
+ case "not-allowed":
44345
+ return customTranslations?.speech_recognition_not_allowed ?? "Microphone access is blocked. Allow it in your browser settings to use speech input.";
44346
+ case "audio-capture":
44347
+ return customTranslations?.speech_recognition_no_microphone ?? "No microphone was found. Connect one to use speech input.";
44348
+ case "no-speech":
44349
+ case "no-transcript":
44350
+ return customTranslations?.speech_recognition_no_speech ?? "No speech was detected. Please try again.";
44351
+
44352
+ // "network" is the engine's speech service being unreachable;
44353
+ // "service-not-allowed" is that service being refused to this page;
44354
+ // "language-not-supported" and "bad-grammar" are misconfiguration the
44355
+ // end user cannot act on (both are logged for the operator instead).
44356
+ // None of these have a remedy the user can apply beyond typing.
44357
+ default:
44358
+ return customTranslations?.speech_recognition_error ?? "Speech input is currently unavailable. Please type your message instead.";
44359
+ }
44360
+ };
44361
+
44362
+ /***/ },
44363
+
43690
44364
  /***/ 61395
43691
44365
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
43692
44366
 
@@ -43704,6 +44378,8 @@ function getSourceBackgroundColor(mappedValue, colors) {
43704
44378
  /* harmony import */ var _store_input_input_reducer__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(16967);
43705
44379
  /* harmony import */ var _store_xapp_overlay_slice__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(97114);
43706
44380
  /* harmony import */ var _store_messages_message_reducer__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(22284);
44381
+ /* harmony import */ var _store_options_persisted_conversation__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(57589);
44382
+
43707
44383
 
43708
44384
 
43709
44385
 
@@ -43773,6 +44449,7 @@ const ConnectedWebchatUI = (0,react_redux__WEBPACK_IMPORTED_MODULE_1__/* .connec
43773
44449
  unseenMessages,
43774
44450
  prevConversations,
43775
44451
  hasRestoredPersistedHistory,
44452
+ willRestorePersistedConversation: (0,_store_options_persisted_conversation__WEBPACK_IMPORTED_MODULE_11__/* .hasPersistedConversationForInitialSession */ ._)(state),
43776
44453
  open,
43777
44454
  typingIndicator: typing,
43778
44455
  inputMode,
@@ -44520,8 +45197,33 @@ function setHasAcceptedTermsInStorage(browserStorage, userId) {
44520
45197
  /* harmony import */ var _store_store__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6236);
44521
45198
 
44522
45199
 
44523
- const allowedHtmlTags = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "label", "legend", "li", "link", "main", "map", "mark", "meta", "meter", "nav", "noframes", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"];
44524
- const allowedHtmlAttributes = ["accept", "accept-charset", "accesskey", "action", "align", "alt", "autocomplete", "autofocus", "autoplay", "bgcolor", "border", "charset", "checked", "cite", "class", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "data", "data-*", "datetime", "default", "dir", "dirname", "disabled", "download", "draggable", "dropzone", "enctype", "for", "form", "formaction", "headers", "height", "hidden", "high", "href", "hreflang", "http-equiv", "id", "ismap", "kind", "label", "lang", "list", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "novalidate", "open", "optimum", "pattern", "placeholder", "poster", "preload", "readonly", "rel", "required", "reversed", "rows", "rowspan", "sandbox", "scope", "selected", "shape", "size", "sizes", "span", "spellcheck", "src", "srcdoc", "srclang", "srcset", "start", "step", "style", "tabindex", "target", "title", "translate", "type", "usemap", "value", "width", "wrap"];
45200
+
45201
+ // Tags removed from the previous allow-list to align with DOMPurify's secure defaults
45202
+ // (WCH-SI10-001). Each tag enables a distinct attack vector:
45203
+ // applet — Java applet execution
45204
+ // base — rewrites all relative URLs on the host page
45205
+ // body / html / head — structural document elements; no legitimate use in sanitised fragments
45206
+ // embed — loads arbitrary external content / plugins
45207
+ // form — posts user data to attacker-controlled URLs
45208
+ // frame / frameset / noframes — clickjacking and legacy frame injection
45209
+ // iframe — inline HTML documents; srcdoc = direct XSS vector
45210
+ // link — loads external stylesheets
45211
+ // meta — HTTP redirects and CSP bypass via http-equiv
45212
+ // object — loads Flash, PDFs, and arbitrary external content
45213
+ // style — CSS injection and attribute-value exfiltration
45214
+ // These tags are blocked by default when no custom tag list is configured.
45215
+ // Tenants can supply a replacement list via widgetSettings.customAllowedHtmlTags,
45216
+ // but dangerous tags are always stripped from that list before it is applied (PR #309).
45217
+ const allowedHtmlTags = ["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "basefont", "bdi", "bdo", "big", "blockquote", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "fieldset", "figcaption", "figure", "font", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "meter", "nav", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "source", "span", "strike", "strong", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"];
45218
+
45219
+ // Attributes that are always blocked regardless of caller configuration (WCH-SI10-001).
45220
+ // Removed from the previous allow-list:
45221
+ // action / formaction — form submission to attacker-controlled URLs
45222
+ // sandbox — giving content control over its own sandbox policy
45223
+ // srcdoc — inline HTML document in an iframe (direct XSS vector)
45224
+ // style — inline CSS injection and attribute-value exfiltration
45225
+ // target — controls navigation target (_blank without rel is risky)
45226
+ const allowedHtmlAttributes = ["accept", "accept-charset", "accesskey", "align", "alt", "autocomplete", "autofocus", "autoplay", "bgcolor", "border", "charset", "checked", "cite", "class", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "data", "data-*", "datetime", "default", "dir", "dirname", "disabled", "download", "draggable", "dropzone", "enctype", "for", "form", "headers", "height", "hidden", "high", "href", "hreflang", "http-equiv", "id", "ismap", "kind", "label", "lang", "list", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "novalidate", "open", "optimum", "pattern", "placeholder", "poster", "preload", "readonly", "rel", "required", "reversed", "rows", "rowspan", "scope", "selected", "shape", "size", "sizes", "span", "spellcheck", "src", "srclang", "srcset", "start", "step", "tabindex", "title", "translate", "type", "usemap", "value", "width", "wrap"];
44525
45227
  const config = {
44526
45228
  ALLOWED_TAGS: allowedHtmlTags,
44527
45229
  ALLOWED_ATTR: allowedHtmlAttributes
@@ -46497,6 +47199,124 @@ const getOptionsKey = ({
46497
47199
 
46498
47200
  /***/ },
46499
47201
 
47202
+ /***/ 57589
47203
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
47204
+
47205
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
47206
+ /* harmony export */ _: () => (/* binding */ hasPersistedConversationForInitialSession)
47207
+ /* harmony export */ });
47208
+ /* harmony import */ var _options__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(48242);
47209
+ /* harmony import */ var _helper_storage__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(62473);
47210
+
47211
+
47212
+
47213
+ /**
47214
+ * Whether browser storage already holds a non-empty conversation for the
47215
+ * session this page load will connect with — i.e. whether the first
47216
+ * connect is going to restore a conversation rather than start one.
47217
+ *
47218
+ * This answers, BEFORE the socket connects, the question the AI-agent
47219
+ * notice needs answered (CGY-3519): a restored conversation is a
47220
+ * continuation and must not be announced. Waiting for the connect to
47221
+ * tell us instead is not an option — it can take longer than the
47222
+ * notice's 600ms announce delay, and an announcement cannot be taken
47223
+ * back.
47224
+ *
47225
+ * It deliberately mirrors the restore in `optionsMiddleware`'s
47226
+ * `SET_OPTIONS` handler: same `getStorage`, same `getOptionsKey`, same
47227
+ * "non-empty messages" condition that `RESET_STATE` uses to set
47228
+ * `hasRestoredPersistedHistory`. Reading the same inputs through the
47229
+ * same helpers is what keeps the prediction and the restore aligned —
47230
+ * including when they agree on "no": if the endpoint config (and with it
47231
+ * `URLToken`) has not loaded yet, the key misses here exactly as it
47232
+ * misses there, and nothing is restored either. The one case where they
47233
+ * part company is benign: a message sent before the first connect is
47234
+ * dispatched ahead of `setOptions`, so `RESET_STATE` finds a non-empty
47235
+ * history and skips the restore while this still says "continued" — the
47236
+ * session IS a continuation for the backend, so staying silent remains
47237
+ * the right outcome.
47238
+ *
47239
+ * `config.initialSessionId` is the pinned `sessionId` from the embedding
47240
+ * options. Without one, the socket client mints a fresh session id per
47241
+ * page load, which no stored entry can match, so there is nothing to
47242
+ * predict.
47243
+ */
47244
+ // mapStateToProps runs on every dispatch and a persisted conversation can
47245
+ // be sizeable, so this must not sit on the hot path: the parse is cached
47246
+ // against the raw string it was derived from, and the read stops
47247
+ // altogether once the answer is spent (see below). Until then `getItem`
47248
+ // runs on every call, which is what keeps the answer current — the key
47249
+ // changes once the endpoint config supplies its URLToken.
47250
+ let lastRead = null;
47251
+ const hasPersistedConversationForInitialSession = state => {
47252
+ const sessionId = state.config.initialSessionId;
47253
+ if (!sessionId) return false;
47254
+
47255
+ // This page load ended up on a session other than the pinned one, so
47256
+ // nothing stored under the pinned key is being restored into it and
47257
+ // there is nothing to predict. "Start new conversation" from the
47258
+ // conversations list does this before the first connect
47259
+ // (`SWITCH_SESSION` with no id mints a fresh session id), and without
47260
+ // this guard that brand-new conversation would inherit the pinned
47261
+ // session's "continued" verdict and never be announced — the mirror
47262
+ // image of the bug this predicate exists for. A REopened previous
47263
+ // conversation also lands here, and is correctly silenced by the
47264
+ // `prevConversations` snapshot instead.
47265
+ if (state.options.sessionId && state.options.sessionId !== sessionId) return false;
47266
+ const {
47267
+ disableLocalStorage,
47268
+ useSessionStorage
47269
+ } = state.config.settings.embeddingConfiguration;
47270
+ try {
47271
+ const browserStorage = (0,_helper_storage__WEBPACK_IMPORTED_MODULE_1__/* .getStorage */ .c)({
47272
+ disableLocalStorage,
47273
+ useSessionStorage
47274
+ });
47275
+ if (!browserStorage) return false;
47276
+
47277
+ // `channel` is not part of the storage key; it is only in the type.
47278
+ const key = (0,_options__WEBPACK_IMPORTED_MODULE_0__/* .getOptionsKey */ .W)({
47279
+ userId: state.options.userId,
47280
+ sessionId,
47281
+ channel: state.options.channel
47282
+ }, state.config);
47283
+
47284
+ // Once the first connect has assigned a session id the answer is
47285
+ // spent: `computeNoticeSession` consults it only while its own
47286
+ // `prev.id` is still "". The key is final well before that (userId
47287
+ // is dispatched on mount, URLToken with the config, and the connect
47288
+ // waits for the config), so the cache is populated by then and every
47289
+ // later dispatch is served without touching storage.
47290
+ if (state.options.sessionId && lastRead?.key === key) return lastRead.result;
47291
+ const persistedString = browserStorage.getItem(key);
47292
+ if (lastRead && lastRead.key === key && lastRead.raw === persistedString) {
47293
+ return lastRead.result;
47294
+ }
47295
+ let result = false;
47296
+ if (persistedString) {
47297
+ try {
47298
+ const messages = JSON.parse(persistedString)?.messages;
47299
+ result = Array.isArray(messages) && messages.length > 0;
47300
+ } catch {
47301
+ result = false;
47302
+ }
47303
+ }
47304
+ lastRead = {
47305
+ key,
47306
+ raw: persistedString,
47307
+ result
47308
+ };
47309
+ return result;
47310
+ } catch {
47311
+ // Restricted embeddings can throw on merely touching storage. The
47312
+ // restore reads it the same way, so nothing would be restored there
47313
+ // either — "no persisted conversation" is the truthful answer.
47314
+ return false;
47315
+ }
47316
+ };
47317
+
47318
+ /***/ },
47319
+
46500
47320
  /***/ 92101
46501
47321
  (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
46502
47322
 
@@ -52417,7 +53237,7 @@ function deconstructPacket(packet) {
52417
53237
  buffers: buffers
52418
53238
  };
52419
53239
  }
52420
- function _deconstructPacket(data, buffers) {
53240
+ function _deconstructPacket(data, buffers, toJSON) {
52421
53241
  if (!data) return data;
52422
53242
  if ((0, is_binary_js_1.isBinary)(data)) {
52423
53243
  const placeholder = {
@@ -52433,6 +53253,9 @@ function _deconstructPacket(data, buffers) {
52433
53253
  }
52434
53254
  return newData;
52435
53255
  } else if (typeof data === "object" && !(data instanceof Date)) {
53256
+ if (data.toJSON && typeof data.toJSON === "function" && !toJSON) {
53257
+ return _deconstructPacket(data.toJSON(), buffers, true);
53258
+ }
52436
53259
  const newData = {};
52437
53260
  for (const key in data) {
52438
53261
  if (Object.prototype.hasOwnProperty.call(data, key)) {
@@ -52486,6 +53309,11 @@ function _reconstructPacket(data, buffers) {
52486
53309
 
52487
53310
 
52488
53311
 
53312
+ var __importDefault = this && this.__importDefault || function (mod) {
53313
+ return mod && mod.__esModule ? mod : {
53314
+ "default": mod
53315
+ };
53316
+ };
52489
53317
  Object.defineProperty(exports, "__esModule", ({
52490
53318
  value: true
52491
53319
  }));
@@ -52494,7 +53322,7 @@ exports.isPacketValid = isPacketValid;
52494
53322
  const component_emitter_1 = __webpack_require__(40368);
52495
53323
  const binary_js_1 = __webpack_require__(56156);
52496
53324
  const is_binary_js_1 = __webpack_require__(10003);
52497
- const debug_1 = __webpack_require__(30887); // debug()
53325
+ const debug_1 = __importDefault(__webpack_require__(30887)); // debug()
52498
53326
  const debug = (0, debug_1.default)("socket.io-parser"); // debug()
52499
53327
  /**
52500
53328
  * These strings must not be used as event names, as they have a special meaning.
@@ -52634,10 +53462,6 @@ class Decoder extends component_emitter_1.Emitter {
52634
53462
  packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;
52635
53463
  // binary packet's json
52636
53464
  this.reconstructor = new BinaryReconstructor(packet);
52637
- // no attachments, labeled binary but no binary data to follow
52638
- if (packet.attachments === 0) {
52639
- super.emitReserved("decoded", packet);
52640
- }
52641
53465
  } else {
52642
53466
  // non-binary full packet
52643
53467
  super.emitReserved("decoded", packet);
@@ -52682,7 +53506,7 @@ class Decoder extends component_emitter_1.Emitter {
52682
53506
  throw new Error("Illegal attachments");
52683
53507
  }
52684
53508
  const n = Number(buf);
52685
- if (!isInteger(n) || n < 0) {
53509
+ if (!isInteger(n) || n < 1) {
52686
53510
  throw new Error("Illegal attachments");
52687
53511
  } else if (n > this.opts.maxAttachments) {
52688
53512
  throw new Error("too many attachments");
@@ -56919,19 +57743,19 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
56919
57743
  ._mainButtonWrapper_zhv80_144 {
56920
57744
  padding: 16px;
56921
57745
  }
56922
- ._slideItem_ldua9_1 {
57746
+ ._slideItem_fa7bu_1 {
56923
57747
  position: relative;
56924
57748
  width: 206px;
56925
57749
  /* height: 150px; */
56926
57750
  overflow: hidden;
56927
57751
  }
56928
57752
 
56929
- ._slideItem_ldua9_1 ._top_ldua9_8 {
57753
+ ._slideItem_fa7bu_1 ._top_fa7bu_8 {
56930
57754
  position: relative;
56931
57755
  border-radius: var(--cc-bubble-border-radius);
56932
57756
  }
56933
57757
 
56934
- ._slideItem_ldua9_1 ._bottom_ldua9_13 {
57758
+ ._slideItem_fa7bu_1 ._bottom_fa7bu_13 {
56935
57759
  border-bottom-left-radius: var(--cc-bubble-border-radius);
56936
57760
  border-bottom-right-radius: var(--cc-bubble-border-radius);
56937
57761
  border: 1px solid var(--cc-black-80);
@@ -56942,7 +57766,7 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
56942
57766
  padding: 8px;
56943
57767
  }
56944
57768
 
56945
- ._slideItem_ldua9_1 ._top_ldua9_8 img {
57769
+ ._slideItem_fa7bu_1 ._top_fa7bu_8 img {
56946
57770
  aspect-ratio: 206/150;
56947
57771
  object-fit: cover;
56948
57772
  object-position: left;
@@ -56952,7 +57776,7 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
56952
57776
  outline: none;
56953
57777
  }
56954
57778
 
56955
- ._slideItem_ldua9_1 ._brokenImage_ldua9_34 {
57779
+ ._slideItem_fa7bu_1 ._brokenImage_fa7bu_34 {
56956
57780
  aspect-ratio: 206/150;
56957
57781
  width: 100%;
56958
57782
  display: block;
@@ -56961,9 +57785,9 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
56961
57785
  background-color: var(--cc-black-80);
56962
57786
  }
56963
57787
 
56964
- ._slideItem_ldua9_1 ._hasExtraInfo_ldua9_43,
56965
- ._slideItem_ldua9_1 ._hasExtraInfo_ldua9_43 img,
56966
- ._slideItem_ldua9_1 ._brokenImage_ldua9_34 {
57788
+ ._slideItem_fa7bu_1 ._hasExtraInfo_fa7bu_43,
57789
+ ._slideItem_fa7bu_1 ._hasExtraInfo_fa7bu_43 img,
57790
+ ._slideItem_fa7bu_1 ._brokenImage_fa7bu_34 {
56967
57791
  border-bottom-left-radius: 0px;
56968
57792
  border-bottom-right-radius: 0px;
56969
57793
  }
@@ -56972,7 +57796,7 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
56972
57796
  ** Default (legacy) layout: the title overlays the bottom of the image.
56973
57797
  ** Used when \`galleryCardTitleBelowImage\` is not enabled.
56974
57798
  */
56975
- ._slideItem_ldua9_1 ._top_ldua9_8 h4 {
57799
+ ._slideItem_fa7bu_1 ._top_fa7bu_8 h4 {
56976
57800
  position: absolute;
56977
57801
  margin: 0px;
56978
57802
  margin-inline-start: 8px;
@@ -56980,32 +57804,69 @@ article ._wrapper_zhv80_1 ._list_zhv80_8 ._listItemRoot_zhv80_8 {
56980
57804
  color: var(--cc-white);
56981
57805
  }
56982
57806
 
57807
+ /*
57808
+ ** A card whose image is missing or failed to load shows the grey
57809
+ ** \`.brokenImage\` placeholder instead. White text on it is 1.6:1, so the
57810
+ ** overlaid title switches to dark text there (WCAG 1.4.3 Contrast Minimum,
57811
+ ** CGY-37634).
57812
+ */
57813
+ ._slideItem_fa7bu_1 ._top_fa7bu_8:has(._brokenImage_fa7bu_34) h4 {
57814
+ color: var(--cc-black-10);
57815
+ }
57816
+
56983
57817
  /*
56984
57818
  ** Opt-in layout (galleryCardTitleBelowImage): the title moves into the content
56985
57819
  ** block beneath the image, so it shares the subtitle's flush margins/padding.
56986
57820
  */
56987
- ._slideItem_ldua9_1 ._bottom_ldua9_13 p,
56988
- ._slideItem_ldua9_1 ._bottom_ldua9_13 h4 {
57821
+ ._slideItem_fa7bu_1 ._bottom_fa7bu_13 p,
57822
+ ._slideItem_fa7bu_1 ._bottom_fa7bu_13 h4 {
56989
57823
  padding: 0px;
56990
57824
  margin: 0px;
56991
57825
  }
56992
57826
 
56993
- ._slideItem_ldua9_1 ._bottom_ldua9_13 ._buttonListItem_ldua9_72 {
57827
+ ._slideItem_fa7bu_1 ._bottom_fa7bu_13 ._buttonListItem_fa7bu_82 {
56994
57828
  width: 100%;
56995
57829
  }
56996
57830
 
57831
+ /*
57832
+ ** default_action link (CGY-37634): wraps the card text inside the content
57833
+ ** block (keeping the block's column gap), or sits on the image area when the
57834
+ ** block has no text. Buttons are never inside it.
57835
+ */
57836
+ ._slideItem_fa7bu_1 ._link_fa7bu_91 {
57837
+ display: flex;
57838
+ flex-direction: column;
57839
+ gap: 8px;
57840
+ }
57841
+ ._slideItem_fa7bu_1 [role="link"] {
57842
+ cursor: pointer;
57843
+ }
57844
+ ._slideItem_fa7bu_1 [role="link"]:focus-visible {
57845
+ outline: 2px solid var(--cc-primary-color-focus);
57846
+ outline-offset: 2px;
57847
+ }
57848
+ /* .slideItem is overflow:hidden and the image area fills it edge to edge, so
57849
+ the focus ring is drawn inside the card; an outset ring there is clipped
57850
+ away entirely (WCAG 2.4.7 Focus Visible). The text link sits inside the
57851
+ content block's 8px padding, so its outset ring is never clipped. */
57852
+ ._slideItem_fa7bu_1 ._top_fa7bu_8[role="link"]:focus-visible {
57853
+ outline-offset: -2px;
57854
+ }
57855
+
56997
57856
  /*
56998
57857
  ** SWIPER MAIN
56999
57858
  ** The following styles are a porting from the original swiper/css and related modules.
57000
57859
  ** The idea is to integrate with modules CSS in order to increase the specificity
57001
57860
  ** and avoid conflicts on consumer apps
57002
57861
  */
57003
- article .swiper._wrapper_ldua9_82 {
57862
+ article .swiper._wrapper_fa7bu_117 {
57004
57863
  margin-left: -20px;
57005
57864
  margin-right: -20px;
57006
57865
  padding-left: 20px;
57007
57866
  padding-right: 20px;
57008
- padding-bottom: 22px;
57867
+ /* The pagination row is in normal flow below the slides (see
57868
+ .swiper-pagination); 2px clearance under it, matching the gap above it. */
57869
+ padding-bottom: 2px;
57009
57870
  padding-top: 0px;
57010
57871
  position: relative;
57011
57872
  overflow: hidden;
@@ -57014,7 +57875,7 @@ article .swiper._wrapper_ldua9_82 {
57014
57875
  z-index: 1;
57015
57876
  display: block;
57016
57877
  }
57017
- article .swiper._wrapper_ldua9_82 .swiper-wrapper {
57878
+ article .swiper._wrapper_fa7bu_117 .swiper-wrapper {
57018
57879
  position: relative;
57019
57880
  width: 100%;
57020
57881
  /* height: 100%; */
@@ -57024,15 +57885,15 @@ article .swiper._wrapper_ldua9_82 .swiper-wrapper {
57024
57885
  transition-timing-function: initial;
57025
57886
  box-sizing: content-box;
57026
57887
  }
57027
- article .swiper._wrapper_ldua9_82 .swiper-android .swiper-slide,
57028
- article .swiper._wrapper_ldua9_82 .swiper-ios .swiper-slide,
57029
- article .swiper._wrapper_ldua9_82 .swiper-wrapper {
57888
+ article .swiper._wrapper_fa7bu_117 .swiper-android .swiper-slide,
57889
+ article .swiper._wrapper_fa7bu_117 .swiper-ios .swiper-slide,
57890
+ article .swiper._wrapper_fa7bu_117 .swiper-wrapper {
57030
57891
  transform: translate3d(0px, 0, 0);
57031
57892
  }
57032
- article .swiper._wrapper_ldua9_82 .swiper-horizontal {
57893
+ article .swiper._wrapper_fa7bu_117 .swiper-horizontal {
57033
57894
  touch-action: pan-y;
57034
57895
  }
57035
- article .swiper._wrapper_ldua9_82 .swiper-slide {
57896
+ article .swiper._wrapper_fa7bu_117 .swiper-slide {
57036
57897
  flex-shrink: 0;
57037
57898
  width: 100%;
57038
57899
  height: 100%;
@@ -57040,15 +57901,15 @@ article .swiper._wrapper_ldua9_82 .swiper-slide {
57040
57901
  transition-property: transform;
57041
57902
  display: block;
57042
57903
  }
57043
- article .swiper._wrapper_ldua9_82 .swiper-slide-invisible-blank {
57904
+ article .swiper._wrapper_fa7bu_117 .swiper-slide-invisible-blank {
57044
57905
  visibility: hidden;
57045
57906
  }
57046
57907
 
57047
57908
  /*
57048
57909
  ** SWIPER PAGINATION
57049
57910
  */
57050
- article .swiper._wrapper_ldua9_82 .gallery-button-prev,
57051
- article .swiper._wrapper_ldua9_82 .gallery-button-next {
57911
+ article .swiper._wrapper_fa7bu_117 .gallery-button-prev,
57912
+ article .swiper._wrapper_fa7bu_117 .gallery-button-next {
57052
57913
  position: absolute;
57053
57914
  top: calc(150px / 2 - 8px);
57054
57915
  z-index: 10;
@@ -57061,68 +57922,104 @@ article .swiper._wrapper_ldua9_82 .gallery-button-next {
57061
57922
  display: flex;
57062
57923
  align-items: center;
57063
57924
  }
57064
- article .swiper._wrapper_ldua9_82 .gallery-button-prev {
57925
+ article .swiper._wrapper_fa7bu_117 .gallery-button-prev {
57065
57926
  left: 20px;
57066
57927
  }
57067
- article .swiper._wrapper_ldua9_82 .gallery-button-next {
57928
+ article .swiper._wrapper_fa7bu_117 .gallery-button-next {
57068
57929
  right: 20px;
57069
57930
  transform: rotate(180deg);
57070
57931
  }
57071
- article .swiper._wrapper_ldua9_82 .gallery-button-prev:dir(rtl) {
57932
+ article .swiper._wrapper_fa7bu_117 .gallery-button-prev:dir(rtl) {
57072
57933
  left: unset;
57073
57934
  right: 20px;
57074
57935
  transform: rotate(180deg);
57075
57936
  }
57076
- article .swiper._wrapper_ldua9_82 .gallery-button-next:dir(rtl) {
57937
+ article .swiper._wrapper_fa7bu_117 .gallery-button-next:dir(rtl) {
57077
57938
  right: unset;
57078
57939
  left: 20px;
57079
57940
  transform: rotate(0deg);
57080
57941
  }
57081
- article .swiper._wrapper_ldua9_82 .swiper-button-disabled {
57942
+ article .swiper._wrapper_fa7bu_117 .swiper-button-disabled {
57082
57943
  opacity: 0;
57083
57944
  }
57084
57945
 
57085
57946
  /*
57086
57947
  ** SWIPER NAVIGATION
57087
57948
  */
57088
- article .swiper._wrapper_ldua9_82 .swiper-pagination {
57089
- position: absolute;
57949
+ /*
57950
+ ** The pagination row is in normal flow beneath the slides. Bullets are fixed
57951
+ ** 24px targets (WCAG 2.5.8) and wrap onto further rows when more than fit in
57952
+ ** one; in flow, extra rows grow the gallery downwards rather than overlapping
57953
+ ** the cards. A single row adds 28px: 2px gap + 24px row + 2px container
57954
+ ** padding.
57955
+ */
57956
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination {
57957
+ position: relative;
57090
57958
  text-align: center;
57091
57959
  transition: 300ms opacity;
57092
57960
  transform: translate3d(0, 0, 0);
57093
57961
  z-index: 10;
57094
57962
  }
57095
- article .swiper._wrapper_ldua9_82 .swiper-pagination.swiper-pagination-hidden {
57963
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination.swiper-pagination-hidden {
57096
57964
  opacity: 0;
57097
57965
  }
57098
- article .swiper._wrapper_ldua9_82 .swiper-pagination-disabled > .swiper-pagination,
57099
- article .swiper._wrapper_ldua9_82 .swiper-pagination.swiper-pagination-disabled {
57966
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-disabled > .swiper-pagination,
57967
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination.swiper-pagination-disabled {
57100
57968
  display: none !important;
57101
57969
  }
57102
- article .swiper._wrapper_ldua9_82 .swiper-horizontal > .swiper-pagination-bullets,
57103
- article .swiper._wrapper_ldua9_82 .swiper-pagination-bullets.swiper-pagination-horizontal {
57104
- bottom: 4px;
57105
- left: 0;
57970
+ article .swiper._wrapper_fa7bu_117 .swiper-horizontal > .swiper-pagination-bullets,
57971
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullets.swiper-pagination-horizontal {
57972
+ margin-top: 2px;
57106
57973
  width: 100%;
57974
+ display: flex;
57975
+ flex-wrap: wrap;
57976
+ justify-content: center;
57977
+ align-items: center;
57107
57978
  }
57108
- article .swiper._wrapper_ldua9_82 .swiper-pagination-bullet {
57979
+ /*
57980
+ ** Each bullet is a 24×24px pointer/focus target (WCAG 2.5.8 Target Size
57981
+ ** Minimum, CGY-37634). The visible 6px dot is drawn by ::before, so the look
57982
+ ** is unchanged while the hit area grows around it; focus styling therefore
57983
+ ** targets ::before too.
57984
+ */
57985
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullet {
57986
+ /* Fixed-size flex item: the 24px target never shrinks when the row is
57987
+ full; overflowing bullets wrap (see .swiper-pagination). */
57988
+ flex: 0 0 24px;
57989
+ width: 24px;
57990
+ height: 24px;
57991
+ display: inline-flex;
57992
+ align-items: center;
57993
+ justify-content: center;
57994
+ /* No border-radius here: browsers clip pointer hit-testing to the rounded
57995
+ shape, which would shrink the target back below 24×24. */
57996
+ background: transparent;
57997
+ opacity: 1;
57998
+ }
57999
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullet::before {
58000
+ content: "";
57109
58001
  width: 6px;
57110
58002
  height: 6px;
57111
- display: inline-block;
57112
58003
  border-radius: 50%;
57113
- background: var(--cc-black-50);
57114
- opacity: 1;
58004
+ background-color: var(--cc-black-50);
58005
+ }
58006
+ /* Declared before the focus rules (and lower in specificity) so a focused
58007
+ active bullet always shows the focus colour, not the active black. */
58008
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullet-active::before {
58009
+ background-color: black;
57115
58010
  }
57116
- article .swiper._wrapper_ldua9_82 .swiper-pagination-bullet:focus {
58011
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullet:focus {
58012
+ outline: none;
58013
+ }
58014
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullet:focus::before {
57117
58015
  background-color: var(--cc-primary-color-focus);
57118
58016
  box-shadow: 0 0 0 4px var(--cc-primary-color-opacity-10);
57119
- outline: none;
57120
58017
  }
57121
- article .swiper._wrapper_ldua9_82 .swiper-pagination-bullet:focus-visible {
58018
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullet:focus-visible::before {
57122
58019
  outline: 2px solid var(--cc-primary-color-focus);
57123
58020
  outline-offset: 2px;
57124
58021
  }
57125
- article .swiper._wrapper_ldua9_82 button.swiper-pagination-bullet {
58022
+ article .swiper._wrapper_fa7bu_117 button.swiper-pagination-bullet {
57126
58023
  border: none;
57127
58024
  margin: 0;
57128
58025
  padding: 0;
@@ -57130,23 +58027,20 @@ article .swiper._wrapper_ldua9_82 button.swiper-pagination-bullet {
57130
58027
  -webkit-appearance: none;
57131
58028
  appearance: none;
57132
58029
  }
57133
- article .swiper._wrapper_ldua9_82 .swiper-pagination-clickable .swiper-pagination-bullet {
58030
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-clickable .swiper-pagination-bullet {
57134
58031
  cursor: pointer;
57135
58032
  }
57136
- article .swiper._wrapper_ldua9_82 .swiper-pagination-bullet:only-child {
58033
+ article .swiper._wrapper_fa7bu_117 .swiper-pagination-bullet:only-child {
57137
58034
  display: none !important;
57138
58035
  }
57139
- article .swiper._wrapper_ldua9_82 .swiper-pagination-bullet-active {
57140
- opacity: 1;
57141
- background-color: black;
57142
- }
57143
58036
  article
57144
- .swiper._wrapper_ldua9_82
58037
+ .swiper._wrapper_fa7bu_117
57145
58038
  .swiper-horizontal > .swiper-pagination-bullets .swiper-pagination-bullet,
57146
58039
  article
57147
- .swiper._wrapper_ldua9_82
58040
+ .swiper._wrapper_fa7bu_117
57148
58041
  .swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet {
57149
- margin: 0 4px;
58042
+ /* Adjacent 24px targets touch but never overlap (WCAG 2.5.8). */
58043
+ margin: 0;
57150
58044
  }
57151
58045
  ._wrapper_1h4l4_1 {
57152
58046
  background-color: white;
@@ -59322,6 +60216,14 @@ const replaceUrlsWithHTMLanchorElem = text2 => {
59322
60216
  });
59323
60217
  return enhancedText;
59324
60218
  };
60219
+ const htmlToPlainText = html2 => {
60220
+ if (!html2) return "";
60221
+ if (typeof DOMParser !== "undefined") {
60222
+ const doc = new DOMParser().parseFromString(html2, "text/html");
60223
+ return (doc.body?.textContent ?? "").trim();
60224
+ }
60225
+ return html2.replace(/<[^>]*>/g, "").trim();
60226
+ };
59325
60227
  const getFocusableElements = element2 => {
59326
60228
  const interactiveEls = element2?.querySelectorAll('a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])');
59327
60229
  const interactiveElsArray = interactiveEls && Array.from(interactiveEls);
@@ -59886,7 +60788,7 @@ function createDOMPurify() {
59886
60788
  NodeFilter: NodeFilter2,
59887
60789
  NamedNodeMap = window2.NamedNodeMap || window2.MozNamedAttrMap,
59888
60790
  HTMLFormElement,
59889
- DOMParser,
60791
+ DOMParser: DOMParser2,
59890
60792
  trustedTypes: trustedTypes2
59891
60793
  } = window2;
59892
60794
  const ElementPrototype = Element2.prototype;
@@ -60248,7 +61150,7 @@ function createDOMPurify() {
60248
61150
  const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
60249
61151
  if (NAMESPACE === HTML_NAMESPACE) {
60250
61152
  try {
60251
- doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
61153
+ doc = new DOMParser2().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
60252
61154
  } catch (_) {}
60253
61155
  }
60254
61156
  if (!doc || !doc.documentElement) {
@@ -68977,7 +69879,7 @@ function linkReference$1(state, node2) {
68977
69879
  state.patch(node2, result);
68978
69880
  return state.applyData(node2, result);
68979
69881
  }
68980
- function link$1(state, node2) {
69882
+ function link$2(state, node2) {
68981
69883
  const properties = {
68982
69884
  href: normalizeUri(node2.url)
68983
69885
  };
@@ -69280,7 +70182,7 @@ const handlers = {
69280
70182
  image: image$1,
69281
70183
  inlineCode: inlineCode$1,
69282
70184
  linkReference: linkReference$1,
69283
- link: link$1,
70185
+ link: link$2,
69284
70186
  listItem: listItem$1,
69285
70187
  list: list$3,
69286
70188
  paragraph: paragraph$2,
@@ -82801,8 +83703,8 @@ function formatLinkAsAutolink(node2, state) {
82801
83703
  // references don’t work), space, or angle brackets…
82802
83704
  !/[\0- <>\u007F]/.test(node2.url));
82803
83705
  }
82804
- link.peek = linkPeek;
82805
- function link(node2, _, state, info) {
83706
+ link$1.peek = linkPeek;
83707
+ function link$1(node2, _, state, info) {
82806
83708
  const quote = checkQuote(state);
82807
83709
  const suffix = quote === '"' ? "Quote" : "Apostrophe";
82808
83710
  const tracker = state.createTracker(info);
@@ -83108,7 +84010,7 @@ const handle = {
83108
84010
  image,
83109
84011
  imageReference,
83110
84012
  inlineCode,
83111
- link,
84013
+ link: link$1,
83112
84014
  linkReference,
83113
84015
  list: list$2,
83114
84016
  listItem,
@@ -84975,25 +85877,15 @@ const LightboxHeader = () => {
84975
85877
  const {
84976
85878
  config: config2
84977
85879
  } = useMessageContext();
84978
- const firstButton = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
85880
+ const downloadButton2 = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
84979
85881
  (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(() => {
84980
85882
  setTimeout(() => {
84981
- firstButton.current?.focus();
85883
+ downloadButton2.current?.focus();
84982
85884
  }, 100);
84983
85885
  }, []);
84984
85886
  const handleDownload = () => {
84985
85887
  window.open(url, "_blank");
84986
85888
  };
84987
- const handleKeyDownload = event => {
84988
- event.key === "Enter" && handleDownload();
84989
- };
84990
- const handleKeyClose = event => {
84991
- if (event.key === "Tab" || event.shiftKey) {
84992
- firstButton.current?.focus();
84993
- event.preventDefault();
84994
- }
84995
- event.code === "Enter" && onClose && onClose();
84996
- };
84997
85889
  const downloadFullsizeImageLabel = config2?.settings.customTranslations?.ariaLabels?.downloadFullsizeImage || "Download full-size image";
84998
85890
  const closeFullsizeImageModalLabel = config2?.settings.customTranslations?.ariaLabels?.closeFullsizeImageModal || "Close full-size image viewer";
84999
85891
  return /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
@@ -85004,15 +85896,13 @@ const LightboxHeader = () => {
85004
85896
  }), /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
85005
85897
  className: classes$h.iconsGroup,
85006
85898
  children: [/* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("button", {
85007
- ref: firstButton,
85899
+ ref: downloadButton2,
85008
85900
  onClick: handleDownload,
85009
- onKeyDown: handleKeyDownload,
85010
85901
  "aria-label": downloadFullsizeImageLabel,
85011
85902
  className: classes$h.icon,
85012
85903
  children: /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(SvgDownload, {})
85013
85904
  }), /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("button", {
85014
85905
  onClick: onClose,
85015
- onKeyDown: handleKeyClose,
85016
85906
  "aria-label": closeFullsizeImageModalLabel,
85017
85907
  className: classes$h.icon,
85018
85908
  children: /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(SvgClose, {})
@@ -85029,12 +85919,34 @@ const Lightbox = () => {
85029
85919
  const {
85030
85920
  config: config2
85031
85921
  } = useMessageContext();
85922
+ const dialogRef = (0,react__WEBPACK_IMPORTED_MODULE_1__.useRef)(null);
85032
85923
  (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(() => {
85033
- const close = event => {
85034
- event.code === "Escape" && onClose && onClose();
85924
+ const handleKeyDown = event => {
85925
+ if (event.key === "Escape" || event.code === "Escape") {
85926
+ onClose && onClose();
85927
+ return;
85928
+ }
85929
+ if (event.key !== "Tab" || !dialogRef.current) return;
85930
+ const {
85931
+ firstFocusable,
85932
+ lastFocusable
85933
+ } = getFocusableElements(dialogRef.current);
85934
+ if (!firstFocusable || !lastFocusable) return;
85935
+ const active = document.activeElement;
85936
+ const focusInsideDialog = !!active && dialogRef.current.contains(active);
85937
+ if (!focusInsideDialog) {
85938
+ event.preventDefault();
85939
+ (event.shiftKey ? lastFocusable : firstFocusable).focus();
85940
+ } else if (event.shiftKey && active === firstFocusable) {
85941
+ event.preventDefault();
85942
+ lastFocusable.focus();
85943
+ } else if (!event.shiftKey && active === lastFocusable) {
85944
+ event.preventDefault();
85945
+ firstFocusable.focus();
85946
+ }
85035
85947
  };
85036
- window.addEventListener("keydown", close);
85037
- return () => window.removeEventListener("keydown", close);
85948
+ window.addEventListener("keydown", handleKeyDown);
85949
+ return () => window.removeEventListener("keydown", handleKeyDown);
85038
85950
  }, [onClose]);
85039
85951
  const handleOnClickBackdrop = event => {
85040
85952
  event.preventDefault();
@@ -85049,7 +85961,9 @@ const Lightbox = () => {
85049
85961
  };
85050
85962
  const lightboxLabel = config2?.settings.customTranslations?.ariaLabels?.fullSizeImageViewerTitle || "Full-size image viewer";
85051
85963
  return /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
85964
+ ref: dialogRef,
85052
85965
  role: "dialog",
85966
+ "aria-modal": "true",
85053
85967
  "aria-label": lightboxLabel,
85054
85968
  className: classes$h.wrapper,
85055
85969
  children: [/* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {
@@ -85058,7 +85972,7 @@ const Lightbox = () => {
85058
85972
  children: /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", {
85059
85973
  className: classes$h.fullImage,
85060
85974
  "data-test": "image-lightbox",
85061
- alt: altText,
85975
+ alt: altText || "",
85062
85976
  src: url,
85063
85977
  onClick: handleOnClickImage,
85064
85978
  onTouchMove: handleOnSwipeImage
@@ -92668,13 +93582,14 @@ const List = () => {
92668
93582
  })]
92669
93583
  });
92670
93584
  };
92671
- const slideItem = "_slideItem_ldua9_1";
92672
- const top = "_top_ldua9_8";
92673
- const bottom = "_bottom_ldua9_13";
92674
- const brokenImage = "_brokenImage_ldua9_34";
92675
- const hasExtraInfo = "_hasExtraInfo_ldua9_43";
92676
- const buttonListItem = "_buttonListItem_ldua9_72";
92677
- const wrapper$1 = "_wrapper_ldua9_82";
93585
+ const slideItem = "_slideItem_fa7bu_1";
93586
+ const top = "_top_fa7bu_8";
93587
+ const bottom = "_bottom_fa7bu_13";
93588
+ const brokenImage = "_brokenImage_fa7bu_34";
93589
+ const hasExtraInfo = "_hasExtraInfo_fa7bu_43";
93590
+ const buttonListItem = "_buttonListItem_fa7bu_82";
93591
+ const link = "_link_fa7bu_91";
93592
+ const wrapper$1 = "_wrapper_fa7bu_117";
92678
93593
  const classes$8 = {
92679
93594
  slideItem,
92680
93595
  top,
@@ -92682,6 +93597,7 @@ const classes$8 = {
92682
93597
  brokenImage,
92683
93598
  hasExtraInfo,
92684
93599
  buttonListItem,
93600
+ link,
92685
93601
  wrapper: wrapper$1
92686
93602
  };
92687
93603
  const getGalleryContent = (elements, isSanitizeEnabled, customAllowedHtmlTags) => {
@@ -98448,6 +99364,18 @@ const SwiperSlide = /* @__PURE__ */(0,react__WEBPACK_IMPORTED_MODULE_1__.forward
98448
99364
  })));
98449
99365
  });
98450
99366
  SwiperSlide.displayName = "SwiperSlide";
99367
+ const getHostname = url => {
99368
+ try {
99369
+ const {
99370
+ protocol,
99371
+ hostname
99372
+ } = new URL(url);
99373
+ return protocol === "http:" || protocol === "https:" ? hostname : "";
99374
+ } catch {
99375
+ return "";
99376
+ }
99377
+ };
99378
+ const NESTED_CONTROL_SELECTOR = "a[href], button, input, select, textarea, summary";
98451
99379
  const GalleryItem = props2 => {
98452
99380
  const {
98453
99381
  slide: slide2,
@@ -98468,7 +99396,6 @@ const GalleryItem = props2 => {
98468
99396
  messageParams,
98469
99397
  "data-message-id": dataMessageId
98470
99398
  } = useMessageContext();
98471
- const hasExtraInfo2 = subtitle || buttons2 && buttons2?.length > 0;
98472
99399
  const titleBelowImage = !!config2?.settings?.layout?.galleryCardTitleBelowImage;
98473
99400
  const [isImageBroken, setImageBroken] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)(false);
98474
99401
  const {
@@ -98476,22 +99403,25 @@ const GalleryItem = props2 => {
98476
99403
  } = useSanitize();
98477
99404
  const titleHtml = processHTML(title || "");
98478
99405
  const subtitleHtml = processHTML(subtitle || "");
98479
- const hasTitle = !!titleHtml;
99406
+ const hasTitle = !!titleHtml.trim();
99407
+ const hasSubtitle = !!subtitleHtml.trim();
99408
+ const hasButtons = !!buttons2 && buttons2.length > 0;
99409
+ const hasExtraInfo2 = hasSubtitle || hasButtons;
98480
99410
  const showContentBlock = titleBelowImage && hasTitle || hasExtraInfo2;
98481
99411
  const titleId = useRandomId("webchatCarouselTemplateTitle");
98482
99412
  const subtitleId = useRandomId("webchatCarouselTemplateSubtitle");
98483
99413
  const shouldBeDisabled = messageParams?.isConversationEnded;
98484
99414
  const opensInNewTab = config2?.settings.customTranslations?.ariaLabels?.opensInNewTab ?? "Opens in new tab";
98485
- const handleClick = () => {
98486
- if (!default_action?.url) return;
98487
- const url = config2?.settings?.layout?.disableUrlButtonSanitization ? default_action.url : distExports.sanitizeUrl(default_action.url);
98488
- if (url === "about:blank") return;
98489
- window.open(url);
98490
- return;
99415
+ const linkUrl = default_action?.url ? config2?.settings?.layout?.disableUrlButtonSanitization ? default_action.url : distExports.sanitizeUrl(default_action.url) : "";
99416
+ const handleClick = event => {
99417
+ const target = event.target;
99418
+ if (target !== event.currentTarget && target.closest(NESTED_CONTROL_SELECTOR)) return;
99419
+ if (!linkUrl || linkUrl === "about:blank") return;
99420
+ window.open(linkUrl);
98491
99421
  };
98492
99422
  const handleKeyDown = event => {
98493
99423
  if (default_action && event.key === "Enter") {
98494
- handleClick();
99424
+ handleClick(event);
98495
99425
  }
98496
99426
  };
98497
99427
  const titleElement = /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(Typography, {
@@ -98503,35 +99433,54 @@ const GalleryItem = props2 => {
98503
99433
  className: "webchat-carousel-template-title",
98504
99434
  id: titleId
98505
99435
  });
99436
+ const subtitleElement = hasSubtitle && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(Typography, {
99437
+ variant: "body-regular",
99438
+ dangerouslySetInnerHTML: {
99439
+ __html: subtitleHtml
99440
+ },
99441
+ id: subtitleId,
99442
+ className: "webchat-carousel-template-subtitle"
99443
+ });
99444
+ const hasBlockText = hasSubtitle || titleBelowImage && hasTitle;
99445
+ const linkTarget = linkUrl && linkUrl !== "about:blank" ? hasBlockText ? "text" : "top" : null;
99446
+ const titleText = linkTarget ? htmlToPlainText(titleHtml) : "";
99447
+ const subtitleText = linkTarget ? htmlToPlainText(subtitleHtml) : "";
99448
+ const linkName = titleText || subtitleText || image_alt_text || getHostname(linkUrl);
99449
+ const linkLabel = [linkName, opensInNewTab].filter(Boolean).join(". ");
99450
+ const linkDescribedBy = hasSubtitle && linkName !== subtitleText ? subtitleId : void 0;
99451
+ const cardLink = (className, children) => /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {
99452
+ className,
99453
+ role: "link",
99454
+ tabIndex: 0,
99455
+ onClick: handleClick,
99456
+ onKeyDown: handleKeyDown,
99457
+ "aria-label": linkLabel,
99458
+ "aria-describedby": linkDescribedBy,
99459
+ children
99460
+ });
99461
+ const topClassName = classnames$1(classes$8.top, showContentBlock && classes$8.hasExtraInfo);
99462
+ const imageArea = /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment, {
99463
+ children: [!titleBelowImage && hasTitle && titleElement, isImageBroken ? /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
99464
+ className: classes$8.brokenImage
99465
+ }) : /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", {
99466
+ src: image_url,
99467
+ alt: image_alt_text || "",
99468
+ className: classes$8.slideImage,
99469
+ onError: () => setImageBroken(true)
99470
+ })]
99471
+ });
99472
+ const blockText = /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment, {
99473
+ children: [titleBelowImage && hasTitle && titleElement, subtitleElement]
99474
+ });
98506
99475
  return /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
98507
99476
  className: classnames$1("webchat-carousel-template-frame", classes$8.slideItem),
98508
- children: [/* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
98509
- className: classnames$1(classes$8.top, showContentBlock && classes$8.hasExtraInfo),
98510
- children: [!titleBelowImage && hasTitle && titleElement, isImageBroken ? /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("span", {
98511
- className: classes$8.brokenImage
98512
- }) : /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", {
98513
- src: image_url,
98514
- alt: image_alt_text || "",
98515
- className: classes$8.slideImage,
98516
- onError: () => setImageBroken(true)
98517
- })]
99477
+ children: [linkTarget === "top" ? cardLink(topClassName, imageArea) : /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("div", {
99478
+ className: topClassName,
99479
+ children: imageArea
98518
99480
  }), showContentBlock && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
98519
99481
  className: classnames$1("webchat-carousel-template-content", classes$8.bottom),
98520
- onClick: handleClick,
98521
- onKeyDown: handleKeyDown,
98522
- role: default_action?.url ? "link" : void 0,
98523
99482
  id: contentId,
98524
- "aria-describedby": default_action?.url && subtitle ? subtitleId : void 0,
98525
- "aria-labelledby": default_action?.url && hasTitle ? titleId : void 0,
98526
- "aria-label": default_action?.url ? `${titleHtml}. ${opensInNewTab}` : void 0,
98527
- children: [titleBelowImage && hasTitle && titleElement, subtitle && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(Typography, {
98528
- variant: "body-regular",
98529
- dangerouslySetInnerHTML: {
98530
- __html: subtitleHtml
98531
- },
98532
- id: subtitleId,
98533
- className: "webchat-carousel-template-subtitle"
98534
- }), buttons2 && buttons2?.length > 0 && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(ActionButtons, {
99483
+ children: [linkTarget === "text" ? cardLink(classnames$1("webchat-carousel-template-link", classes$8.link), blockText) : blockText, hasButtons && /* @__PURE__ */(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(ActionButtons, {
98535
99484
  dataMessageId,
98536
99485
  buttonClassName: classnames$1(buttonClasses.primaryButton, buttonClasses.actionButton, "webchat-carousel-template-button"),
98537
99486
  buttonListItemClassName: classes$8.buttonListItem,
@@ -98563,22 +99512,24 @@ const Gallery = () => {
98563
99512
  const carouselContentId = useRandomId("webchatCarouselContentButton");
98564
99513
  (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(() => {
98565
99514
  const chatHistory = document.getElementById("webchatChatHistoryWrapperLiveLogPanel");
98566
- const firstCardContent = document.getElementById(`${carouselContentId}-0`);
98567
- const firstButton = firstCardContent?.getElementsByTagName("button")?.[0];
99515
+ const messageRoot = dataMessageId ? document.querySelector(`[data-message-id="${dataMessageId.replace(/["\\]/g, "\\$&")}"]`) : null;
99516
+ const firstCard = messageRoot?.querySelector(".webchat-carousel-template-frame") ?? document.getElementById(`${carouselContentId}-0`)?.closest(".webchat-carousel-template-frame");
99517
+ const firstLink = firstCard?.querySelector('[role="link"]');
99518
+ const firstButton = firstCard?.getElementsByTagName("button")?.[0];
98568
99519
  if (!config2?.settings?.widgetSettings?.enableAutoFocus) return;
98569
99520
  if (!chatHistory?.contains(document.activeElement)) return;
98570
- if (firstCardContent?.getAttribute("role") === "link") {
99521
+ if (firstLink) {
98571
99522
  setTimeout(() => {
98572
- firstCardContent?.focus();
99523
+ firstLink.focus();
98573
99524
  }, 200);
98574
99525
  } else if (firstButton) {
98575
99526
  setTimeout(() => {
98576
99527
  firstButton?.focus();
98577
99528
  }, 200);
98578
99529
  }
98579
- }, [carouselContentId, config2?.settings?.widgetSettings?.enableAutoFocus]);
99530
+ }, [carouselContentId, dataMessageId, config2?.settings?.widgetSettings?.enableAutoFocus]);
98580
99531
  (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(() => {
98581
- const galleryMessage = document.querySelector(`[data-message-id="${dataMessageId}"]`);
99532
+ const galleryMessage = dataMessageId ? document.querySelector(`[data-message-id="${dataMessageId.replace(/["\\]/g, "\\$&")}"]`) : null;
98582
99533
  const swiperWrapper = galleryMessage?.querySelector(".swiper-wrapper");
98583
99534
  if (swiperWrapper) {
98584
99535
  swiperWrapper.removeAttribute("aria-live");
@@ -117355,12 +118306,23 @@ function requireL10n() {
117355
118306
  }
117356
118307
  var l10nExports = requireL10n();
117357
118308
  const l10n = /* @__PURE__ */getDefaultExportFromCjs(l10nExports);
118309
+ let instanceSeq = 0;
118310
+ function visuallyHide(element2) {
118311
+ element2.style.position = "absolute";
118312
+ element2.style.width = "1px";
118313
+ element2.style.height = "1px";
118314
+ element2.style.overflow = "hidden";
118315
+ element2.style.clip = "rect(0, 0, 0, 0)";
118316
+ element2.style.whiteSpace = "nowrap";
118317
+ }
117358
118318
  function customElements(pluginConfig) {
117359
118319
  return function (fp) {
117360
118320
  const {
117361
118321
  arrowIcon: arrowIcon2,
117362
- customTranslations
118322
+ customTranslations,
118323
+ localeId
117363
118324
  } = pluginConfig;
118325
+ const instanceId = ++instanceSeq;
117364
118326
  const navKeydownHandlers = /* @__PURE__ */new WeakMap();
117365
118327
  const observers = [];
117366
118328
  const timeouts = [];
@@ -117372,12 +118334,7 @@ function customElements(pluginConfig) {
117372
118334
  liveRegion.setAttribute("aria-live", "assertive");
117373
118335
  liveRegion.setAttribute("aria-atomic", "true");
117374
118336
  liveRegion.setAttribute("role", "alert");
117375
- liveRegion.style.position = "absolute";
117376
- liveRegion.style.width = "1px";
117377
- liveRegion.style.height = "1px";
117378
- liveRegion.style.overflow = "hidden";
117379
- liveRegion.style.clip = "rect(0, 0, 0, 0)";
117380
- liveRegion.style.whiteSpace = "nowrap";
118337
+ visuallyHide(liveRegion);
117381
118338
  fp.calendarContainer.appendChild(liveRegion);
117382
118339
  }
117383
118340
  function announce(message2) {
@@ -117520,51 +118477,150 @@ function customElements(pluginConfig) {
117520
118477
  }
117521
118478
  }
117522
118479
  const GRID_COLS = 7;
118480
+ const hasWeekNumbers = () => !!fp?.config?.weekNumbers;
118481
+ const colOffset = () => hasWeekNumbers() ? 1 : 0;
118482
+ const weekHeaderId = rowIndex => `webchat-datepicker-${instanceId}-week-${rowIndex}`;
118483
+ const weekNumberLabel = () => customTranslations?.ariaLabels?.datePickerWeekNumber || "Week";
118484
+ const dayCellId = index2 => `webchat-datepicker-${instanceId}-day-${index2}`;
117523
118485
  function setDateSelectAlly() {
117524
118486
  fp?.calendarContainer?.setAttribute("tabindex", "-1");
117525
- const grid = fp?.calendarContainer?.querySelector(".flatpickr-rContainer");
118487
+ const rContainer = fp?.calendarContainer?.querySelector(".flatpickr-rContainer");
118488
+ const innerContainer = fp?.calendarContainer?.querySelector(".flatpickr-innerContainer");
118489
+ const grid = hasWeekNumbers() ? innerContainer : rContainer;
118490
+ const layoutWrapper = hasWeekNumbers() ? rContainer : innerContainer;
117526
118491
  if (grid) {
117527
118492
  grid.setAttribute("role", "grid");
117528
118493
  grid.setAttribute("aria-label", customTranslations?.ariaLabels?.datePickerGridLabel || "Calendar");
117529
118494
  grid.setAttribute("aria-description", customTranslations?.ariaLabels?.datePickerGridDescription || "Use arrow keys to navigate through dates");
117530
- grid.setAttribute("aria-colcount", String(GRID_COLS));
118495
+ grid.setAttribute("aria-colcount", String(GRID_COLS + colOffset()));
117531
118496
  grid.setAttribute("aria-rowcount", "7");
117532
118497
  }
117533
- const innerContainer = fp?.calendarContainer?.getElementsByClassName("flatpickr-innerContainer")[0];
117534
- innerContainer?.removeAttribute("tabindex");
117535
- innerContainer?.removeAttribute("role");
117536
- innerContainer?.removeAttribute("aria-activedescendant");
117537
- const weekdayContainer = fp?.calendarContainer?.querySelector(".flatpickr-weekdays");
117538
- if (weekdayContainer) {
117539
- weekdayContainer.setAttribute("role", "row");
117540
- weekdayContainer.setAttribute("aria-rowindex", "1");
117541
- }
117542
- const weekdaySpans = fp?.calendarContainer?.querySelectorAll(".flatpickr-weekday");
118498
+ layoutWrapper?.removeAttribute("tabindex");
118499
+ layoutWrapper?.removeAttribute("aria-activedescendant");
118500
+ layoutWrapper?.setAttribute("role", "presentation");
118501
+ if (hasWeekNumbers()) fp?.weekWrapper?.setAttribute("role", "presentation");
118502
+ const weekdayRow = fp?.calendarContainer?.querySelector(".flatpickr-weekdays");
118503
+ if (weekdayRow) {
118504
+ weekdayRow.setAttribute("role", "row");
118505
+ weekdayRow.setAttribute("aria-rowindex", "1");
118506
+ }
118507
+ weekdayRow?.querySelector(".flatpickr-weekdaycontainer")?.setAttribute("role", "presentation");
118508
+ const weekdaySpans = fp?.calendarContainer?.querySelectorAll(".flatpickr-weekdaycontainer .flatpickr-weekday");
117543
118509
  weekdaySpans?.forEach((span, index2) => {
117544
118510
  span.setAttribute("role", "columnheader");
117545
118511
  span.setAttribute("abbr", span.textContent?.trim() || "");
117546
- span.setAttribute("aria-colindex", String(index2 % GRID_COLS + 1));
118512
+ span.setAttribute("aria-colindex", String(index2 % GRID_COLS + 1 + colOffset()));
118513
+ });
118514
+ const visualWeekHeader = fp?.weekWrapper?.querySelector(".flatpickr-weekday");
118515
+ if (visualWeekHeader && weekdayRow) {
118516
+ visualWeekHeader.setAttribute("aria-hidden", "true");
118517
+ let weekHeader = weekdayRow.querySelector(":scope > [data-week-header]");
118518
+ if (!weekHeader) {
118519
+ weekHeader = document.createElement("span");
118520
+ weekHeader.dataset.weekHeader = "true";
118521
+ visuallyHide(weekHeader);
118522
+ weekdayRow.prepend(weekHeader);
118523
+ }
118524
+ weekHeader.id = weekHeaderId(1);
118525
+ weekHeader.setAttribute("role", "columnheader");
118526
+ weekHeader.setAttribute("abbr", visualWeekHeader.textContent?.trim() || "");
118527
+ weekHeader.setAttribute("aria-colindex", "1");
118528
+ weekHeader.textContent = weekNumberLabel();
118529
+ }
118530
+ }
118531
+ function syncWeekNumbers() {
118532
+ if (!hasWeekNumbers() || !fp?.weekNumbers) return;
118533
+ fp.weekNumbers.setAttribute("role", "presentation");
118534
+ const weekCells = Array.from(fp.weekNumbers.querySelectorAll(":scope > .flatpickr-day"));
118535
+ weekCells.forEach((cell, i) => {
118536
+ const rowIndex = i + 2;
118537
+ cell.id = weekHeaderId(rowIndex);
118538
+ cell.setAttribute("role", "rowheader");
118539
+ cell.setAttribute("aria-rowindex", String(rowIndex));
118540
+ cell.setAttribute("aria-colindex", "1");
118541
+ cell.setAttribute("aria-label", `${weekNumberLabel()} ${cell.textContent?.trim()}`);
117547
118542
  });
117548
118543
  }
117549
118544
  function applyDayGridIndices() {
117550
118545
  const dayContainer = fp?.calendarContainer?.querySelector(".dayContainer");
117551
118546
  if (!dayContainer) return;
117552
- dayContainer.setAttribute("role", "rowgroup");
118547
+ dayContainer.setAttribute("role", "presentation");
117553
118548
  const dayCells = Array.from(dayContainer.querySelectorAll(":scope > .flatpickr-day"));
117554
118549
  dayCells.forEach((cell, i) => {
117555
- cell.setAttribute("aria-rowindex", String(Math.floor(i / GRID_COLS) + 2));
117556
- cell.setAttribute("aria-colindex", String(i % GRID_COLS + 1));
117557
- });
117558
- }
117559
- function markSelectedCells() {
117560
- getDayCells().forEach(day => {
117561
- if (day.classList.contains("selected")) {
117562
- day.setAttribute("aria-selected", "true");
118550
+ cell.id = dayCellId(i);
118551
+ const rowIndex = Math.floor(i / GRID_COLS) + 2;
118552
+ cell.setAttribute("aria-rowindex", String(rowIndex));
118553
+ cell.setAttribute("aria-colindex", String(i % GRID_COLS + 1 + colOffset()));
118554
+ const weekHeader = hasWeekNumbers() ? fp?.weekNumbers?.querySelector(`[aria-rowindex="${rowIndex}"]`) : null;
118555
+ if (weekHeader?.id) {
118556
+ cell.setAttribute("aria-describedby", weekHeader.id);
117563
118557
  } else {
117564
- day.removeAttribute("aria-selected");
118558
+ cell.removeAttribute("aria-describedby");
117565
118559
  }
117566
118560
  });
117567
118561
  }
118562
+ function syncGridRows() {
118563
+ const daysContainer = fp?.calendarContainer?.querySelector(".flatpickr-days");
118564
+ const dayContainer = daysContainer?.querySelector(":scope > .dayContainer");
118565
+ if (!daysContainer || !dayContainer) return;
118566
+ daysContainer.setAttribute("role", "rowgroup");
118567
+ const dayCells = Array.from(dayContainer.querySelectorAll(":scope > .flatpickr-day"));
118568
+ const rowCount = Math.ceil(dayCells.length / GRID_COLS);
118569
+ for (let r = 0; r < rowCount; r++) {
118570
+ const rowIndex = r + 2;
118571
+ let row = daysContainer.querySelector(`:scope > [data-grid-row="${rowIndex}"]`);
118572
+ if (!row) {
118573
+ row = document.createElement("div");
118574
+ row.dataset.gridRow = String(rowIndex);
118575
+ row.setAttribute("role", "row");
118576
+ visuallyHide(row);
118577
+ daysContainer.appendChild(row);
118578
+ }
118579
+ row.setAttribute("aria-rowindex", String(rowIndex));
118580
+ const owned = dayCells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).map(c => c.id);
118581
+ const weekHeader = hasWeekNumbers() ? fp?.weekNumbers?.querySelector(`[aria-rowindex="${rowIndex}"]`) : null;
118582
+ if (weekHeader?.id) owned.unshift(weekHeader.id);
118583
+ row.setAttribute("aria-owns", owned.join(" "));
118584
+ }
118585
+ }
118586
+ function isSelectedCell(cell) {
118587
+ return cell.classList.contains("selected") || cell.classList.contains("inRange") || cell.classList.contains("startRange") || cell.classList.contains("endRange");
118588
+ }
118589
+ const pendingLabelRestore = /* @__PURE__ */new Set();
118590
+ function syncDayCellState(cell) {
118591
+ if (isSelectedCell(cell)) {
118592
+ cell.setAttribute("aria-selected", "true");
118593
+ } else {
118594
+ cell.removeAttribute("aria-selected");
118595
+ }
118596
+ if (pendingLabelRestore.has(cell)) return;
118597
+ const date = cellDate(cell);
118598
+ if (date) cell.setAttribute("aria-label", getDayLabel(cell, date));
118599
+ }
118600
+ function syncAllDayCells() {
118601
+ getDayCells().forEach(syncDayCellState);
118602
+ }
118603
+ function observeDayCellClasses() {
118604
+ const daysContainer = fp?.calendarContainer?.querySelector(".flatpickr-days");
118605
+ if (!daysContainer || daysContainer.dataset.classObserved === "true") return;
118606
+ daysContainer.dataset.classObserved = "true";
118607
+ const observer = new MutationObserver(records => {
118608
+ const touched = /* @__PURE__ */new Set();
118609
+ records.forEach(record => {
118610
+ const cell = record.target;
118611
+ if (cell.classList?.contains("flatpickr-day") && cell.closest(".dayContainer")) {
118612
+ touched.add(cell);
118613
+ }
118614
+ });
118615
+ touched.forEach(syncDayCellState);
118616
+ });
118617
+ observer.observe(daysContainer, {
118618
+ attributes: true,
118619
+ attributeFilter: ["class"],
118620
+ subtree: true
118621
+ });
118622
+ observers.push(observer);
118623
+ }
117568
118624
  function restoreRovingDay() {
117569
118625
  const existing = fp?.calendarContainer?.querySelector(".dayContainer .flatpickr-day[tabindex='0']");
117570
118626
  setActiveDay(existing || getInitialActiveDay(), {
@@ -117572,8 +118628,10 @@ function customElements(pluginConfig) {
117572
118628
  });
117573
118629
  }
117574
118630
  function refreshGridAfterRebuild() {
118631
+ syncWeekNumbers();
117575
118632
  applyDayGridIndices();
117576
- markSelectedCells();
118633
+ syncGridRows();
118634
+ syncAllDayCells();
117577
118635
  restoreRovingDay();
117578
118636
  }
117579
118637
  function observeDayGrid() {
@@ -117656,6 +118714,10 @@ function customElements(pluginConfig) {
117656
118714
  yearInput.setAttribute("tabindex", "0");
117657
118715
  }
117658
118716
  }
118717
+ function setValueInputAlly() {
118718
+ const headingId = fp?.calendarContainer?.closest('[role="dialog"]')?.getAttribute("aria-labelledby");
118719
+ if (headingId) fp?.input?.setAttribute("aria-labelledby", headingId);
118720
+ }
117659
118721
  function setTimeAlly() {
117660
118722
  fp?.calendarContainer?.setAttribute("aria-labelledby", "webchatDatePickerHeaderLabel");
117661
118723
  if (fp?.config?.enableTime) {
@@ -117665,17 +118727,95 @@ function customElements(pluginConfig) {
117665
118727
  minutesField?.setAttribute("tabIndex", "0");
117666
118728
  const amPmField = fp?.timeContainer?.getElementsByClassName("flatpickr-am-pm")?.[0];
117667
118729
  amPmField?.setAttribute("tabIndex", "0");
117668
- amPmField?.setAttribute("role", "button");
117669
118730
  }
117670
118731
  }
118732
+ function getAmPmValue() {
118733
+ return Array.from(fp?.amPM?.childNodes || []).filter(node2 => node2.nodeType === Node.TEXT_NODE).map(node2 => node2.textContent || "").join("").trim();
118734
+ }
118735
+ function setAmPmAlly() {
118736
+ const amPm = fp?.amPM;
118737
+ if (!amPm || amPm.dataset.a11yBound === "true") return;
118738
+ amPm.dataset.a11yBound = "true";
118739
+ const [am, pm] = fp.l10n.amPM;
118740
+ amPm.setAttribute("role", "spinbutton");
118741
+ amPm.setAttribute("aria-label", customTranslations?.ariaLabels?.datePickerAmPm || "AM/PM");
118742
+ amPm.removeAttribute("title");
118743
+ amPm.setAttribute("aria-valuemin", "0");
118744
+ amPm.setAttribute("aria-valuemax", "1");
118745
+ const syncValue = () => {
118746
+ const value = getAmPmValue();
118747
+ if (!value) return;
118748
+ amPm.setAttribute("aria-valuenow", value === am ? "1" : "0");
118749
+ amPm.setAttribute("aria-valuetext", value);
118750
+ };
118751
+ syncValue();
118752
+ const toggle = () => {
118753
+ amPm.click();
118754
+ syncValue();
118755
+ };
118756
+ const setTo = target => {
118757
+ if (getAmPmValue() !== target) toggle();
118758
+ };
118759
+ amPm.addEventListener("keydown", event => {
118760
+ const actions = {
118761
+ ArrowUp: () => setTo(am),
118762
+ ArrowDown: () => setTo(pm),
118763
+ // APG spinbutton: Home = minimum (aria-valuenow 0 = PM), End = maximum (1 = AM).
118764
+ Home: () => setTo(pm),
118765
+ End: () => setTo(am),
118766
+ Enter: toggle,
118767
+ " ": toggle,
118768
+ Spacebar: toggle
118769
+ };
118770
+ const action = actions[event.key];
118771
+ if (!action) return;
118772
+ event.preventDefault();
118773
+ event.stopPropagation();
118774
+ action();
118775
+ });
118776
+ const observer = new MutationObserver(syncValue);
118777
+ observer.observe(amPm, {
118778
+ childList: true,
118779
+ characterData: true,
118780
+ subtree: true
118781
+ });
118782
+ observers.push(observer);
118783
+ }
117671
118784
  function getInMonthCells() {
117672
118785
  return getDayCells().filter(c => !c.classList.contains("prevMonthDay") && !c.classList.contains("nextMonthDay"));
117673
118786
  }
117674
118787
  function cellDate(cell) {
117675
118788
  return cell.dateObj;
117676
118789
  }
117677
- function getDayLabel(date) {
117678
- return `${fp.l10n.months.longhand[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
118790
+ const dateFormatter = (() => {
118791
+ if (!localeId) return null;
118792
+ try {
118793
+ return new Intl.DateTimeFormat(localeId.replace("_", "-"), {
118794
+ weekday: "long",
118795
+ year: "numeric",
118796
+ month: "long",
118797
+ day: "numeric"
118798
+ });
118799
+ } catch {
118800
+ return null;
118801
+ }
118802
+ })();
118803
+ function formatSpokenDate(date) {
118804
+ if (dateFormatter) return dateFormatter.format(date);
118805
+ const weekday = fp.l10n.weekdays.longhand[date.getDay()];
118806
+ const month = fp.l10n.months.longhand[date.getMonth()];
118807
+ return `${weekday}, ${month} ${date.getDate()}, ${date.getFullYear()}`;
118808
+ }
118809
+ function getDayLabel(cell, date) {
118810
+ const labels = customTranslations?.ariaLabels;
118811
+ const parts = [formatSpokenDate(date)];
118812
+ if (cell.classList.contains("startRange")) {
118813
+ parts.push(labels?.datePickerRangeStart || "start of range");
118814
+ }
118815
+ if (cell.classList.contains("endRange")) {
118816
+ parts.push(labels?.datePickerRangeEnd || "end of range");
118817
+ }
118818
+ return parts.join(", ");
117679
118819
  }
117680
118820
  function sameYMD(a, b) {
117681
118821
  return !!a && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
@@ -117828,11 +118968,12 @@ function customElements(pluginConfig) {
117828
118968
  day.setAttribute("tabindex", day === target ? "0" : "-1");
117829
118969
  });
117830
118970
  }
117831
- const dateLabel = target.getAttribute("aria-label");
117832
- if (dateLabel) {
118971
+ if (target.getAttribute("aria-label") && !pendingLabelRestore.has(target)) {
118972
+ pendingLabelRestore.add(target);
117833
118973
  target.setAttribute("aria-label", "");
117834
118974
  setTimeout(() => {
117835
- if (target.isConnected) target.setAttribute("aria-label", dateLabel);
118975
+ pendingLabelRestore.delete(target);
118976
+ if (target.isConnected) syncDayCellState(target);
117836
118977
  }, 0);
117837
118978
  }
117838
118979
  });
@@ -117846,7 +118987,7 @@ function customElements(pluginConfig) {
117846
118987
  liveRegion = null;
117847
118988
  }
117848
118989
  return {
117849
- onReady: [upsertTimeArrows, buildTimeArrows, setDateSelectAlly, setTimeAlly, setMonthSelectAlly, setYearSelectAlly, setNavButtonsAlly, observeNavButtons, observeMonthSelector, createLiveRegion, setGridKeyNavigation, syncRovingTabindex,
118990
+ onReady: [upsertTimeArrows, buildTimeArrows, setDateSelectAlly, setValueInputAlly, setTimeAlly, setMonthSelectAlly, setYearSelectAlly, setNavButtonsAlly, observeNavButtons, observeMonthSelector, createLiveRegion, setAmPmAlly, setGridKeyNavigation, syncRovingTabindex, observeDayCellClasses,
117850
118991
  // Apply grid row roles + the initial roving-focus day, and keep them correct
117851
118992
  // across every flatpickr rebuild. Does not steal focus — the dialog focuses its
117852
118993
  // heading on open; the first Tab into the grid lands on the focusable day.
@@ -117864,13 +119005,7 @@ function customElements(pluginConfig) {
117864
119005
  dayElem.innerHTML = `<span class='dayInner'>${dayElem.innerHTML}</span>`;
117865
119006
  dayElem.setAttribute("role", "gridcell");
117866
119007
  dayElem.setAttribute("tabindex", "-1");
117867
- if (dayElem.classList.contains("selected")) {
117868
- dayElem.setAttribute("aria-selected", "true");
117869
- } else {
117870
- dayElem.removeAttribute("aria-selected");
117871
- }
117872
- const date = cellDate(dayElem);
117873
- if (date) dayElem.setAttribute("aria-label", getDayLabel(date));
119008
+ syncDayCellState(dayElem);
117874
119009
  }, handleWeekNumbers],
117875
119010
  onValueUpdate: [upsertTimeArrows,
117876
119011
  // Selecting a date rebuilds the day grid synchronously (flatpickr calls buildDays()
@@ -117972,7 +119107,8 @@ const getOptionsFromMessage = (message2, customTranslations) => {
117972
119107
  formatDate: !data.dateFormat ? date => moment(date).locale(momentLocaleId).format(dateFormatLocalString) : void 0,
117973
119108
  plugins: [customElements({
117974
119109
  arrowIcon,
117975
- customTranslations: customTranslations || {}
119110
+ customTranslations: customTranslations || {},
119111
+ localeId: momentLocaleId
117976
119112
  })]
117977
119113
  };
117978
119114
  const enable_disable = Array.isArray(data?.enable_disable) && data.enable_disable.length > 0 ? data.enable_disable : null;