@depict-ai/ui 4.10.20 → 4.10.22

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.22";
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 => {
@@ -941,8 +941,15 @@ function $45a8953d760b5e58$export$a1fd619a4743ca08(_ref) {
941
941
  setFirstResultInViewport(Math.min(...intersecting) + 1); // +1 because we want to start counting at 1
942
942
  setLastResultsInViewport(Math.max(...intersecting) + 1);
943
943
  });
944
- return renderedDisplays_;
944
+ // Content blocks placed after the last product (but still in its row) have no product card to render in front of, so render them after the cards once there are no more products to load
945
+ const trailingContentBlocks_ = $b8hua$createMemo(() => {
946
+ var _content_blocks_, _content_blocks_$trai;
947
+ if (!all_products_loaded_[0]()) return [];
948
+ return (content_blocks_ === null || content_blocks_ === void 0 ? void 0 : (_content_blocks_ = content_blocks_()) === null || _content_blocks_ === void 0 ? void 0 : (_content_blocks_$trai = _content_blocks_.trailing_) === null || _content_blocks_$trai === void 0 ? void 0 : _content_blocks_$trai.call(_content_blocks_, displays_by_key_().size)) || [];
949
+ });
950
+ return [renderedDisplays_, trailingContentBlocks_]; // Regarding the as unknown: https://github.com/solidjs/solid/releases/tag/v1.7.0
945
951
  }
952
+
946
953
  var $63f1f8dbcd07d3b0$var$_tmpl$ = ["<div", ' class="', '">', "</div>"],
947
954
  $63f1f8dbcd07d3b0$var$_tmpl$2 = ["<style", ' type="text/css">', "</style>"];
948
955
  function $63f1f8dbcd07d3b0$export$d21e32dfedac04be(props) {
@@ -1260,10 +1267,18 @@ function $ff7feae4dddd6073$export$791ef57e62e21c3f(_ref) {
1260
1267
  set_top_row_shortened_by_(blocks_in_top_row_taken - 1); // minus one because the content block wrapper also is an element
1261
1268
  });
1262
1269
 
1263
- return $b8hua$createMemo(() => $ff7feae4dddd6073$export$92aa5d691d588f3f({
1270
+ const remapped_ = $b8hua$createMemo(() => $ff7feae4dddd6073$export$92aa5d691d588f3f({
1264
1271
  wrapped_blocks_: get_wrapped_blocks_(),
1265
1272
  n_cols_currently_showing_: n_cols_currently_showing_()
1266
- }).content_blocks_by_index_);
1273
+ }));
1274
+ const content_blocks_by_index_ = $b8hua$createMemo(() => remapped_().content_blocks_by_index_);
1275
+ return Object.assign(content_blocks_by_index_, {
1276
+ trailing_: product_count => $ff7feae4dddd6073$export$b94605e5b3ce01c({
1277
+ ...remapped_(),
1278
+ n_cols_currently_showing_: n_cols_currently_showing_(),
1279
+ product_count: product_count
1280
+ })
1281
+ });
1267
1282
  }
