@depict-ai/ui 4.10.20 → 4.10.21

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.
@@ -10,7 +10,7 @@ import $b8hua$babelruntimehelpersclassPrivateFieldGet from "@babel/runtime/helpe
10
10
  /*! Depict.ai search and product listing SDK */
11
11
  import { register_es_version } from "../../multi_import_deduplicator.mjs";
12
12
  register_es_version("ui", "ES10");
13
- const $8db9a15135765c11$export$83d89fbfd8236492 = "4.10.20";
13
+ const $8db9a15135765c11$export$83d89fbfd8236492 = "4.10.21";
14
14
  function $00a5b94f0c11efbb$export$d9f36cf74e24d595(media_query) {
15
15
  const signal = $b8hua$createSignal(false);
16
16
  $b8hua$onCleanup($b8hua$javascript_media_query(media_query, _ref => {
@@ -2113,6 +2113,63 @@ function $4711a82d08d2021b$export$fecbb157621501bd(_ref) {
2113
2113
  }));
2114
2114
  return outmost_element;
2115
2115
  }
2116
+
2117
+ /**
2118
+ * `HTMLElement.focus()` is a **silent no-op** on an element that is not being rendered
2119
+ * (`display: none`, `visibility: hidden`, a collapsed menu). Nothing throws, focus simply
2120
+ * stays where it was, so a caller that does not check believes it restored focus when it
2121
+ * left it on `<body>` — which puts the keyboard user back at the top of the document.
2122
+ *
2123
+ * That is reachable whenever the element which opened a modal stops being rendered while the
2124
+ * modal is open, and the common way for that to happen is the responsive-duplicate header:
2125
+ * a theme renders the same search trigger twice, once for desktop and once for mobile, and
2126
+ * hides one at the current breakpoint. Open the modal from the desktop trigger, cross the
2127
+ * breakpoint (rotate a tablet, resize a window), close the modal, and the remembered trigger
2128
+ * is still `isConnected` but no longer rendered. Verified against a live merchant storefront
2129
+ * on 4.10.19.
2130
+ *
2131
+ * So: verify that focus landed, and when it did not, look for a rendered twin of the same
2132
+ * control — the other half of the responsive pair — and use that instead.
2133
+ */ /**
2134
+ * An element can only take focus if it is actually being rendered.
2135
+ *
2136
+ * Injectable because it is the one part of this module that needs a layout engine: jsdom
2137
+ * reports no client rects for anything, so the specs pass their own predicate to exercise the
2138
+ * matching rules. The real predicate is only exercised in a browser.
2139
+ */
2140
+ const $1c5106dfe72d554c$export$e0652711c614c755 = element => !!element.getClientRects().length;
2141
+ /**
2142
+ * `document.activeElement` is the outermost shadow host when focus is inside a shadow tree,
2143
+ * so ask the element's own root rather than the document — matching how the rest of
2144
+ * modal_opener treats shadow-mounted modals.
2145
+ */
2146
+ const $1c5106dfe72d554c$var$has_focus = element => element.getRootNode().activeElement === element;
2147
+ /**
2148
+ * The accessible-name-ish identity we match twins on. Deliberately narrow: an explicit label
2149
+ * or the trimmed text. Two controls that share a tag and this string are the same control
2150
+ * rendered twice, which is exactly the responsive-duplicate case.
2151
+ */
2152
+ const $1c5106dfe72d554c$var$identity_of = element => (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "").trim().replace(/\s+/g, " ");
2153
+ function $1c5106dfe72d554c$export$5800d9d535a06e96(element) {
2154
+ let rendered = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : $1c5106dfe72d554c$export$e0652711c614c755;
2155
+ const identity = $1c5106dfe72d554c$var$identity_of(element);
2156
+ if (!identity) return; // nothing to match on — an icon-only button with no label
2157
+ const twins = [...element.ownerDocument.querySelectorAll(element.localName)].filter(candidate => candidate !== element && rendered(candidate) && $1c5106dfe72d554c$var$identity_of(candidate) === identity);
2158
+ return twins.length === 1 ? twins[0] : undefined;
2159
+ }
2160
+ function $1c5106dfe72d554c$export$51461d633c351649(element) {
2161
+ let rendered = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : $1c5106dfe72d554c$export$e0652711c614c755;
2162
+ element.focus({
2163
+ preventScroll: true
2164
+ });
2165
+ if ($1c5106dfe72d554c$var$has_focus(element)) return true;
2166
+ const twin = $1c5106dfe72d554c$export$5800d9d535a06e96(element, rendered);
2167
+ if (!twin) return false;
2168
+ twin.focus({
2169
+ preventScroll: true
2170
+ });
2171
+ return $1c5106dfe72d554c$var$has_focus(twin);
2172
+ }
2116
2173
  async function $d5f1e6a0f9117286$export$79b1565500412d9b(search_modal, modal_fn_opts, parent) {
2117
2174
  const body = parent || document.body || (await (0, $b8hua$observer).wait_for_element("body"));
2118
2175
  let dispose_and_remove;
@@ -2178,9 +2235,12 @@ async function $d5f1e6a0f9117286$export$79b1565500412d9b(search_modal, modal_fn_
2178
2235
  });
2179
2236
  focused = requested_focus_target.getRootNode().activeElement === requested_focus_target;
2180
2237
  }
