@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.
@@ -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.22";
15
15
  /**
16
16
  * Use `javascript_media_query` but return a reactive accessor instead of taking a callback function
17
17
  */
@@ -425,10 +425,8 @@ export function remap_blocks_to_index<T = () => JSX.Element | Promise<JSX.Elemen
425
425
  })[];
426
426
  n_cols_currently_showing_: number | null;
427
427
  }): {
428
- content_blocks_by_index_: (Awaited<T> | T[])[];
429
- readonly displaced_products_?: undefined;
430
- } | {
431
428
  readonly content_blocks_by_index_: (Awaited<T> | T[])[];
429
+ readonly rows_by_index_: number[][];
432
430
  readonly displaced_products_: (number | (true | undefined)[])[];
433
431
  };
434
432
  /** @jsxImportSource solid-js */
@@ -485,6 +483,10 @@ export class PseudoRouter {
485
483
  * 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
484
  */
487
485
  listing_query_state_key: string | undefined;
486
+ /**
487
+ * 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
488
+ */
489
+ navigation_to_same_href_listeners_: Set<VoidFunction>;
488
490
  navigate_: {
489
491
  /**
490
492
  * 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.22";
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, ({
@@ -899,8 +899,14 @@ function $0449e7d49398c077$export$a1fd619a4743ca08({
899
899
  setFirstResultInViewport(Math.min(...intersecting) + 1); // +1 because we want to start counting at 1
900
900
  setLastResultsInViewport(Math.max(...intersecting) + 1);
901
901
  });
902
- return renderedDisplays_;
902
+ // 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
903
+ const trailingContentBlocks_ = $i7OB2$createMemo(() => {
904
+ if (!all_products_loaded_[0]()) return [];
905
+ return content_blocks_?.()?.trailing_?.(displays_by_key_().size) || [];
906
+ });
907
+ return [renderedDisplays_, trailingContentBlocks_]; // Regarding the as unknown: https://github.com/solidjs/solid/releases/tag/v1.7.0
903
908
  }
909
+
904
910
  var $c8d5ff4524619fcd$var$_tmpl$ = ["<div", ' class="', '">', "</div>"],
905
911
  $c8d5ff4524619fcd$var$_tmpl$2 = ["<style", ' type="text/css">', "</style>"];
906
912
  function $c8d5ff4524619fcd$export$d21e32dfedac04be(props) {
@@ -1201,10 +1207,18 @@ function $e7a7eb102ddc0c79$export$791ef57e62e21c3f({
1201
1207
  set_top_row_shortened_by_(blocks_in_top_row_taken - 1); // minus one because the content block wrapper also is an element
1202
1208
  });
1203
1209
 
1204
- return $i7OB2$createMemo(() => $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1210
+ const remapped_ = $i7OB2$createMemo(() => $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1205
1211
  wrapped_blocks_: get_wrapped_blocks_(),
1206
1212
  n_cols_currently_showing_: n_cols_currently_showing_()
1207
- }).content_blocks_by_index_);
1213
+ }));
1214
+ const content_blocks_by_index_ = $i7OB2$createMemo(() => remapped_().content_blocks_by_index_);
1215
+ return Object.assign(content_blocks_by_index_, {
1216
+ trailing_: product_count => $e7a7eb102ddc0c79$export$b94605e5b3ce01c({
1217
+ ...remapped_(),
1218
+ n_cols_currently_showing_: n_cols_currently_showing_(),
1219
+ product_count: product_count
1220
+ })
1221
+ });
1208
1222
  }
1209
1223
  function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1210
1224
  wrapped_blocks_: wrapped_blocks_,
@@ -1212,13 +1226,16 @@ function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1212
1226
  }) {
1213
1227
  // This computation should be quite cheap since it just moves around things into an array
1214
1228
  const content_blocks_by_index_ = [];
1229
+ const rows_by_index_ = []; // the row of each block at each index of content_blocks_by_index_, in the same order
1215
1230
  const constructed_block_arrays = new WeakSet();
1216
1231
  // Need to keep track of products displaced by blocks above when mapping rows to indexes in the actual grid
1217
1232
  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
1218
1233
  if (!n_cols_currently_showing_)
1219
1234
  // Nope out of here if we don't know how many columns are showing
1220
1235
  return {
1221
- content_blocks_by_index_: content_blocks_by_index_
1236
+ content_blocks_by_index_: content_blocks_by_index_,
1237
+ rows_by_index_: rows_by_index_,
1238
+ displaced_products_: displaced_products_
1222
1239
  };
1223
1240
  for (let i = 0; i < wrapped_blocks_.length; i++) {
1224
1241
  // i = row to show at
@@ -1263,6 +1280,7 @@ function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1263
1280
  content_blocks_by_index_[final_target_index] = array;
1264
1281
  }
1265
1282
  } else content_blocks_by_index_[final_target_index] = block_content;
1283
+ (rows_by_index_[final_target_index] ||= []).push(i);
1266
1284
  // Update displacement
1267
1285
  for (let j = 0; j < span_rows; j++) {
1268
1286
  const row = displaced_products_[i + j] ||= [];
@@ -1271,9 +1289,44 @@ function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1271
1289
  }
1272
1290
  return {
1273
1291
  content_blocks_by_index_: content_blocks_by_index_,
1292
+ rows_by_index_: rows_by_index_,
1274
1293
  displaced_products_: displaced_products_
1275
1294
  };
1276
1295
  }
1296
+ function $e7a7eb102ddc0c79$export$b94605e5b3ce01c({
1297
+ content_blocks_by_index_: content_blocks_by_index_,
1298
+ rows_by_index_: rows_by_index_,
1299
+ displaced_products_: displaced_products_,
1300
+ n_cols_currently_showing_: n_cols_currently_showing_,
1301
+ product_count: product_count
1302
+ }) {
1303
+ const trailing = [];
1304
+ if (!n_cols_currently_showing_ || product_count <= 0) return trailing;
1305
+ // Find the row the last product lands in: each row has room for the columns that blocks don't take up
1306
+ let last_product_row = 0;
1307
+ for (let seen_products = 0;; last_product_row++) {
1308
+ const row = displaced_products_[last_product_row];
1309
+ const taken_by_blocks = typeof row === "number" ? row : row ? row.filter(item => item).length : 0;
1310
+ seen_products += Math.max(0, n_cols_currently_showing_ - taken_by_blocks);
1311
+ if (seen_products >= product_count) break;
1312
+ // Rows past the ones blocks take up all have room for products, so we're done by then at the latest
1313
+ if (last_product_row >= displaced_products_.length + product_count) break;
1314
+ }
1315
+ for (let i = product_count; i < content_blocks_by_index_.length; i++) {
1316
+ const block = content_blocks_by_index_[i];
1317
+ if (block === undefined) continue;
1318
+ const rows = rows_by_index_[i] || [];
1319
+ if (Array.isArray(block) && rows.length > 1) {
1320
+ // 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
1321
+ const in_last_row = block.filter((_, j) => rows[j] <= last_product_row);
1322
+ if (in_last_row.length) trailing.push(in_last_row);
1323
+ continue;
1324
+ }
1325
+ if ((rows[0] ?? Infinity) > last_product_row) continue; // a row below the last product's row: keep it hidden
1326
+ trailing.push(block);
1327
+ }
1328
+ return trailing;
1329
+ }
1277
1330
  /**
1278
1331
  * 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.
1279
1332
  * Then renders the content and wraps it into a div with the correct styling.
@@ -1303,6 +1356,7 @@ function $e7a7eb102ddc0c79$var$wrap_blocks_in_div(rendered_content_blocks, max_s
1303
1356
  content: () => {
1304
1357
  // 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
1305
1358
  // 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
1359
+ // (or, once all products have loaded, ResultsProductCards renders the blocks placed after the last product)
1306
1360
  set_rendering_delayed_(false);
1307
1361
  return content;
1308
1362
  },
@@ -2020,6 +2074,61 @@ function $1adfe60f3a14a1d7$export$fecbb157621501bd({
2020
2074
  }));
2021
2075
  return outmost_element;
2022
2076
  }
2077
+
2078
+ /**
2079
+ * `HTMLElement.focus()` is a **silent no-op** on an element that is not being rendered
2080
+ * (`display: none`, `visibility: hidden`, a collapsed menu). Nothing throws, focus simply
2081
+ * stays where it was, so a caller that does not check believes it restored focus when it
2082
+ * left it on `<body>` — which puts the keyboard user back at the top of the document.
2083
+ *
2084
+ * That is reachable whenever the element which opened a modal stops being rendered while the
2085
+ * modal is open, and the common way for that to happen is the responsive-duplicate header:
2086
+ * a theme renders the same search trigger twice, once for desktop and once for mobile, and
2087
+ * hides one at the current breakpoint. Open the modal from the desktop trigger, cross the
2088
+ * breakpoint (rotate a tablet, resize a window), close the modal, and the remembered trigger
2089
+ * is still `isConnected` but no longer rendered. Verified against a live merchant storefront
2090
+ * on 4.10.19.
2091
+ *
2092
+ * So: verify that focus landed, and when it did not, look for a rendered twin of the same
2093
+ * control — the other half of the responsive pair — and use that instead.
2094
+ */ /**
2095
+ * An element can only take focus if it is actually being rendered.
2096
+ *
2097
+ * Injectable because it is the one part of this module that needs a layout engine: jsdom
2098
+ * reports no client rects for anything, so the specs pass their own predicate to exercise the
2099
+ * matching rules. The real predicate is only exercised in a browser.
2100
+ */
2101
+ const $07001c226aedc5fb$export$e0652711c614c755 = element => !!element.getClientRects().length;
2102
+ /**
2103
+ * `document.activeElement` is the outermost shadow host when focus is inside a shadow tree,
2104
+ * so ask the element's own root rather than the document — matching how the rest of
2105
+ * modal_opener treats shadow-mounted modals.
2106
+ */
2107
+ const $07001c226aedc5fb$var$has_focus = element => element.getRootNode().activeElement === element;
2108
+ /**
2109
+ * The accessible-name-ish identity we match twins on. Deliberately narrow: an explicit label
2110
+ * or the trimmed text. Two controls that share a tag and this string are the same control
2111
+ * rendered twice, which is exactly the responsive-duplicate case.
2112
+ */
2113
+ const $07001c226aedc5fb$var$identity_of = element => (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "").trim().replace(/\s+/g, " ");
2114
+ function $07001c226aedc5fb$export$5800d9d535a06e96(element, rendered = $07001c226aedc5fb$export$e0652711c614c755) {
2115
+ const identity = $07001c226aedc5fb$var$identity_of(element);
2116
+ if (!identity) return; // nothing to match on — an icon-only button with no label
2117
+ const twins = [...element.ownerDocument.querySelectorAll(element.localName)].filter(candidate => candidate !== element && rendered(candidate) && $07001c226aedc5fb$var$identity_of(candidate) === identity);
2118
+ return twins.length === 1 ? twins[0] : undefined;
2119
+ }
2120
+ function $07001c226aedc5fb$export$51461d633c351649(element, rendered = $07001c226aedc5fb$export$e0652711c614c755) {
2121
+ element.focus({
2122
+ preventScroll: true
2123
+ });
2124
+ if ($07001c226aedc5fb$var$has_focus(element)) return true;
2125
+ const twin = $07001c226aedc5fb$export$5800d9d535a06e96(element, rendered);
2126
+ if (!twin) return false;
2127
+ twin.focus({
2128
+ preventScroll: true
2129
+ });
2130
+ return $07001c226aedc5fb$var$has_focus(twin);
2131
+ }
2023
2132
  async function $194bd6b8e4c59830$export$79b1565500412d9b(search_modal, modal_fn_opts, parent) {
2024
2133
  const body = parent || document.body || (await (0, $i7OB2$observer).wait_for_element("body"));
2025
2134
  let dispose_and_remove;
@@ -2080,9 +2189,12 @@ async function $194bd6b8e4c59830$export$79b1565500412d9b(search_modal, modal_fn_
2080
2189
  });
2081
2190
  focused = requested_focus_target.getRootNode().activeElement === requested_focus_target;
2082
2191
  }
2083
- if (!focused && element_focused_before_open?.isConnected) element_focused_before_open.focus({
2084
- preventScroll: true
2085
- });
2192
+ if (!focused && element_focused_before_open?.isConnected)
2193
+ // restore_focus_to rather than a bare focus(): the remembered trigger can be
2194
+ // connected but no longer rendered (a responsive-duplicate header whose other
2195
+ // half is showing now), and focus() on an unrendered element silently does
2196
+ // nothing, leaving the keyboard user at the top of the page.
2197
+ $07001c226aedc5fb$export$51461d633c351649(element_focused_before_open);
2086
2198
  } finally {
2087
2199
  // Cleared on a task instead of synchronously in case a browser delivers the focus event async
2088
2200
  setTimeout(() => restoring_focus = false, 0);
@@ -7487,16 +7599,20 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7487
7599
  * 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
7600
  */ /**
7489
7601
  * 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) {
7602
+ */ /**
7603
+ * 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
7604
+ */
7605
+ navigation_to_same_href_listeners_ = new Set();
7606
+ #throwing_on_navigation(options, on_throw_) {
7492
7607
  const {
7493
7608
  on_navigation_: on_navigation_
7494
7609
  } = this;
7495
7610
  if (typeof on_navigation_ !== "function") throw new Error("on_navigation_ is not a function");
7496
7611
  // make sure that we don't accidentally catchify if the SDK consumer has fucked up
7497
7612
  try {
7498
- return on_navigation_(...args);
7613
+ return on_navigation_(options);
7499
7614
  } catch (e) {
7615
+ on_throw_?.();
7500
7616
  queueMicrotask(() => {
7501
7617
  throw e;
7502
7618
  });
@@ -7517,6 +7633,28 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7517
7633
  (0, $i7OB2$instant_exec_on_suspect_history_change).add(handler);
7518
7634
  });