1268
1283
  function $ff7feae4dddd6073$export$92aa5d691d588f3f(_ref2) {
1269
1284
  let {
@@ -1272,13 +1287,16 @@ function $ff7feae4dddd6073$export$92aa5d691d588f3f(_ref2) {
1272
1287
  } = _ref2;
1273
1288
  // This computation should be quite cheap since it just moves around things into an array
1274
1289
  const content_blocks_by_index_ = [];
1290
+ const rows_by_index_ = []; // the row of each block at each index of content_blocks_by_index_, in the same order
1275
1291
  const constructed_block_arrays = new WeakSet();
1276
1292
  // Need to keep track of products displaced by blocks above when mapping rows to indexes in the actual grid
1277
1293
  const displaced_products_ = []; // undefined = is product, true = is content block if number instead of tuple number indicates content blocks in that row. Index in first array is row, index in second array is column
1278
1294
  if (!n_cols_currently_showing_)
1279
1295
  // Nope out of here if we don't know how many columns are showing
1280
1296
  return {
1281
- content_blocks_by_index_: content_blocks_by_index_
1297
+ content_blocks_by_index_: content_blocks_by_index_,
1298
+ rows_by_index_: rows_by_index_,
1299
+ displaced_products_: displaced_products_
1282
1300
  };
1283
1301
  for (let i = 0; i < wrapped_blocks_.length; i++) {
1284
1302
  // i = row to show at
@@ -1326,6 +1344,7 @@ function $ff7feae4dddd6073$export$92aa5d691d588f3f(_ref2) {
1326
1344
  content_blocks_by_index_[final_target_index] = array;
1327
1345
  }
1328
1346
  } else content_blocks_by_index_[final_target_index] = block_content;
1347
+ (rows_by_index_[final_target_index] || (rows_by_index_[final_target_index] = [])).push(i);
1329
1348
  // Update displacement
1330
1349
  for (let j = 0; j < span_rows; j++) {
1331
1350
  var _ref3;
@@ -1335,9 +1354,46 @@ function $ff7feae4dddd6073$export$92aa5d691d588f3f(_ref2) {
1335
1354
  }
1336
1355
  return {
1337
1356
  content_blocks_by_index_: content_blocks_by_index_,
1357
+ rows_by_index_: rows_by_index_,
1338
1358
  displaced_products_: displaced_products_
1339
1359
  };
1340
1360
  }
1361
+ function $ff7feae4dddd6073$export$b94605e5b3ce01c(_ref4) {
1362
+ let {
1363
+ content_blocks_by_index_: content_blocks_by_index_,
1364
+ rows_by_index_: rows_by_index_,
1365
+ displaced_products_: displaced_products_,
1366
+ n_cols_currently_showing_: n_cols_currently_showing_,
1367
+ product_count: product_count
1368
+ } = _ref4;
1369
+ const trailing = [];
1370
+ if (!n_cols_currently_showing_ || product_count <= 0) return trailing;
1371
+ // Find the row the last product lands in: each row has room for the columns that blocks don't take up
1372
+ let last_product_row = 0;
1373
+ for (let seen_products = 0;; last_product_row++) {
1374
+ const row = displaced_products_[last_product_row];
1375
+ const taken_by_blocks = typeof row === "number" ? row : row ? row.filter(item => item).length : 0;
1376
+ seen_products += Math.max(0, n_cols_currently_showing_ - taken_by_blocks);
1377
+ if (seen_products >= product_count) break;
1378
+ // Rows past the ones blocks take up all have room for products, so we're done by then at the latest
1379
+ if (last_product_row >= displaced_products_.length + product_count) break;
1380
+ }
1381
+ for (let i = product_count; i < content_blocks_by_index_.length; i++) {
1382
+ var _rows$;
1383
+ const block = content_blocks_by_index_[i];
1384
+ if (block === undefined) continue;
1385
+ const rows = rows_by_index_[i] || [];
1386
+ if (Array.isArray(block) && rows.length > 1) {
1387
+ // Several blocks share this index (e.g. the end of one row and the start of the next): keep the ones in the last product's row
1388
+ const in_last_row = block.filter((_, j) => rows[j] <= last_product_row);
1389
+ if (in_last_row.length) trailing.push(in_last_row);
1390
+ continue;
1391
+ }
1392
+ if (((_rows$ = rows[0]) !== null && _rows$ !== void 0 ? _rows$ : Infinity) > last_product_row) continue; // a row below the last product's row: keep it hidden
1393
+ trailing.push(block);
1394
+ }
1395
+ return trailing;
1396
+ }
1341
1397
  /**
1342
1398
  * Takes a store that contains an array from where index is number of row and value is the result of executing the content block rendering function.
1343
1399
  * Then renders the content and wraps it into a div with the correct styling.
@@ -1370,6 +1426,7 @@ function $ff7feae4dddd6073$var$wrap_blocks_in_div(rendered_content_blocks, max_s
1370
1426
  content: () => {
1371
1427
  // Sneaky on-demand rendering, so we don't render more blocks than we can show: we have the rendered value here and don't return a function that renders because if our block would be moved around we'd be guaranteed to re-render
1372
1428
  // However, no need to count products to know if we should render: the <For> loops in Placeholders and render_displays will only call this function if there's actually a product here that wants to display this block
1429
+ // (or, once all products have loaded, ResultsProductCards renders the blocks placed after the last product)
1373
1430
  set_rendering_delayed_(false);
1374
1431
  return content;
1375
1432
  },
@@ -2113,6 +2170,63 @@ function $4711a82d08d2021b$export$fecbb157621501bd(_ref) {
2113
2170
  }));
2114
2171
  return outmost_element;
2115
2172
  }
2173
+
2174
+ /**
2175
+ * `HTMLElement.focus()` is a **silent no-op** on an element that is not being rendered
2176
+ * (`display: none`, `visibility: hidden`, a collapsed menu). Nothing throws, focus simply
2177
+ * stays where it was, so a caller that does not check believes it restored focus when it
2178
+ * left it on `<body>` — which puts the keyboard user back at the top of the document.
2179
+ *
2180
+ * That is reachable whenever the element which opened a modal stops being rendered while the
2181
+ * modal is open, and the common way for that to happen is the responsive-duplicate header:
2182
+ * a theme renders the same search trigger twice, once for desktop and once for mobile, and
2183
+ * hides one at the current breakpoint. Open the modal from the desktop trigger, cross the
2184
+ * breakpoint (rotate a tablet, resize a window), close the modal, and the remembered trigger
2185
+ * is still `isConnected` but no longer rendered. Verified against a live merchant storefront
2186
+ * on 4.10.19.
2187
+ *
2188
+ * So: verify that focus landed, and when it did not, look for a rendered twin of the same
2189
+ * control — the other half of the responsive pair — and use that instead.
2190
+ */ /**
2191
+ * An element can only take focus if it is actually being rendered.
2192
+ *
2193
+ * Injectable because it is the one part of this module that needs a layout engine: jsdom
2194
+ * reports no client rects for anything, so the specs pass their own predicate to exercise the
2195
+ * matching rules. The real predicate is only exercised in a browser.
2196
+ */
2197
+ const $1c5106dfe72d554c$export$e0652711c614c755 = element => !!element.getClientRects().length;
2198
+ /**
2199
+ * `document.activeElement` is the outermost shadow host when focus is inside a shadow tree,
2200
+ * so ask the element's own root rather than the document — matching how the rest of
2201
+ * modal_opener treats shadow-mounted modals.
2202
+ */
2203
+ const $1c5106dfe72d554c$var$has_focus = element => element.getRootNode().activeElement === element;
2204
+ /**
2205
+ * The accessible-name-ish identity we match twins on. Deliberately narrow: an explicit label
2206
+ * or the trimmed text. Two controls that share a tag and this string are the same control
2207
+ * rendered twice, which is exactly the responsive-duplicate case.
2208
+ */
2209
+ const $1c5106dfe72d554c$var$identity_of = element => (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "").trim().replace(/\s+/g, " ");
2210
+ function $1c5106dfe72d554c$export$5800d9d535a06e96(element) {
2211
+ let rendered = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : $1c5106dfe72d554c$export$e0652711c614c755;
2212
+ const identity = $1c5106dfe72d554c$var$identity_of(element);
2213
+ if (!identity) return; // nothing to match on — an icon-only button with no label
2214
+ const twins = [...element.ownerDocument.querySelectorAll(element.localName)].filter(candidate => candidate !== element && rendered(candidate) && $1c5106dfe72d554c$var$identity_of(candidate) === identity);
2215
+ return twins.length === 1 ? twins[0] : undefined;
2216
+ }
2217
+ function $1c5106dfe72d554c$export$51461d633c351649(element) {
2218
+ let rendered = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : $1c5106dfe72d554c$export$e0652711c614c755;
2219
+ element.focus({
2220
+ preventScroll: true
2221
+ });
2222
+ if ($1c5106dfe72d554c$var$has_focus(element)) return true;
2223
+ const twin = $1c5106dfe72d554c$export$5800d9d535a06e96(element, rendered);
2224
+ if (!twin) return false;
2225
+ twin.focus({
2226
+ preventScroll: true
2227
+ });
2228
+ return $1c5106dfe72d554c$var$has_focus(twin);
2229
+ }
2116
2230
  async function $d5f1e6a0f9117286$export$79b1565500412d9b(search_modal, modal_fn_opts, parent) {
2117
2231
  const body = parent || document.body || (await (0, $b8hua$observer).wait_for_element("body"));
2118
2232
  let dispose_and_remove;
@@ -2178,9 +2292,12 @@ async function $d5f1e6a0f9117286$export$79b1565500412d9b(search_modal, modal_fn_
2178
2292
  });
2179
2293
  focused = requested_focus_target.getRootNode().activeElement === requested_focus_target;
2180
2294
  }
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
- });
2295
+ if (!focused && element_focused_before_open !== null && element_focused_before_open !== void 0 && element_focused_before_open.isConnected)
2296
+ // restore_focus_to rather than a bare focus(): the remembered trigger can be
2297
+ // connected but no longer rendered (a responsive-duplicate header whose other
2298
+ // half is showing now), and focus() on an unrendered element silently does
2299
+ // nothing, leaving the keyboard user at the top of the page.
2300
+ $1c5106dfe72d554c$export$51461d633c351649(element_focused_before_open);
2184
2301
  } finally {
2185
2302
  // Cleared on a task instead of synchronously in case a browser delivers the focus event async
2186
2303
  setTimeout(() => restoring_focus = false, 0);
@@ -7939,20 +8056,28 @@ function $ac571292d606f30b$var$_classPrivateMethodGet(receiver, privateSet, fn)
7939
8056
  }