2181
- if (!focused && element_focused_before_open !== null && element_focused_before_open !== void 0 && element_focused_before_open.isConnected) element_focused_before_open.focus({
2182
- preventScroll: true
2183
- });
2238
+ if (!focused && element_focused_before_open !== null && element_focused_before_open !== void 0 && element_focused_before_open.isConnected)
2239
+ // restore_focus_to rather than a bare focus(): the remembered trigger can be
2240
+ // connected but no longer rendered (a responsive-duplicate header whose other
2241
+ // half is showing now), and focus() on an unrendered element silently does
2242
+ // nothing, leaving the keyboard user at the top of the page.
2243
+ $1c5106dfe72d554c$export$51461d633c351649(element_focused_before_open);
2184
2244
  } finally {
2185
2245
  // Cleared on a task instead of synchronously in case a browser delivers the focus event async
2186
2246
  setTimeout(() => restoring_focus = false, 0);
@@ -7939,20 +7999,28 @@ function $ac571292d606f30b$var$_classPrivateMethodGet(receiver, privateSet, fn)
7939
7999
  }
7940
8000
  var $ac571292d606f30b$var$_throwing_on_navigation = /*#__PURE__*/new WeakSet();
7941
8001
  var $ac571292d606f30b$var$_get_navigation_happened_promise = /*#__PURE__*/new WeakSet();