7519
7635
  }
7636
+ /**
7637
+ * Calls navigation_to_same_href_listeners_ if the navigation we're about to request ends on the href we're already on
7638
+ * @returns function to stop watching, call it when the navigation failed
7639
+ */
7640
+ #watch_for_navigation_to_same_href(target_url) {
7641
+ const href_before = location.href;
7642
+ const stop_watching = () => {
7643
+ (0, $i7OB2$instant_exec_on_suspect_history_change).delete(on_history_change);
7644
+ clearTimeout(timeout);
7645
+ };
7646
+ const notify_if_href_unchanged = $i7OB2$catchify1(() => {
7647
+ stop_watching();
7648
+ if (location.href !== href_before) return;
7649
+ this.navigation_to_same_href_listeners_.forEach(listener => $i7OB2$catchify1(listener)());
7650
+ });
7651
+ // 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
7652
+ const on_history_change = () => notify_if_href_unchanged();
7653
+ // 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
7654
+ const timeout = target_url.href === href_before ? setTimeout(notify_if_href_unchanged, 500) : setTimeout(stop_watching, 10000);
7655
+ (0, $i7OB2$instant_exec_on_suspect_history_change).add(on_history_change);
7656
+ return stop_watching;
7657
+ }
7520
7658
  navigate_ = {
7521
7659
  /**
7522
7660
  * Call this if you *just* want to replace the state object in history.state
@@ -7556,6 +7694,7 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7556
7694
  }
7557
7695
  event_?.preventDefault();
7558
7696
  const promise = this.#get_navigation_happened_promise(true);
7697
+ const stop_watching_for_same_href = this.#watch_for_navigation_to_same_href(new_url_);
7559
7698
  if (force_spa_navigation_ && wants_hard_navigation) {
7560
7699
  // Don't allow hard navigation if we're forcing SPA navigation, which we do for things where PageReplacer can be used for performance
7561
7700
  const new_state = history.state;
@@ -7569,7 +7708,7 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
7569
7708
  is_replace: is_replace_,
7570
7709
  new_url: new_url_ instanceof URL ? new_url_ : new URL(new_url_, location.href),
7571
7710
  scroll: scroll
7572
- });
7711
+ }, stop_watching_for_same_href);
7573
7712
  await promise;
7574
7713
  })
7575
7714
  };
@@ -8867,11 +9006,17 @@ var $ffb84ffbf4014982$var$_tmpl$ = ["<svg", ' xmlns="http://www.w3.org/2000/svg"
8867
9006
  function $ffb84ffbf4014982$export$8d8893ffd8aca39a() {
8868
9007
  return $i7OB2$ssr($ffb84ffbf4014982$var$_tmpl$, $i7OB2$ssrHydrationKey());
8869
9008
  }
8870
- function $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_) {
9009
+ function $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_, router_) {
8871
9010
  const [exit_iterable, exitable_iterable] = $i7OB2$make_asyncIterable_exiter((0, $i7OB2$href_change_ipns));
9011
+ const {
9012
+ navigation_to_same_href_listeners_: navigation_to_same_href_listeners_
9013
+ } = router_;
8872
9014
  let set_initial_values = false;
8873
9015
  let old_pathname;
8874
9016
  let old_param_value;
9017
+ // A card linking to the page one is already on doesn't change the href, close the modal anyway so the click does something
9018
+ navigation_to_same_href_listeners_.add(close_modal);
9019
+ $i7OB2$onCleanup(() => navigation_to_same_href_listeners_.delete(close_modal));
8875
9020
  $i7OB2$onCleanup($i7OB2$catchify1(exit_iterable));
8876
9021
  $i7OB2$catchify1(async () => {
8877
9022
  for await (const href of exitable_iterable) {
@@ -9016,7 +9161,7 @@ function $42912431795f188e$var$RawClassicSearchModal({
9016
9161
  }
9017
9162
  })));
9018
9163
  if (disable_scrolling_) $aea1edf92433e81e$export$fe334901e8bebd57();
9019
- $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9164
+ $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9020
9165
  window.addEventListener("keydown", close_on_escape_handler);
9021
9166
  $i7OB2$onCleanup($i7OB2$catchify1(() => window.removeEventListener("keydown", close_on_escape_handler)));
9022
9167
  if (closing_animation_) register_closing_animation_(closing_animation_);
@@ -9709,7 +9854,7 @@ function $500a16dd18ee5a71$var$RawSearchModalV2({
9709
9854
  });
9710
9855
  const noResults = $i7OB2$createMemo(prev => (get_modal_search_results_()?.loading === false && !suggestions_response_.loading || !!prev) && totalNumberResults() === 0 && previous_searches_matching_query().length === 0);
9711
9856
  if (disable_scrolling_) $aea1edf92433e81e$export$fe334901e8bebd57();
9712
- $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
9857
+ $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
9713
9858
  addEventListener("keydown", close_on_escape_handler);
9714
9859
  $i7OB2$onCleanup(() => removeEventListener("keydown", close_on_escape_handler));
9715
9860
  $i7OB2$createEffect($i7OB2$on(get_search_field_value_, () => {