7940
8057
  var $ac571292d606f30b$var$_throwing_on_navigation = /*#__PURE__*/new WeakSet();
7941
8058
  var $ac571292d606f30b$var$_get_navigation_happened_promise = /*#__PURE__*/new WeakSet();
8059
+ var $ac571292d606f30b$var$_watch_for_navigation_to_same_href = /*#__PURE__*/new WeakSet();
7942
8060
  class $ac571292d606f30b$export$7e1297a3a19f10d8 {
7943
8061
  /**
7944
8062
  * 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
8063
  */ /**
7946
8064
  * 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
8065
  */ /**
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
- */
8066
+ * 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
8067
+ */ /**
8068
+ * Vue has async router that needs to be awaited (it navigates after a microtask)
8069
+ * This standardises waiting for all kinds of routers
8070
+ */ /**
8071
+ * Calls navigation_to_same_href_listeners_ if the navigation we're about to request ends on the href we're already on
8072
+ * @returns function to stop watching, call it when the navigation failed
8073
+ */
7951
8074
  constructor(on_navigation, listing_query_state_key) {
8075
+ $ac571292d606f30b$var$_classPrivateMethodInitSpec(this, $ac571292d606f30b$var$_watch_for_navigation_to_same_href);
7952
8076
  $ac571292d606f30b$var$_classPrivateMethodInitSpec(this, $ac571292d606f30b$var$_get_navigation_happened_promise);
7953
8077
  $ac571292d606f30b$var$_classPrivateMethodInitSpec(this, $ac571292d606f30b$var$_throwing_on_navigation);
7954
8078
  $b8hua$babelruntimehelpersdefineProperty(this, "on_navigation_", void 0);
7955
8079
  $b8hua$babelruntimehelpersdefineProperty(this, "listing_query_state_key", void 0);
8080
+ $b8hua$babelruntimehelpersdefineProperty(this, "navigation_to_same_href_listeners_", new Set());
7956
8081
  $b8hua$babelruntimehelpersdefineProperty(this, "navigate_", {
7957
8082
  /**
7958
8083
  * Call this if you *just* want to replace the state object in history.state
@@ -7994,6 +8119,7 @@ class $ac571292d606f30b$export$7e1297a3a19f10d8 {
7994
8119
  }
7995
8120
  event_ === null || event_ === void 0 || event_.preventDefault();
7996
8121
  const promise = $ac571292d606f30b$var$_classPrivateMethodGet(this, $ac571292d606f30b$var$_get_navigation_happened_promise, $ac571292d606f30b$var$_get_navigation_happened_promise2).call(this, true);
8122
+ 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
8123
  if (force_spa_navigation_ && wants_hard_navigation) {
7998
8124
  // Don't allow hard navigation if we're forcing SPA navigation, which we do for things where PageReplacer can be used for performance
7999
8125
  const new_state = history.state;
@@ -8007,7 +8133,7 @@ class $ac571292d606f30b$export$7e1297a3a19f10d8 {
8007
8133
  is_replace: is_replace_,
8008
8134
  new_url: new_url_ instanceof URL ? new_url_ : new URL(new_url_, location.href),
8009
8135
  scroll: scroll
8010
- });
8136
+ }, stop_watching_for_same_href);
8011
8137
  await promise;
8012
8138
  })
8013
8139
  });
@@ -8015,15 +8141,16 @@ class $ac571292d606f30b$export$7e1297a3a19f10d8 {
8015
8141
  this.on_navigation_ = on_navigation;
8016
8142
  }
8017
8143
  }
8018
- function $ac571292d606f30b$var$_throwing_on_navigation2() {
8144
+ function $ac571292d606f30b$var$_throwing_on_navigation2(options, on_throw_) {
8019
8145
  const {
8020
8146
  on_navigation_: on_navigation_
8021
8147
  } = this;
8022
8148
  if (typeof on_navigation_ !== "function") throw new Error("on_navigation_ is not a function");
8023
8149
  // make sure that we don't accidentally catchify if the SDK consumer has fucked up
8024
8150
  try {
8025
- return on_navigation_(...arguments);
8151
+ return on_navigation_(options);
8026
8152
  } catch (e) {
8153
+ on_throw_ === null || on_throw_ === void 0 || on_throw_();
8027
8154
  queueMicrotask(() => {
8028
8155
  throw e;
8029
8156
  });
@@ -8040,6 +8167,24 @@ function $ac571292d606f30b$var$_get_navigation_happened_promise2(is_replace) {
8040
8167
  (0, $b8hua$instant_exec_on_suspect_history_change).add(handler);
8041
8168
  });
8042
8169
  }
8170
+ function $ac571292d606f30b$var$_watch_for_navigation_to_same_href2(target_url) {
8171
+ const href_before = location.href;
8172
+ const stop_watching = () => {
8173
+ (0, $b8hua$instant_exec_on_suspect_history_change).delete(on_history_change);
8174
+ clearTimeout(timeout);
8175
+ };
8176
+ const notify_if_href_unchanged = $b8hua$catchify1(() => {
8177
+ stop_watching();
8178
+ if (location.href !== href_before) return;
8179
+ this.navigation_to_same_href_listeners_.forEach(listener => $b8hua$catchify1(listener)());
8180
+ });
8181
+ // 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
8182
+ const on_history_change = () => notify_if_href_unchanged();
8183
+ // 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
8184
+ const timeout = target_url.href === href_before ? setTimeout(notify_if_href_unchanged, 500) : setTimeout(stop_watching, 10000);
8185
+ (0, $b8hua$instant_exec_on_suspect_history_change).add(on_history_change);
8186
+ return stop_watching;
8187
+ }
8043
8188
  var $e3efeb558d952696$var$_tmpl$ = ["<button", ' class="show-more major" type="button">', "</button>"],
8044
8189
  $e3efeb558d952696$var$_tmpl$2 = ["<h2", ' class="recs-title">', "</h2>"],
8045
8190
  $e3efeb558d952696$var$_tmpl$3 = ["<div", ' class="recommendation-grid"><!--$-->', "<!--/--><!--$-->", "<!--/--></div>"];
@@ -9412,11 +9557,17 @@ var $fac58b5610eec6a9$var$_tmpl$ = ["<svg", ' xmlns="http://www.w3.org/2000/svg"
9412
9557
  function $fac58b5610eec6a9$export$8d8893ffd8aca39a() {
9413
9558
  return $b8hua$ssr($fac58b5610eec6a9$var$_tmpl$, $b8hua$ssrHydrationKey());
9414
9559
  }
9415
- function $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_) {
9560
+ function $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_, router_) {
9416
9561
  const [exit_iterable, exitable_iterable] = $b8hua$make_asyncIterable_exiter((0, $b8hua$href_change_ipns));
9562
+ const {
9563
+ navigation_to_same_href_listeners_: navigation_to_same_href_listeners_
9564
+ } = router_;
9417
9565
  let set_initial_values = false;
9418
9566
  let old_pathname;
9419
9567
  let old_param_value;
9568
+ // A card linking to the page one is already on doesn't change the href, close the modal anyway so the click does something
9569
+ navigation_to_same_href_listeners_.add(close_modal);
9570
+ $b8hua$onCleanup(() => navigation_to_same_href_listeners_.delete(close_modal));
9420
9571
  $b8hua$onCleanup($b8hua$catchify1(exit_iterable));
9421
9572
  $b8hua$catchify1(async () => {
9422
9573
  for await (const href of exitable_iterable) {
@@ -9566,7 +9717,7 @@ function $40411f2ec772c376$var$RawClassicSearchModal(_ref) {
9566
9717
  }
9567
9718
  })));
9568
9719
  if (disable_scrolling_) $e56d241d0f33d459$export$fe334901e8bebd57();
9569
- $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9720
+ $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9570
9721
  window.addEventListener("keydown", close_on_escape_handler);
9571
9722
  $b8hua$onCleanup($b8hua$catchify1(() => window.removeEventListener("keydown", close_on_escape_handler)));
9572
9723
  if (closing_animation_) register_closing_animation_(closing_animation_);
@@ -10299,7 +10450,7 @@ function $06fcd2a345efd24d$var$RawSearchModalV2(_ref) {
10299
10450
  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
10451
  });
10301
10452
  if (disable_scrolling_) $e56d241d0f33d459$export$fe334901e8bebd57();
10302
- $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
10453
+ $a82a41ab7188eda2$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
10303
10454
  addEventListener("keydown", close_on_escape_handler);
10304
10455
  $b8hua$onCleanup(() => removeEventListener("keydown", close_on_escape_handler));
10305
10456
  $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.22";
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, ({
@@ -1001,8 +1001,14 @@ function $ac167f6af6a48fc3$export$a1fd619a4743ca08({
1001
1001
  setFirstResultInViewport(Math.min(...intersecting) + 1); // +1 because we want to start counting at 1
1002
1002
  setLastResultsInViewport(Math.max(...intersecting) + 1);
1003
1003
  });
1004
- return renderedDisplays_;
1004
+ // Content blocks placed after the last product (but still in its row) have no product card to render in front of, so render them after the cards once there are no more products to load
1005
+ const trailingContentBlocks_ = $5vyXB$solidjs.createMemo(() => {
1006
+ if (!all_products_loaded_[0]()) return [];
1007
+ return content_blocks_?.()?.trailing_?.(displays_by_key_().size) || [];
1008
+ });
1009
+ return [renderedDisplays_, trailingContentBlocks_]; // Regarding the as unknown: https://github.com/solidjs/solid/releases/tag/v1.7.0
1005
1010
  }
1011
+
1006
1012
  var $f48d0fbb00835aaa$var$_tmpl$ = ["<div", ' class="', '">', "</div>"],
1007
1013
  $f48d0fbb00835aaa$var$_tmpl$2 = ["<style", ' type="text/css">', "</style>"];
1008
1014
  function $f48d0fbb00835aaa$export$d21e32dfedac04be(props) {
@@ -1303,10 +1309,18 @@ function $d92f338ba8f9da9b$export$791ef57e62e21c3f({
1303
1309
  set_top_row_shortened_by_(blocks_in_top_row_taken - 1); // minus one because the content block wrapper also is an element
1304
1310
  });
1305
1311
 
1306
- return $5vyXB$solidjs.createMemo(() => $d92f338ba8f9da9b$export$92aa5d691d588f3f({
1312
+ const remapped_ = $5vyXB$solidjs.createMemo(() => $d92f338ba8f9da9b$export$92aa5d691d588f3f({
1307
1313
  wrapped_blocks_: get_wrapped_blocks_(),
1308
1314
  n_cols_currently_showing_: n_cols_currently_showing_()
1309
- }).content_blocks_by_index_);
1315
+ }));
1316
+ const content_blocks_by_index_ = $5vyXB$solidjs.createMemo(() => remapped_().content_blocks_by_index_);
1317
+ return Object.assign(content_blocks_by_index_, {
1318
+ trailing_: product_count => $d92f338ba8f9da9b$export$b94605e5b3ce01c({
1319
+ ...remapped_(),
1320
+ n_cols_currently_showing_: n_cols_currently_showing_(),
1321
+ product_count: product_count
1322
+ })
1323
+ });
1310
1324
  }
1311
1325
  function $d92f338ba8f9da9b$export$92aa5d691d588f3f({
1312
1326
  wrapped_blocks_: wrapped_blocks_,
@@ -1314,13 +1328,16 @@ function $d92f338ba8f9da9b$export$92aa5d691d588f3f({
1314
1328
  }) {
1315
1329
  // This computation should be quite cheap since it just moves around things into an array
1316
1330
  const content_blocks_by_index_ = [];
1331
+ const rows_by_index_ = []; // the row of each block at each index of content_blocks_by_index_, in the same order
1317
1332
  const constructed_block_arrays = new WeakSet();
1318
1333
  // Need to keep track of products displaced by blocks above when mapping rows to indexes in the actual grid
1319
1334
  const displaced_products_ = []; // undefined = is product, true = is content block if number instead of tuple number indicates content blocks in that row. Index in first array is row, index in second array is column
1320
1335
  if (!n_cols_currently_showing_)
1321
1336
  // Nope out of here if we don't know how many columns are showing
1322
1337
  return {
1323
- content_blocks_by_index_: content_blocks_by_index_
1338
+ content_blocks_by_index_: content_blocks_by_index_,
1339
+ rows_by_index_: rows_by_index_,
1340
+ displaced_products_: displaced_products_
1324
1341
  };
1325
1342
  for (let i = 0; i < wrapped_blocks_.length; i++) {
1326
1343
  // i = row to show at
@@ -1365,6 +1382,7 @@ function $d92f338ba8f9da9b$export$92aa5d691d588f3f({
1365
1382
  content_blocks_by_index_[final_target_index] = array;
1366
1383
  }
1367
1384
  } else content_blocks_by_index_[final_target_index] = block_content;
1385
+ (rows_by_index_[final_target_index] ||= []).push(i);
1368
1386
  // Update displacement
1369
1387
  for (let j = 0; j < span_rows; j++) {
1370
1388
  const row = displaced_products_[i + j] ||= [];
@@ -1373,9 +1391,44 @@ function $d92f338ba8f9da9b$export$92aa5d691d588f3f({
1373
1391
  }
1374
1392
  return {
1375
1393
  content_blocks_by_index_: content_blocks_by_index_,
1394
+ rows_by_index_: rows_by_index_,
1376
1395
  displaced_products_: displaced_products_
1377
1396
  };
1378
1397
  }
1398
+ function $d92f338ba8f9da9b$export$b94605e5b3ce01c({
1399
+ content_blocks_by_index_: content_blocks_by_index_,
1400
+ rows_by_index_: rows_by_index_,
1401
+ displaced_products_: displaced_products_,
1402
+ n_cols_currently_showing_: n_cols_currently_showing_,
1403
+ product_count: product_count
1404
+ }) {
1405
+ const trailing = [];
1406
+ if (!n_cols_currently_showing_ || product_count <= 0) return trailing;
1407
+ // Find the row the last product lands in: each row has room for the columns that blocks don't take up
1408
+ let last_product_row = 0;
1409
+ for (let seen_products = 0;; last_product_row++) {
1410
+ const row = displaced_products_[last_product_row];
1411
+ const taken_by_blocks = typeof row === "number" ? row : row ? row.filter(item => item).length : 0;
1412
+ seen_products += Math.max(0, n_cols_currently_showing_ - taken_by_blocks);
1413
+ if (seen_products >= product_count) break;
1414
+ // Rows past the ones blocks take up all have room for products, so we're done by then at the latest
1415
+ if (last_product_row >= displaced_products_.length + product_count) break;
1416
+ }
1417
+ for (let i = product_count; i < content_blocks_by_index_.length; i++) {
1418
+ const block = content_blocks_by_index_[i];
1419
+ if (block === undefined) continue;
1420
+ const rows = rows_by_index_[i] || [];
1421
+ if (Array.isArray(block) && rows.length > 1) {
1422
+ // Several blocks share this index (e.g. the end of one row and the start of the next): keep the ones in the last product's row
1423
+ const in_last_row = block.filter((_, j) => rows[j] <= last_product_row);
1424
+ if (in_last_row.length) trailing.push(in_last_row);
1425
+ continue;
1426
+ }
1427
+ if ((rows[0] ?? Infinity) > last_product_row) continue; // a row below the last product's row: keep it hidden
1428
+ trailing.push(block);
1429
+ }
1430
+ return trailing;
1431
+ }
1379
1432
  /**
1380
1433
  * Takes a store that contains an array from where index is number of row and value is the result of executing the content block rendering function.
1381
1434
  * Then renders the content and wraps it into a div with the correct styling.
@@ -1405,6 +1458,7 @@ function $d92f338ba8f9da9b$var$wrap_blocks_in_div(rendered_content_blocks, max_s
1405
1458
  content: () => {
1406
1459
  // Sneaky on-demand rendering, so we don't render more blocks than we can show: we have the rendered value here and don't return a function that renders because if our block would be moved around we'd be guaranteed to re-render
1407
1460
  // However, no need to count products to know if we should render: the <For> loops in Placeholders and render_displays will only call this function if there's actually a product here that wants to display this block
1461
+ // (or, once all products have loaded, ResultsProductCards renders the blocks placed after the last product)
1408
1462
  set_rendering_delayed_(false);
1409
1463
  return content;
1410
1464
  },
@@ -2122,6 +2176,61 @@ function $235a2771a44e28e9$export$fecbb157621501bd({
2122
2176
  }));
2123
2177
  return outmost_element;
2124
2178
  }
2179
+
2180
+ /**
2181
+ * `HTMLElement.focus()` is a **silent no-op** on an element that is not being rendered
2182
+ * (`display: none`, `visibility: hidden`, a collapsed menu). Nothing throws, focus simply
2183
+ * stays where it was, so a caller that does not check believes it restored focus when it
2184
+ * left it on `<body>` — which puts the keyboard user back at the top of the document.
2185
+ *
2186
+ * That is reachable whenever the element which opened a modal stops being rendered while the
2187
+ * modal is open, and the common way for that to happen is the responsive-duplicate header:
2188
+ * a theme renders the same search trigger twice, once for desktop and once for mobile, and
2189
+ * hides one at the current breakpoint. Open the modal from the desktop trigger, cross the
2190
+ * breakpoint (rotate a tablet, resize a window), close the modal, and the remembered trigger
2191
+ * is still `isConnected` but no longer rendered. Verified against a live merchant storefront
2192
+ * on 4.10.19.
2193
+ *
2194
+ * So: verify that focus landed, and when it did not, look for a rendered twin of the same
2195
+ * control — the other half of the responsive pair — and use that instead.
2196
+ */ /**
2197
+ * An element can only take focus if it is actually being rendered.
2198
+ *
2199
+ * Injectable because it is the one part of this module that needs a layout engine: jsdom
2200
+ * reports no client rects for anything, so the specs pass their own predicate to exercise the
2201
+ * matching rules. The real predicate is only exercised in a browser.
2202
+ */
2203
+ const $7f80d1bc95bbbc60$export$e0652711c614c755 = element => !!element.getClientRects().length;
2204
+ /**
2205
+ * `document.activeElement` is the outermost shadow host when focus is inside a shadow tree,
2206
+ * so ask the element's own root rather than the document — matching how the rest of
2207
+ * modal_opener treats shadow-mounted modals.
2208
+ */
2209
+ const $7f80d1bc95bbbc60$var$has_focus = element => element.getRootNode().activeElement === element;
2210
+ /**
2211
+ * The accessible-name-ish identity we match twins on. Deliberately narrow: an explicit label
2212
+ * or the trimmed text. Two controls that share a tag and this string are the same control
2213
+ * rendered twice, which is exactly the responsive-duplicate case.
2214
+ */
2215
+ const $7f80d1bc95bbbc60$var$identity_of = element => (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "").trim().replace(/\s+/g, " ");
2216
+ function $7f80d1bc95bbbc60$export$5800d9d535a06e96(element, rendered = $7f80d1bc95bbbc60$export$e0652711c614c755) {
2217
+ const identity = $7f80d1bc95bbbc60$var$identity_of(element);
2218
+ if (!identity) return; // nothing to match on — an icon-only button with no label
2219
+ const twins = [...element.ownerDocument.querySelectorAll(element.localName)].filter(candidate => candidate !== element && rendered(candidate) && $7f80d1bc95bbbc60$var$identity_of(candidate) === identity);
2220
+ return twins.length === 1 ? twins[0] : undefined;
2221
+ }
2222
+ function $7f80d1bc95bbbc60$export$51461d633c351649(element, rendered = $7f80d1bc95bbbc60$export$e0652711c614c755) {
2223
+ element.focus({
2224
+ preventScroll: true
2225
+ });
2226
+ if ($7f80d1bc95bbbc60$var$has_focus(element)) return true;
2227
+ const twin = $7f80d1bc95bbbc60$export$5800d9d535a06e96(element, rendered);
2228
+ if (!twin) return false;
2229
+ twin.focus({
2230
+ preventScroll: true
2231
+ });
2232
+ return $7f80d1bc95bbbc60$var$has_focus(twin);
2233
+ }
2125
2234
  async function $9abb0a35d4b60ed3$export$79b1565500412d9b(search_modal, modal_fn_opts, parent) {
2126
2235
  const body = parent || document.body || (await (0, $5vyXB$depictaiutilishared.observer).wait_for_element("body"));
2127
2236
  let dispose_and_remove;
@@ -2182,9 +2291,12 @@ async function $9abb0a35d4b60ed3$export$79b1565500412d9b(search_modal, modal_fn_
2182
2291
  });
2183
2292
  focused = requested_focus_target.getRootNode().activeElement === requested_focus_target;
2184
2293
  }
2185
- if (!focused && element_focused_before_open?.isConnected) element_focused_before_open.focus({
2186
- preventScroll: true
2187
- });
2294
+ if (!focused && element_focused_before_open?.isConnected)
2295
+ // restore_focus_to rather than a bare focus(): the remembered trigger can be
2296
+ // connected but no longer rendered (a responsive-duplicate header whose other
2297
+ // half is showing now), and focus() on an unrendered element silently does
2298
+ // nothing, leaving the keyboard user at the top of the page.
2299
+ $7f80d1bc95bbbc60$export$51461d633c351649(element_focused_before_open);
2188
2300
  } finally {
2189
2301
  // Cleared on a task instead of synchronously in case a browser delivers the focus event async
2190
2302
  setTimeout(() => restoring_focus = false, 0);
@@ -7589,16 +7701,20 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7589
7701
  * 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
7702
  */ /**
7591
7703
  * 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) {
7704
+ */ /**
7705
+ * 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
7706
+ */
7707
+ navigation_to_same_href_listeners_ = new Set();
7708
+ #throwing_on_navigation(options, on_throw_) {
7594
7709
  const {
7595
7710
  on_navigation_: on_navigation_
7596
7711
  } = this;
7597
7712
  if (typeof on_navigation_ !== "function") throw new Error("on_navigation_ is not a function");
7598
7713
  // make sure that we don't accidentally catchify if the SDK consumer has fucked up
7599
7714
  try {
7600
- return on_navigation_(...args);
7715
+ return on_navigation_(options);
7601
7716
  } catch (e) {
7717
+ on_throw_?.();
7602
7718
  queueMicrotask(() => {
7603
7719
  throw e;
7604
7720
  });
@@ -7619,6 +7735,28 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7619
7735
  (0, $5vyXB$depictaiutilishared.instant_exec_on_suspect_history_change).add(handler);
7620
7736
  });
7621
7737
  }
7738
+ /**
7739
+ * Calls navigation_to_same_href_listeners_ if the navigation we're about to request ends on the href we're already on
7740
+ * @returns function to stop watching, call it when the navigation failed
7741
+ */
7742
+ #watch_for_navigation_to_same_href(target_url) {
7743
+ const href_before = location.href;
7744
+ const stop_watching = () => {
7745
+ (0, $5vyXB$depictaiutilishared.instant_exec_on_suspect_history_change).delete(on_history_change);
7746
+ clearTimeout(timeout);
7747
+ };
7748
+ const notify_if_href_unchanged = $5vyXB$depictaiutilishared.catchify(() => {
7749
+ stop_watching();
7750
+ if (location.href !== href_before) return;
7751
+ this.navigation_to_same_href_listeners_.forEach(listener => $5vyXB$depictaiutilishared.catchify(listener)());
7752
+ });
7753
+ // 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
7754
+ const on_history_change = () => notify_if_href_unchanged();
7755
+ // 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
7756
+ const timeout = target_url.href === href_before ? setTimeout(notify_if_href_unchanged, 500) : setTimeout(stop_watching, 10000);
7757
+ (0, $5vyXB$depictaiutilishared.instant_exec_on_suspect_history_change).add(on_history_change);
7758
+ return stop_watching;
7759
+ }
7622
7760
  navigate_ = {
7623
7761
  /**
7624
7762
  * Call this if you *just* want to replace the state object in history.state
@@ -7658,6 +7796,7 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7658
7796
  }
7659
7797
  event_?.preventDefault();
7660
7798
  const promise = this.#get_navigation_happened_promise(true);
7799
+ const stop_watching_for_same_href = this.#watch_for_navigation_to_same_href(new_url_);
7661
7800
  if (force_spa_navigation_ && wants_hard_navigation) {
7662
7801
  // Don't allow hard navigation if we're forcing SPA navigation, which we do for things where PageReplacer can be used for performance
7663
7802
  const new_state = history.state;
@@ -7671,7 +7810,7 @@ class $e96dd13889871fc0$export$7e1297a3a19f10d8 {
7671
7810
  is_replace: is_replace_,
7672
7811
  new_url: new_url_ instanceof URL ? new_url_ : new URL(new_url_, location.href),
7673
7812
  scroll: scroll
7674
- });
7813
+ }, stop_watching_for_same_href);
7675
7814
  await promise;
7676
7815
  })
7677
7816
  };
@@ -8969,11 +9108,17 @@ var $610b584d4f54699e$var$_tmpl$ = ["<svg", ' xmlns="http://www.w3.org/2000/svg"
8969
9108
  function $610b584d4f54699e$export$8d8893ffd8aca39a() {
8970
9109
  return $5vyXB$solidjsweb.ssr($610b584d4f54699e$var$_tmpl$, $5vyXB$solidjsweb.ssrHydrationKey());
8971
9110
  }
8972
- function $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_) {
9111
+ function $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_, router_) {
8973
9112
  const [exit_iterable, exitable_iterable] = $5vyXB$depictaiutilishared.make_asyncIterable_exiter((0, $5vyXB$depictaiutilishared.href_change_ipns));
9113
+ const {
9114
+ navigation_to_same_href_listeners_: navigation_to_same_href_listeners_
9115
+ } = router_;
8974
9116
  let set_initial_values = false;
8975
9117
  let old_pathname;
8976
9118
  let old_param_value;
9119
+ // A card linking to the page one is already on doesn't change the href, close the modal anyway so the click does something
9120
+ navigation_to_same_href_listeners_.add(close_modal);
9121
+ $5vyXB$solidjs.onCleanup(() => navigation_to_same_href_listeners_.delete(close_modal));
8977
9122
  $5vyXB$solidjs.onCleanup($5vyXB$depictaiutilishared.catchify(exit_iterable));
8978
9123
  $5vyXB$depictaiutilishared.catchify(async () => {
8979
9124
  for await (const href of exitable_iterable) {
@@ -9118,7 +9263,7 @@ function $44f0b2e312a9b710$var$RawClassicSearchModal({
9118
9263
  }
9119
9264
  })));
9120
9265
  if (disable_scrolling_) $ca1176b694a6ee2e$export$fe334901e8bebd57();
9121
- $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9266
+ $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9122
9267
  window.addEventListener("keydown", close_on_escape_handler);
9123
9268
  $5vyXB$solidjs.onCleanup($5vyXB$depictaiutilishared.catchify(() => window.removeEventListener("keydown", close_on_escape_handler)));
9124
9269
  if (closing_animation_) register_closing_animation_(closing_animation_);
@@ -9811,7 +9956,7 @@ function $11597ae03e703a47$var$RawSearchModalV2({
9811
9956
  });
9812
9957
  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
9958
  if (disable_scrolling_) $ca1176b694a6ee2e$export$fe334901e8bebd57();
9814
- $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9959
+ $eb3039a27db40508$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9815
9960
  addEventListener("keydown", close_on_escape_handler);
9816
9961
  $5vyXB$solidjs.onCleanup(() => removeEventListener("keydown", close_on_escape_handler));
9817
9962
  $5vyXB$solidjs.createEffect($5vyXB$solidjs.on(get_search_field_value_, () => {