8002
+ var $ac571292d606f30b$var$_watch_for_navigation_to_same_href = /*#__PURE__*/new WeakSet();
7942
8003
  class $ac571292d606f30b$export$7e1297a3a19f10d8 {
7943
8004
  /**
7944
8005
  * Function that will be called when we want to navigate, or a string saying it's not an SPA. You can change this OTG
7945
8006
  */ /**
7946
8007
  * We store listing_id in history.state and do operate on this both here and in js-ui, this is the key to be used which js-ui provides
7947
8008
  */ /**
7948
- * Vue has async router that needs to be awaited (it navigates after a microtask)
7949
- * This standardises waiting for all kinds of routers
7950
- */
8009
+ * Called when a go_to_ didn't change location.href, for example when following a link to the page one is already on. href_change_ipns stays silent then, so whoever waits for a href change to react to a navigation has to listen here too
8010
+ */ /**
8011
+ * Vue has async router that needs to be awaited (it navigates after a microtask)
8012
+ * This standardises waiting for all kinds of routers
8013
+ */ /**
8014
+ * Calls navigation_to_same_href_listeners_ if the navigation we're about to request ends on the href we're already on
8015
+ * @returns function to stop watching, call it when the navigation failed
8016
+ */
7951
8017
  constructor(on_navigation, listing_query_state_key) {
8018
+ $ac571292d606f30b$var$_classPrivateMethodInitSpec(this, $ac571292d606f30b$var$_watch_for_navigation_to_same_href);
7952
8019
  $ac571292d606f30b$var$_classPrivateMethodInitSpec(this, $ac571292d606f30b$var$_get_navigation_happened_promise);
7953
8020
  $ac571292d606f30b$var$_classPrivateMethodInitSpec(this, $ac571292d606f30b$var$_throwing_on_navigation);
7954
8021
  $b8hua$babelruntimehelpersdefineProperty(this, "on_navigation_", void 0);
7955
8022
  $b8hua$babelruntimehelpersdefineProperty(this, "listing_query_state_key", void 0);
8023
+ $b8hua$babelruntimehelpersdefineProperty(this, "navigation_to_same_href_listeners_", new Set());
7956
8024
  $b8hua$babelruntimehelpersdefineProperty(this, "navigate_", {
7957
8025
  /**
7958
8026
  * Call this if you *just* want to replace the state object in history.state
@@ -7994,6 +8062,7 @@ class $ac571292d606f30b$export$7e1297a3a19f10d8 {
7994
8062
  }
7995
8063
  event_ === null || event_ === void 0 || event_.preventDefault();
7996
8064
  const promise = $ac571292d606f30b$var$_classPrivateMethodGet(this, $ac571292d606f30b$var$_get_navigation_happened_promise, $ac571292d606f30b$var$_get_navigation_happened_promise2).call(this, true);
8065
+ const stop_watching_for_same_href = $ac571292d606f30b$var$_classPrivateMethodGet(this, $ac571292d606f30b$var$_watch_for_navigation_to_same_href, $ac571292d606f30b$var$_watch_for_navigation_to_same_href2).call(this, new_url_);
7997
8066
  if (force_spa_navigation_ && wants_hard_navigation) {
7998
8067
  // Don't allow hard navigation if we're forcing SPA navigation, which we do for things where PageReplacer can be used for performance
7999
8068
  const new_state = history.state;
@@ -8007,7 +8076,7 @@ class $ac571292d606f30b$export$7e1297a3a19f10d8 {
8007
8076
  is_replace: is_replace_,
8008
8077
  new_url: new_url_ instanceof URL ? new_url_ : new URL(new_url_, location.href),
8009
8078
  scroll: scroll
8010
- });
8079
+ }, stop_watching_for_same_href);
8011
8080
  await promise;
8012
8081
  })
8013
8082
  });
@@ -8015,15 +8084,16 @@ class $ac571292d606f30b$export$7e1297a3a19f10d8 {
8015
8084
  this.on_navigation_ = on_navigation;
8016
8085
  }
8017
8086
  }
8018
- function $ac571292d606f30b$var$_throwing_on_navigation2() {
8087
+ function $ac571292d606f30b$var$_throwing_on_navigation2(options, on_throw_) {
8019
8088
  const {
8020
8089
  on_navigation_: on_navigation_
8021
8090
  } = this;
8022
8091
  if (typeof on_navigation_ !== "function") throw new Error("on_navigation_ is not a function");
8023
8092
  // make sure that we don't accidentally catchify if the SDK consumer has fucked up
8024
8093
  try {
8025
- return on_navigation_(...arguments);
8094
+ return on_navigation_(options);
8026
8095
  } catch (e) {
8096
+ on_throw_ === null || on_throw_ === void 0 || on_throw_();
8027
8097
  queueMicrotask(() => {
8028
8098
  throw e;
8029
8099
  });
@@ -8040,6 +8110,24 @@ function $ac571292d606f30b$var$_get_navigation_happened_promise2(is_replace) {
8040
8110
  (0, $b8hua$instant_exec_on_suspect_history_change).add(handler);
8041
8111
  });
8042
8112
  }
8113
+ function $ac571292d606f30b$var$_watch_for_navigation_to_same_href2(target_url) {
8114
+ const href_before = location.href;
8115
+ const stop_watching = () => {
8116
+ (0, $b8hua$instant_exec_on_suspect_history_change).delete(on_history_change);
8117
+ clearTimeout(timeout);
8118
+ };
8119
+ const notify_if_href_unchanged = $b8hua$catchify1(() => {
8120
+ stop_watching();
8121
+ if (location.href !== href_before) return;
8122
+ this.navigation_to_same_href_listeners_.forEach(listener => $b8hua$catchify1(listener)());
8123
+ });
8124
+ // The router has acted once it touches history, if the href is still the same then it took us to the page we're on
8125
+ const on_history_change = () => notify_if_href_unchanged();
8126
+ // A router asked to go to the exact href we're on might not touch history at all, so don't wait longer than 500ms for it. For any other target we can't tell a slow navigation from one that goes nowhere, so only stop listening after a while
8127
+ const timeout = target_url.href === href_before ? setTimeout(notify_if_href_unchanged, 500) : setTimeout(stop_watching, 10000);
8128
+ (0, $b8hua$instant_exec_on_suspect_history_change).add(on_history_change);
8129
+ return stop_watching;
8130
+ }
8043
8131
  var $e3efeb558d952696$var$_tmpl$ = ["<button", ' class="show-more major" type="button">', "</button>"],
8044
8132
  $e3efeb558d952696$var$_tmpl$2 = ["<h2", ' class="recs-title">', "</h2>"],
8045
8133
  $e3efeb558d952696$var$_tmpl$3 = ["<div", ' class="recommendation-grid"><!--$-->', "<!--/--><!--$-->", "<!--/--></div>"];
@@ -9412,11 +9500,17 @@ var $fac58b5610eec6a9$var$_tmpl$ = ["<svg", ' xmlns="http://www.w3.org/2000/svg"
9412
9500
  function $fac58b5610eec6a9$export$8d8893ffd8aca39a() {
9413
9501
  return $b8hua$ssr($fac58b5610eec6a9$var$_tmpl$, $b8hua$ssrHydrationKey());
9414
9502
  }
9415
- function $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_) {
9503
+ function $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_, router_) {
9416
9504
  const [exit_iterable, exitable_iterable] = $b8hua$make_asyncIterable_exiter((0, $b8hua$href_change_ipns));
9505
+ const {
9506
+ navigation_to_same_href_listeners_: navigation_to_same_href_listeners_
9507
+ } = router_;
9417
9508
  let set_initial_values = false;
9418
9509
  let old_pathname;
9419
9510
  let old_param_value;
9511
+ // A card linking to the page one is already on doesn't change the href, close the modal anyway so the click does something
9512
+ navigation_to_same_href_listeners_.add(close_modal);
9513
+ $b8hua$onCleanup(() => navigation_to_same_href_listeners_.delete(close_modal));
9420
9514
  $b8hua$onCleanup($b8hua$catchify1(exit_iterable));
9421
9515
  $b8hua$catchify1(async () => {
9422
9516
  for await (const href of exitable_iterable) {
@@ -9566,7 +9660,7 @@ function $40411f2ec772c376$var$RawClassicSearchModal(_ref) {
9566
9660
  }
9567
9661
  })));
9568
9662
  if (disable_scrolling_) $e56d241d0f33d459$export$fe334901e8bebd57();
9569
- $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9663
+ $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9570
9664
  window.addEventListener("keydown", close_on_escape_handler);
9571
9665
  $b8hua$onCleanup($b8hua$catchify1(() => window.removeEventListener("keydown", close_on_escape_handler)));
9572
9666
  if (closing_animation_) register_closing_animation_(closing_animation_);
@@ -10299,7 +10393,7 @@ function $06fcd2a345efd24d$var$RawSearchModalV2(_ref) {
10299
10393
  return (((_get_modal_search_res5 = get_modal_search_results_()) === null || _get_modal_search_res5 === void 0 ? void 0 : _get_modal_search_res5.loading) === false && !suggestions_response_.loading || !!prev) && totalNumberResults() === 0 && previous_searches_matching_query().length === 0;
10300
10394
  });
10301
10395
  if (disable_scrolling_) $e56d241d0f33d459$export$fe334901e8bebd57();
10302
- $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
10396
+ $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
10303
10397
  addEventListener("keydown", close_on_escape_handler);
10304
10398
  $b8hua$onCleanup(() => removeEventListener("keydown", close_on_escape_handler));
10305
10399
  $b8hua$createEffect($b8hua$on(get_search_field_value_, () => {
@@ -109,7 +109,7 @@ $parcel$export(module.exports, "BreadCrumbs", () => $1f4925833f07df29$export$5b7
109
109
  $parcel$export(module.exports, "QuickLinks", () => $1f4925833f07df29$export$de035477d6fef996);
110
110
  $parcel$export(module.exports, "CategoryPage", () => $14f289047713d67a$export$e2bf8ce7779e827e);
111
111
  /*! Depict.ai search and product listing SDK */
112
- const $b757f75811f3261f$export$83d89fbfd8236492 = "4.10.20";
112
+ const $b757f75811f3261f$export$83d89fbfd8236492 = "4.10.21";
113
113
  function $b54e6192d0caa293$export$d9f36cf74e24d595(media_query) {
114
114
  const signal = $5vyXB$solidjs.createSignal(false);
115
115
  $5vyXB$solidjs.onCleanup($5vyXB$depictaiutilishared.javascript_media_query(media_query, ({
@@ -2122,6 +2122,61 @@ function $235a2771a44e28e9$export$fecbb157621501bd({
2122
2122
  }));
2123
2123
  return outmost_element;
2124
2124
  }
2125
+
2126
+ /**
2127
+ * `HTMLElement.focus()` is a **silent no-op** on an element that is not being rendered
2128
+ * (`display: none`, `visibility: hidden`, a collapsed menu). Nothing throws, focus simply
2129
+ * stays where it was, so a caller that does not check believes it restored focus when it
2130
+ * left it on `<body>` — which puts the keyboard user back at the top of the document.
2131
+ *
2132
+ * That is reachable whenever the element which opened a modal stops being rendered while the
2133
+ * modal is open, and the common way for that to happen is the responsive-duplicate header:
2134
+ * a theme renders the same search trigger twice, once for desktop and once for mobile, and
2135
+ * hides one at the current breakpoint. Open the modal from the desktop trigger, cross the
2136
+ * breakpoint (rotate a tablet, resize a window), close the modal, and the remembered trigger
2137
+ * is still `isConnected` but no longer rendered. Verified against a live merchant storefront
2138
+ * on 4.10.19.
2139
+ *
2140
+ * So: verify that focus landed, and when it did not, look for a rendered twin of the same
2141
+ * control — the other half of the responsive pair — and use that instead.
2142
+ */ /**
2143
+ * An element can only take focus if it is actually being rendered.
2144
+ *
2145
+ * Injectable because it is the one part of this module that needs a layout engine: jsdom
2146
+ * reports no client rects for anything, so the specs pass their own predicate to exercise the
2147
+ * matching rules. The real predicate is only exercised in a browser.
2148
+ */
2149
+ const $7f80d1bc95bbbc60$export$e0652711c614c755 = element => !!element.getClientRects().length;
2150
+ /**
2151
+ * `document.activeElement` is the outermost shadow host when focus is inside a shadow tree,
2152
+ * so ask the element's own root rather than the document — matching how the rest of
2153
+ * modal_opener treats shadow-mounted modals.
2154
+ */
2155
+ const $7f80d1bc95bbbc60$var$has_focus = element => element.getRootNode().activeElement === element;
2156
+ /**
2157
+ * The accessible-name-ish identity we match twins on. Deliberately narrow: an explicit label
2158
+ * or the trimmed text. Two controls that share a tag and this string are the same control
2159
+ * rendered twice, which is exactly the responsive-duplicate case.
2160
+ */
2161
+ const $7f80d1bc95bbbc60$var$identity_of = element => (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "").trim().replace(/\s+/g, " ");
2162
+ function $7f80d1bc95bbbc60$export$5800d9d535a06e96(element, rendered = $7f80d1bc95bbbc60$export$e0652711c614c755) {
2163
+ const identity = $7f80d1bc95bbbc60$var$identity_of(element);
2164
+ if (!identity) return; // nothing to match on — an icon-only button with no label
2165
+ const twins = [...element.ownerDocument.querySelectorAll(element.localName)].filter(candidate => candidate !== element && rendered(candidate) && $7f80d1bc95bbbc60$var$identity_of(candidate) === identity);
2166
+ return twins.length === 1 ? twins[0] : undefined;
2167
+ }
2168
+ function $7f80d1bc95bbbc60$export$51461d633c351649(element, rendered = $7f80d1bc95bbbc60$export$e0652711c614c755) {
2169
+ element.focus({
2170
+ preventScroll: true
2171
+ });
2172
+ if ($7f80d1bc95bbbc60$var$has_focus(element)) return true;
2173
+ const twin = $7f80d1bc95bbbc60$export$5800d9d535a06e96(element, rendered);
2174
+ if (!twin) return false;
2175
+ twin.focus({
2176
+ preventScroll: true
2177
+ });
2178
+ return $7f80d1bc95bbbc60$var$has_focus(twin);
2179
+ }
2125
2180
  async function $9abb0a35d4b60ed3$export$79b1565500412d9b(search_modal, modal_fn_opts, parent) {
2126
2181
  const body = parent || document.body || (await (0, $5vyXB$depictaiutilishared.observer).wait_for_element("body"));
2127
2182
  let dispose_and_remove;
@@ -2182,9 +2237,12 @@ async function $9abb0a35d4b60ed3$export$79b1565500412d9b(search_modal, modal_fn_
2182
2237
  });
2183
2238
  focused = requested_focus_target.getRootNode().activeElement === requested_focus_target;
2184
2239
  }
2185
- if (!focused && element_focused_before_open?.isConnected) element_focused_before_open.focus({
2186
- preventScroll: true
2187
- });
2240
+ if (!focused && element_focused_before_open?.isConnected)
2241
+ // restore_focus_to rather than a bare focus(): the remembered trigger can be
2242
+ // connected but no longer rendered (a responsive-duplicate header whose other
2243
+ // half is showing now), and focus() on an unrendered element silently does
2244
+ // nothing, leaving the keyboard user at the top of the page.
2245
+ $7f80d1bc95bbbc60$export$51461d633c351649(element_focused_before_open);
2188
2246
  } finally {
2189
2247
  // Cleared on a task instead of synchronously in case a browser delivers the focus event async
2190
2248
  setTimeout(() => restoring_focus = false, 0);
@@ -7589,16 +7647,20 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7589
7647
  * Function that will be called when we want to navigate, or a string saying it's not an SPA. You can change this OTG
7590
7648
  */ /**
7591
7649
  * We store listing_id in history.state and do operate on this both here and in js-ui, this is the key to be used which js-ui provides
7592
- */
7593
- #throwing_on_navigation(...args) {
7650
+ */ /**
7651
+ * Called when a go_to_ didn't change location.href, for example when following a link to the page one is already on. href_change_ipns stays silent then, so whoever waits for a href change to react to a navigation has to listen here too
7652
+ */
7653
+ navigation_to_same_href_listeners_ = new Set();
7654
+ #throwing_on_navigation(options, on_throw_) {
7594
7655
  const {
7595
7656
  on_navigation_: on_navigation_
7596
7657
  } = this;
7597
7658
  if (typeof on_navigation_ !== "function") throw new Error("on_navigation_ is not a function");
7598
7659
  // make sure that we don't accidentally catchify if the SDK consumer has fucked up
7599
7660
  try {
7600
- return on_navigation_(...args);
7661
+ return on_navigation_(options);
7601
7662
  } catch (e) {
7663
+ on_throw_?.();
7602
7664
  queueMicrotask(() => {
7603
7665
  throw e;
7604
7666
  });
@@ -7619,6 +7681,28 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7619
7681
  (0, $5vyXB$depictaiutilishared.instant_exec_on_suspect_history_change).add(handler);
7620
7682
  });
7621
7683
  }
7684
+ /**
7685
+ * Calls navigation_to_same_href_listeners_ if the navigation we're about to request ends on the href we're already on
7686
+ * @returns function to stop watching, call it when the navigation failed
7687
+ */
7688
+ #watch_for_navigation_to_same_href(target_url) {
7689
+ const href_before = location.href;
7690
+ const stop_watching = () => {
7691
+ (0, $5vyXB$depictaiutilishared.instant_exec_on_suspect_history_change).delete(on_history_change);
7692
+ clearTimeout(timeout);
7693
+ };
7694
+ const notify_if_href_unchanged = $5vyXB$depictaiutilishared.catchify(() => {
7695
+ stop_watching();
7696
+ if (location.href !== href_before) return;
7697
+ this.navigation_to_same_href_listeners_.forEach(listener => $5vyXB$depictaiutilishared.catchify(listener)());
7698
+ });
7699
+ // The router has acted once it touches history, if the href is still the same then it took us to the page we're on
7700
+ const on_history_change = () => notify_if_href_unchanged();
7701
+ // A router asked to go to the exact href we're on might not touch history at all, so don't wait longer than 500ms for it. For any other target we can't tell a slow navigation from one that goes nowhere, so only stop listening after a while
7702
+ const timeout = target_url.href === href_before ? setTimeout(notify_if_href_unchanged, 500) : setTimeout(stop_watching, 10000);
7703
+ (0, $5vyXB$depictaiutilishared.instant_exec_on_suspect_history_change).add(on_history_change);
7704
+ return stop_watching;
7705
+ }
7622
7706
  navigate_ = {
7623
7707
  /**
7624
7708
  * Call this if you *just* want to replace the state object in history.state
@@ -7658,6 +7742,7 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7658
7742
  }
7659
7743
  event_?.preventDefault();
7660
7744
  const promise = this.#get_navigation_happened_promise(true);
7745
+ const stop_watching_for_same_href = this.#watch_for_navigation_to_same_href(new_url_);
7661
7746
  if (force_spa_navigation_ && wants_hard_navigation) {
7662
7747
  // Don't allow hard navigation if we're forcing SPA navigation, which we do for things where PageReplacer can be used for performance
7663
7748
  const new_state = history.state;
@@ -7671,7 +7756,7 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7671
7756
  is_replace: is_replace_,
7672
7757
  new_url: new_url_ instanceof URL ? new_url_ : new URL(new_url_, location.href),
7673
7758
  scroll: scroll
7674
- });
7759
+ }, stop_watching_for_same_href);
7675
7760
  await promise;
7676
7761
  })
7677
7762
  };
@@ -8969,11 +9054,17 @@ var $610b584d4f54699e$var$_tmpl$ = ["<svg", ' xmlns="http://www.w3.org/2000/svg"
8969
9054
  function $610b584d4f54699e$export$8d8893ffd8aca39a() {
8970
9055
  return $5vyXB$solidjsweb.ssr($610b584d4f54699e$var$_tmpl$, $5vyXB$solidjsweb.ssrHydrationKey());
8971
9056
  }
8972
- function $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_) {
9057
+ function $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_, router_) {
8973
9058
  const [exit_iterable, exitable_iterable] = $5vyXB$depictaiutilishared.make_asyncIterable_exiter((0, $5vyXB$depictaiutilishared.href_change_ipns));
9059
+ const {
9060
+ navigation_to_same_href_listeners_: navigation_to_same_href_listeners_
9061
+ } = router_;
8974
9062
  let set_initial_values = false;
8975
9063
  let old_pathname;
8976
9064
  let old_param_value;
9065
+ // A card linking to the page one is already on doesn't change the href, close the modal anyway so the click does something
9066
+ navigation_to_same_href_listeners_.add(close_modal);
9067
+ $5vyXB$solidjs.onCleanup(() => navigation_to_same_href_listeners_.delete(close_modal));
8977
9068
  $5vyXB$solidjs.onCleanup($5vyXB$depictaiutilishared.catchify(exit_iterable));
8978
9069
  $5vyXB$depictaiutilishared.catchify(async () => {
8979
9070
  for await (const href of exitable_iterable) {
@@ -9118,7 +9209,7 @@ function $44f0b2e312a9b710$var$RawClassicSearchModal({
9118
9209
  }
9119
9210
  })));
9120
9211
  if (disable_scrolling_) $ca1176b694a6ee2e$export$fe334901e8bebd57();
9121
- $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9212
+ $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9122
9213
  window.addEventListener("keydown", close_on_escape_handler);
9123
9214
  $5vyXB$solidjs.onCleanup($5vyXB$depictaiutilishared.catchify(() => window.removeEventListener("keydown", close_on_escape_handler)));
9124
9215
  if (closing_animation_) register_closing_animation_(closing_animation_);
@@ -9811,7 +9902,7 @@ function $11597ae03e703a47$var$RawSearchModalV2({
9811
9902
  });
9812
9903
  const noResults = $5vyXB$solidjs.createMemo(prev => (get_modal_search_results_()?.loading === false && !suggestions_response_.loading || !!prev) && totalNumberResults() === 0 && previous_searches_matching_query().length === 0);
9813
9904
  if (disable_scrolling_) $ca1176b694a6ee2e$export$fe334901e8bebd57();
9814
- $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9905
+ $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9815
9906
  addEventListener("keydown", close_on_escape_handler);
9816
9907
  $5vyXB$solidjs.onCleanup(() => removeEventListener("keydown", close_on_escape_handler));
9817
9908
  $5vyXB$solidjs.createEffect($5vyXB$solidjs.on(get_search_field_value_, () => {
@@ -11,7 +11,7 @@ import { GetListingRequest } from "@depict-ai/types/api/GetListingRequest";
11
11
  import { BaseSearchRequestV3 } from "@depict-ai/types/api/BaseSearchRequestV3";
12
12
  import { FeaturedInDisplay } from "@depict-ai/types/api/FeaturedInResponseV3";
13
13
  import { SearchFilter as _SearchFilter3 } from "@depict-ai/types/api/SearchRequestV2";
14
- export const version = "4.10.20";
14
+ export const version = "4.10.21";
15
15
  /**
16
16
  * Use `javascript_media_query` but return a reactive accessor instead of taking a callback function
17
17
  */
@@ -485,6 +485,10 @@ export class PseudoRouter {
485
485
  * We store listing_id in history.state and do operate on this both here and in js-ui, this is the key to be used which js-ui provides
486
486
  */
487
487
  listing_query_state_key: string | undefined;
488
+ /**
489
+ * Called when a go_to_ didn't change location.href, for example when following a link to the page one is already on. href_change_ipns stays silent then, so whoever waits for a href change to react to a navigation has to listen here too
490
+ */
491
+ navigation_to_same_href_listeners_: Set<VoidFunction>;
488
492
  navigate_: {
489
493
  /**
490
494
  * Call this if you *just* want to replace the state object in history.state
@@ -7,7 +7,7 @@ import { createStore as $i7OB2$createStore, reconcile as $i7OB2$reconcile } from
7
7
  /*! Depict.ai search and product listing SDK */
8
8
  import { register_es_version } from "../../multi_import_deduplicator.mjs";
9
9
  register_es_version("ui", "latest");
10
- const $ec291e0010116877$export$83d89fbfd8236492 = "4.10.20";
10
+ const $ec291e0010116877$export$83d89fbfd8236492 = "4.10.21";
11
11
  function $dc9262ef6f786812$export$d9f36cf74e24d595(media_query) {
12
12
  const signal = $i7OB2$createSignal(false);
13
13
  $i7OB2$onCleanup($i7OB2$javascript_media_query(media_query, ({
@@ -2020,6 +2020,61 @@ function $1adfe60f3a14a1d7$export$fecbb157621501bd({
2020
2020
  }));
2021
2021
  return outmost_element;
2022
2022
  }
2023
+
2024
+ /**
2025
+ * `HTMLElement.focus()` is a **silent no-op** on an element that is not being rendered
2026
+ * (`display: none`, `visibility: hidden`, a collapsed menu). Nothing throws, focus simply
2027
+ * stays where it was, so a caller that does not check believes it restored focus when it
2028
+ * left it on `<body>` — which puts the keyboard user back at the top of the document.
2029
+ *
2030
+ * That is reachable whenever the element which opened a modal stops being rendered while the
2031
+ * modal is open, and the common way for that to happen is the responsive-duplicate header:
2032
+ * a theme renders the same search trigger twice, once for desktop and once for mobile, and
2033
+ * hides one at the current breakpoint. Open the modal from the desktop trigger, cross the
2034
+ * breakpoint (rotate a tablet, resize a window), close the modal, and the remembered trigger
2035
+ * is still `isConnected` but no longer rendered. Verified against a live merchant storefront
2036
+ * on 4.10.19.
2037
+ *
2038
+ * So: verify that focus landed, and when it did not, look for a rendered twin of the same
2039
+ * control — the other half of the responsive pair — and use that instead.
2040
+ */ /**
2041
+ * An element can only take focus if it is actually being rendered.
2042
+ *
2043
+ * Injectable because it is the one part of this module that needs a layout engine: jsdom
2044
+ * reports no client rects for anything, so the specs pass their own predicate to exercise the
2045
+ * matching rules. The real predicate is only exercised in a browser.
2046
+ */
2047
+ const $07001c226aedc5fb$export$e0652711c614c755 = element => !!element.getClientRects().length;
2048
+ /**
2049
+ * `document.activeElement` is the outermost shadow host when focus is inside a shadow tree,
2050
+ * so ask the element's own root rather than the document — matching how the rest of
2051
+ * modal_opener treats shadow-mounted modals.
2052
+ */
2053
+ const $07001c226aedc5fb$var$has_focus = element => element.getRootNode().activeElement === element;
2054
+ /**
2055
+ * The accessible-name-ish identity we match twins on. Deliberately narrow: an explicit label
2056
+ * or the trimmed text. Two controls that share a tag and this string are the same control
2057
+ * rendered twice, which is exactly the responsive-duplicate case.
2058
+ */
2059
+ const $07001c226aedc5fb$var$identity_of = element => (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "").trim().replace(/\s+/g, " ");
2060
+ function $07001c226aedc5fb$export$5800d9d535a06e96(element, rendered = $07001c226aedc5fb$export$e0652711c614c755) {
2061
+ const identity = $07001c226aedc5fb$var$identity_of(element);
2062
+ if (!identity) return; // nothing to match on — an icon-only button with no label
2063
+ const twins = [...element.ownerDocument.querySelectorAll(element.localName)].filter(candidate => candidate !== element && rendered(candidate) && $07001c226aedc5fb$var$identity_of(candidate) === identity);
2064
+ return twins.length === 1 ? twins[0] : undefined;
2065
+ }
2066
+ function $07001c226aedc5fb$export$51461d633c351649(element, rendered = $07001c226aedc5fb$export$e0652711c614c755) {
2067
+ element.focus({
2068
+ preventScroll: true
2069
+ });
2070
+ if ($07001c226aedc5fb$var$has_focus(element)) return true;
2071
+ const twin = $07001c226aedc5fb$export$5800d9d535a06e96(element, rendered);
2072
+ if (!twin) return false;
2073
+ twin.focus({
2074
+ preventScroll: true
2075
+ });
2076
+ return $07001c226aedc5fb$var$has_focus(twin);
2077
+ }
2023
2078
  async function $194bd6b8e4c59830$export$79b1565500412d9b(search_modal, modal_fn_opts, parent) {
2024
2079
  const body = parent || document.body || (await (0, $i7OB2$observer).wait_for_element("body"));
2025
2080
  let dispose_and_remove;
@@ -2080,9 +2135,12 @@ async function $194bd6b8e4c59830$export$79b1565500412d9b(search_modal, modal_fn_
2080
2135
  });
2081
2136
  focused = requested_focus_target.getRootNode().activeElement === requested_focus_target;
2082
2137
  }
2083
- if (!focused && element_focused_before_open?.isConnected) element_focused_before_open.focus({
2084
- preventScroll: true
2085
- });
2138
+ if (!focused && element_focused_before_open?.isConnected)
2139
+ // restore_focus_to rather than a bare focus(): the remembered trigger can be
2140
+ // connected but no longer rendered (a responsive-duplicate header whose other
2141
+ // half is showing now), and focus() on an unrendered element silently does
2142
+ // nothing, leaving the keyboard user at the top of the page.
2143
+ $07001c226aedc5fb$export$51461d633c351649(element_focused_before_open);
2086
2144
  } finally {
2087
2145
  // Cleared on a task instead of synchronously in case a browser delivers the focus event async
2088
2146
  setTimeout(() => restoring_focus = false, 0);
@@ -7487,16 +7545,20 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7487
7545
  * Function that will be called when we want to navigate, or a string saying it's not an SPA. You can change this OTG
7488
7546
  */ /**
7489
7547
  * We store listing_id in history.state and do operate on this both here and in js-ui, this is the key to be used which js-ui provides
7490
- */
7491
- #throwing_on_navigation(...args) {
7548
+ */ /**
7549
+ * Called when a go_to_ didn't change location.href, for example when following a link to the page one is already on. href_change_ipns stays silent then, so whoever waits for a href change to react to a navigation has to listen here too
7550
+ */
7551
+ navigation_to_same_href_listeners_ = new Set();
7552
+ #throwing_on_navigation(options, on_throw_) {
7492
7553
  const {
7493
7554
  on_navigation_: on_navigation_
7494
7555
  } = this;
7495
7556
  if (typeof on_navigation_ !== "function") throw new Error("on_navigation_ is not a function");
7496
7557
  // make sure that we don't accidentally catchify if the SDK consumer has fucked up
7497
7558
  try {
7498
- return on_navigation_(...args);
7559
+ return on_navigation_(options);
7499
7560
  } catch (e) {
7561
+ on_throw_?.();
7500
7562
  queueMicrotask(() => {
7501
7563
  throw e;
7502
7564
  });
@@ -7517,6 +7579,28 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7517
7579
  (0, $i7OB2$instant_exec_on_suspect_history_change).add(handler);
7518
7580
  });
7519
7581
  }
7582
+ /**
7583
+ * Calls navigation_to_same_href_listeners_ if the navigation we're about to request ends on the href we're already on
7584
+ * @returns function to stop watching, call it when the navigation failed
7585
+ */
7586
+ #watch_for_navigation_to_same_href(target_url) {
7587
+ const href_before = location.href;
7588
+ const stop_watching = () => {
7589
+ (0, $i7OB2$instant_exec_on_suspect_history_change).delete(on_history_change);
7590
+ clearTimeout(timeout);
7591
+ };
7592
+ const notify_if_href_unchanged = $i7OB2$catchify1(() => {
7593
+ stop_watching();
7594
+ if (location.href !== href_before) return;
7595
+ this.navigation_to_same_href_listeners_.forEach(listener => $i7OB2$catchify1(listener)());
7596
+ });
7597
+ // The router has acted once it touches history, if the href is still the same then it took us to the page we're on
7598
+ const on_history_change = () => notify_if_href_unchanged();
7599
+ // A router asked to go to the exact href we're on might not touch history at all, so don't wait longer than 500ms for it. For any other target we can't tell a slow navigation from one that goes nowhere, so only stop listening after a while
7600
+ const timeout = target_url.href === href_before ? setTimeout(notify_if_href_unchanged, 500) : setTimeout(stop_watching, 10000);
7601
+ (0, $i7OB2$instant_exec_on_suspect_history_change).add(on_history_change);
7602
+ return stop_watching;
7603
+ }
7520
7604
  navigate_ = {
7521
7605
  /**
7522
7606
  * Call this if you *just* want to replace the state object in history.state
@@ -7556,6 +7640,7 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7556
7640
  }
7557
7641
  event_?.preventDefault();
7558
7642
  const promise = this.#get_navigation_happened_promise(true);
7643
+ const stop_watching_for_same_href = this.#watch_for_navigation_to_same_href(new_url_);
7559
7644
  if (force_spa_navigation_ && wants_hard_navigation) {
7560
7645
  // Don't allow hard navigation if we're forcing SPA navigation, which we do for things where PageReplacer can be used for performance
7561
7646
  const new_state = history.state;
@@ -7569,7 +7654,7 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7569
7654
  is_replace: is_replace_,
7570
7655
  new_url: new_url_ instanceof URL ? new_url_ : new URL(new_url_, location.href),
7571
7656
  scroll: scroll
7572
- });
7657
+ }, stop_watching_for_same_href);
7573
7658
  await promise;
7574
7659
  })
7575
7660
  };
@@ -8867,11 +8952,17 @@ var $ffb84ffbf4014982$var$_tmpl$ = ["<svg", ' xmlns="http://www.w3.org/2000/svg"
8867
8952
  function $ffb84ffbf4014982$export$8d8893ffd8aca39a() {
8868
8953
  return $i7OB2$ssr($ffb84ffbf4014982$var$_tmpl$, $i7OB2$ssrHydrationKey());
8869
8954
  }
8870
- function $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_) {
8955
+ function $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_, router_) {
8871
8956
  const [exit_iterable, exitable_iterable] = $i7OB2$make_asyncIterable_exiter((0, $i7OB2$href_change_ipns));
8957
+ const {
8958
+ navigation_to_same_href_listeners_: navigation_to_same_href_listeners_
8959
+ } = router_;
8872
8960
  let set_initial_values = false;
8873
8961
  let old_pathname;
8874
8962
  let old_param_value;
8963
+ // A card linking to the page one is already on doesn't change the href, close the modal anyway so the click does something
8964
+ navigation_to_same_href_listeners_.add(close_modal);
8965
+ $i7OB2$onCleanup(() => navigation_to_same_href_listeners_.delete(close_modal));
8875
8966
  $i7OB2$onCleanup($i7OB2$catchify1(exit_iterable));
8876
8967
  $i7OB2$catchify1(async () => {
8877
8968
  for await (const href of exitable_iterable) {
@@ -9016,7 +9107,7 @@ function $42912431795f188e$var$RawClassicSearchModal({
9016
9107
  }
9017
9108
  })));
9018
9109
  if (disable_scrolling_) $aea1edf92433e81e$export$fe334901e8bebd57();
9019
- $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9110
+ $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9020
9111
  window.addEventListener("keydown", close_on_escape_handler);
9021
9112
  $i7OB2$onCleanup($i7OB2$catchify1(() => window.removeEventListener("keydown", close_on_escape_handler)));
9022
9113
  if (closing_animation_) register_closing_animation_(closing_animation_);
@@ -9709,7 +9800,7 @@ function $500a16dd18ee5a71$var$RawSearchModalV2({
9709
9800
  });
9710
9801
  const noResults = $i7OB2$createMemo(prev => (get_modal_search_results_()?.loading === false && !suggestions_response_.loading || !!prev) && totalNumberResults() === 0 && previous_searches_matching_query().length === 0);
9711
9802
  if (disable_scrolling_) $aea1edf92433e81e$export$fe334901e8bebd57();
9712
- $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9803
+ $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9713
9804
  addEventListener("keydown", close_on_escape_handler);
9714
9805
  $i7OB2$onCleanup(() => removeEventListener("keydown", close_on_escape_handler));
9715
9806
  $i7OB2$createEffect($i7OB2$on(get_search_field_value_, () => {