@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.
package/latest/index.d.ts CHANGED
@@ -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
package/latest/index.js CHANGED
@@ -7,7 +7,7 @@ import { createStore as $9HbP8$createStore, reconcile as $9HbP8$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 = $9HbP8$createSignal(false);
13
13
  $9HbP8$onCleanup($9HbP8$javascript_media_query(media_query, ({
@@ -1130,8 +1130,14 @@ function $0449e7d49398c077$export$a1fd619a4743ca08({
1130
1130
  setFirstResultInViewport(Math.min(...intersecting) + 1); // +1 because we want to start counting at 1
1131
1131
  setLastResultsInViewport(Math.max(...intersecting) + 1);
1132
1132
  });
1133
- return renderedDisplays_;
1133
+ // 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
1134
+ const trailingContentBlocks_ = $9HbP8$createMemo(() => {
1135
+ if (!all_products_loaded_[0]()) return [];
1136
+ return content_blocks_?.()?.trailing_?.(displays_by_key_().size) || [];
1137
+ });
1138
+ return [renderedDisplays_, trailingContentBlocks_]; // Regarding the as unknown: https://github.com/solidjs/solid/releases/tag/v1.7.0
1134
1139
  }
1140
+
1135
1141
  var $c8d5ff4524619fcd$var$_tmpl$ = /*#__PURE__*/$9HbP8$template(`<div>`),
1136
1142
  $c8d5ff4524619fcd$var$_tmpl$2 = /*#__PURE__*/$9HbP8$template(`<style type=text/css>`);
1137
1143
  function $c8d5ff4524619fcd$export$d21e32dfedac04be(props) {
@@ -1481,10 +1487,18 @@ function $e7a7eb102ddc0c79$export$791ef57e62e21c3f({
1481
1487
  set_top_row_shortened_by_(blocks_in_top_row_taken - 1); // minus one because the content block wrapper also is an element
1482
1488
  });
1483
1489
 
1484
- return $9HbP8$createMemo(() => $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1490
+ const remapped_ = $9HbP8$createMemo(() => $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1485
1491
  wrapped_blocks_: get_wrapped_blocks_(),
1486
1492
  n_cols_currently_showing_: n_cols_currently_showing_()
1487
- }).content_blocks_by_index_);
1493
+ }));
1494
+ const content_blocks_by_index_ = $9HbP8$createMemo(() => remapped_().content_blocks_by_index_);
1495
+ return Object.assign(content_blocks_by_index_, {
1496
+ trailing_: product_count => $e7a7eb102ddc0c79$export$b94605e5b3ce01c({
1497
+ ...remapped_(),
1498
+ n_cols_currently_showing_: n_cols_currently_showing_(),
1499
+ product_count: product_count
1500
+ })
1501
+ });
1488
1502
  }
1489
1503
  function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1490
1504
  wrapped_blocks_: wrapped_blocks_,
@@ -1492,13 +1506,16 @@ function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1492
1506
  }) {
1493
1507
  // This computation should be quite cheap since it just moves around things into an array
1494
1508
  const content_blocks_by_index_ = [];
1509
+ const rows_by_index_ = []; // the row of each block at each index of content_blocks_by_index_, in the same order
1495
1510
  const constructed_block_arrays = new WeakSet();
1496
1511
  // Need to keep track of products displaced by blocks above when mapping rows to indexes in the actual grid
1497
1512
  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
1498
1513
  if (!n_cols_currently_showing_)
1499
1514
  // Nope out of here if we don't know how many columns are showing
1500
1515
  return {
1501
- content_blocks_by_index_: content_blocks_by_index_
1516
+ content_blocks_by_index_: content_blocks_by_index_,
1517
+ rows_by_index_: rows_by_index_,
1518
+ displaced_products_: displaced_products_
1502
1519
  };
1503
1520
  for (let i = 0; i < wrapped_blocks_.length; i++) {
1504
1521
  // i = row to show at
@@ -1543,6 +1560,7 @@ function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1543
1560
  content_blocks_by_index_[final_target_index] = array;
1544
1561
  }
1545
1562
  } else content_blocks_by_index_[final_target_index] = block_content;
1563
+ (rows_by_index_[final_target_index] ||= []).push(i);
1546
1564
  // Update displacement
1547
1565
  for (let j = 0; j < span_rows; j++) {
1548
1566
  const row = displaced_products_[i + j] ||= [];
@@ -1551,9 +1569,44 @@ function $e7a7eb102ddc0c79$export$92aa5d691d588f3f({
1551
1569
  }
1552
1570
  return {
1553
1571
  content_blocks_by_index_: content_blocks_by_index_,
1572
+ rows_by_index_: rows_by_index_,
1554
1573
  displaced_products_: displaced_products_
1555
1574
  };
1556
1575
  }
1576
+ function $e7a7eb102ddc0c79$export$b94605e5b3ce01c({
1577
+ content_blocks_by_index_: content_blocks_by_index_,
1578
+ rows_by_index_: rows_by_index_,
1579
+ displaced_products_: displaced_products_,
1580
+ n_cols_currently_showing_: n_cols_currently_showing_,
1581
+ product_count: product_count
1582
+ }) {
1583
+ const trailing = [];
1584
+ if (!n_cols_currently_showing_ || product_count <= 0) return trailing;
1585
+ // Find the row the last product lands in: each row has room for the columns that blocks don't take up
1586
+ let last_product_row = 0;
1587
+ for (let seen_products = 0;; last_product_row++) {
1588
+ const row = displaced_products_[last_product_row];
1589
+ const taken_by_blocks = typeof row === "number" ? row : row ? row.filter(item => item).length : 0;
1590
+ seen_products += Math.max(0, n_cols_currently_showing_ - taken_by_blocks);
1591
+ if (seen_products >= product_count) break;
1592
+ // Rows past the ones blocks take up all have room for products, so we're done by then at the latest
1593
+ if (last_product_row >= displaced_products_.length + product_count) break;
1594
+ }
1595
+ for (let i = product_count; i < content_blocks_by_index_.length; i++) {
1596
+ const block = content_blocks_by_index_[i];
1597
+ if (block === undefined) continue;
1598
+ const rows = rows_by_index_[i] || [];
1599
+ if (Array.isArray(block) && rows.length > 1) {
1600
+ // 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
1601
+ const in_last_row = block.filter((_, j) => rows[j] <= last_product_row);
1602
+ if (in_last_row.length) trailing.push(in_last_row);
1603
+ continue;
1604
+ }
1605
+ if ((rows[0] ?? Infinity) > last_product_row) continue; // a row below the last product's row: keep it hidden
1606
+ trailing.push(block);
1607
+ }
1608
+ return trailing;
1609
+ }
1557
1610
  /**
1558
1611
  * 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.
1559
1612
  * Then renders the content and wraps it into a div with the correct styling.
@@ -1603,6 +1656,7 @@ function $e7a7eb102ddc0c79$var$wrap_blocks_in_div(rendered_content_blocks, max_s
1603
1656
  content: () => {
1604
1657
  // 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
1605
1658
  // 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
1659
+ // (or, once all products have loaded, ResultsProductCards renders the blocks placed after the last product)
1606
1660
  set_rendering_delayed_(false);
1607
1661
  return content;
1608
1662
  },
@@ -2462,6 +2516,61 @@ function $1adfe60f3a14a1d7$export$fecbb157621501bd({
2462
2516
  return outmost_element;
2463
2517
  }
2464
2518
  $9HbP8$delegateEvents(["click"]);
2519
+
2520
+ /**
2521
+ * `HTMLElement.focus()` is a **silent no-op** on an element that is not being rendered
2522
+ * (`display: none`, `visibility: hidden`, a collapsed menu). Nothing throws, focus simply
2523
+ * stays where it was, so a caller that does not check believes it restored focus when it
2524
+ * left it on `<body>` — which puts the keyboard user back at the top of the document.
2525
+ *
2526
+ * That is reachable whenever the element which opened a modal stops being rendered while the
2527
+ * modal is open, and the common way for that to happen is the responsive-duplicate header:
2528
+ * a theme renders the same search trigger twice, once for desktop and once for mobile, and
2529
+ * hides one at the current breakpoint. Open the modal from the desktop trigger, cross the
2530
+ * breakpoint (rotate a tablet, resize a window), close the modal, and the remembered trigger
2531
+ * is still `isConnected` but no longer rendered. Verified against a live merchant storefront
2532
+ * on 4.10.19.
2533
+ *
2534
+ * So: verify that focus landed, and when it did not, look for a rendered twin of the same
2535
+ * control — the other half of the responsive pair — and use that instead.
2536
+ */ /**
2537
+ * An element can only take focus if it is actually being rendered.
2538
+ *
2539
+ * Injectable because it is the one part of this module that needs a layout engine: jsdom
2540
+ * reports no client rects for anything, so the specs pass their own predicate to exercise the
2541
+ * matching rules. The real predicate is only exercised in a browser.
2542
+ */
2543
+ const $07001c226aedc5fb$export$e0652711c614c755 = element => !!element.getClientRects().length;
2544
+ /**
2545
+ * `document.activeElement` is the outermost shadow host when focus is inside a shadow tree,
2546
+ * so ask the element's own root rather than the document — matching how the rest of
2547
+ * modal_opener treats shadow-mounted modals.
2548
+ */
2549
+ const $07001c226aedc5fb$var$has_focus = element => element.getRootNode().activeElement === element;
2550
+ /**
2551
+ * The accessible-name-ish identity we match twins on. Deliberately narrow: an explicit label
2552
+ * or the trimmed text. Two controls that share a tag and this string are the same control
2553
+ * rendered twice, which is exactly the responsive-duplicate case.
2554
+ */
2555
+ const $07001c226aedc5fb$var$identity_of = element => (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || "").trim().replace(/\s+/g, " ");
2556
+ function $07001c226aedc5fb$export$5800d9d535a06e96(element, rendered = $07001c226aedc5fb$export$e0652711c614c755) {
2557
+ const identity = $07001c226aedc5fb$var$identity_of(element);
2558
+ if (!identity) return; // nothing to match on — an icon-only button with no label
2559
+ const twins = [...element.ownerDocument.querySelectorAll(element.localName)].filter(candidate => candidate !== element && rendered(candidate) && $07001c226aedc5fb$var$identity_of(candidate) === identity);
2560
+ return twins.length === 1 ? twins[0] : undefined;
2561
+ }
2562
+ function $07001c226aedc5fb$export$51461d633c351649(element, rendered = $07001c226aedc5fb$export$e0652711c614c755) {
2563
+ element.focus({
2564
+ preventScroll: true
2565
+ });
2566
+ if ($07001c226aedc5fb$var$has_focus(element)) return true;
2567
+ const twin = $07001c226aedc5fb$export$5800d9d535a06e96(element, rendered);
2568
+ if (!twin) return false;
2569
+ twin.focus({
2570
+ preventScroll: true
2571
+ });
2572
+ return $07001c226aedc5fb$var$has_focus(twin);
2573
+ }
2465
2574
  async function $194bd6b8e4c59830$export$79b1565500412d9b(search_modal, modal_fn_opts, parent) {
2466
2575
  const body = parent || document.body || (await (0, $9HbP8$observer).wait_for_element("body"));
2467
2576
  let dispose_and_remove;
@@ -2522,9 +2631,12 @@ async function $194bd6b8e4c59830$export$79b1565500412d9b(search_modal, modal_fn_
2522
2631
  });
2523
2632
  focused = requested_focus_target.getRootNode().activeElement === requested_focus_target;
2524
2633
  }
2525
- if (!focused && element_focused_before_open?.isConnected) element_focused_before_open.focus({
2526
- preventScroll: true
2527
- });
2634
+ if (!focused && element_focused_before_open?.isConnected)
2635
+ // restore_focus_to rather than a bare focus(): the remembered trigger can be
2636
+ // connected but no longer rendered (a responsive-duplicate header whose other
2637
+ // half is showing now), and focus() on an unrendered element silently does
2638
+ // nothing, leaving the keyboard user at the top of the page.
2639
+ $07001c226aedc5fb$export$51461d633c351649(element_focused_before_open);
2528
2640
  } finally {
2529
2641
  // Cleared on a task instead of synchronously in case a browser delivers the focus event async
2530
2642
  setTimeout(() => restoring_focus = false, 0);
@@ -9165,16 +9277,20 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
9165
9277
  * Function that will be called when we want to navigate, or a string saying it's not an SPA. You can change this OTG
9166
9278
  */ /**
9167
9279
  * 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
9168
- */
9169
- #throwing_on_navigation(...args) {
9280
+ */ /**
9281
+ * 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
9282
+ */
9283
+ navigation_to_same_href_listeners_ = new Set();
9284
+ #throwing_on_navigation(options, on_throw_) {
9170
9285
  const {
9171
9286
  on_navigation_: on_navigation_
9172
9287
  } = this;
9173
9288
  if (typeof on_navigation_ !== "function") throw new Error("on_navigation_ is not a function");
9174
9289
  // make sure that we don't accidentally catchify if the SDK consumer has fucked up
9175
9290
  try {
9176
- return on_navigation_(...args);
9291
+ return on_navigation_(options);
9177
9292
  } catch (e) {
9293
+ on_throw_?.();
9178
9294
  queueMicrotask(() => {
9179
9295
  throw e;
9180
9296
  });
@@ -9195,6 +9311,28 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
9195
9311
  (0, $9HbP8$instant_exec_on_suspect_history_change).add(handler);
9196
9312
  });
9197
9313
  }
9314
+ /**
9315
+ * Calls navigation_to_same_href_listeners_ if the navigation we're about to request ends on the href we're already on
9316
+ * @returns function to stop watching, call it when the navigation failed
9317
+ */
9318
+ #watch_for_navigation_to_same_href(target_url) {
9319
+ const href_before = location.href;
9320
+ const stop_watching = () => {
9321
+ (0, $9HbP8$instant_exec_on_suspect_history_change).delete(on_history_change);
9322
+ clearTimeout(timeout);
9323
+ };
9324
+ const notify_if_href_unchanged = $9HbP8$catchify1(() => {
9325
+ stop_watching();
9326
+ if (location.href !== href_before) return;
9327
+ this.navigation_to_same_href_listeners_.forEach(listener => $9HbP8$catchify1(listener)());
9328
+ });
9329
+ // 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
9330
+ const on_history_change = () => notify_if_href_unchanged();
9331
+ // 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
9332
+ const timeout = target_url.href === href_before ? setTimeout(notify_if_href_unchanged, 500) : setTimeout(stop_watching, 10000);
9333
+ (0, $9HbP8$instant_exec_on_suspect_history_change).add(on_history_change);
9334
+ return stop_watching;
9335
+ }
9198
9336
  navigate_ = {
9199
9337
  /**
9200
9338
  * Call this if you *just* want to replace the state object in history.state
@@ -9234,6 +9372,7 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
9234
9372
  }
9235
9373
  event_?.preventDefault();
9236
9374
  const promise = this.#get_navigation_happened_promise(true);
9375
+ const stop_watching_for_same_href = this.#watch_for_navigation_to_same_href(new_url_);
9237
9376
  if (force_spa_navigation_ && wants_hard_navigation) {
9238
9377
  // Don't allow hard navigation if we're forcing SPA navigation, which we do for things where PageReplacer can be used for performance
9239
9378
  const new_state = history.state;
@@ -9247,7 +9386,7 @@ class $4fd2ba03703a7d92$export$7e1297a3a19f10d8 {
9247
9386
  is_replace: is_replace_,
9248
9387
  new_url: new_url_ instanceof URL ? new_url_ : new URL(new_url_, location.href),
9249
9388
  scroll: scroll
9250
- });
9389
+ }, stop_watching_for_same_href);
9251
9390
  await promise;
9252
9391
  })
9253
9392
  };
@@ -10974,11 +11113,17 @@ var $ffb84ffbf4014982$var$_tmpl$ = /*#__PURE__*/$9HbP8$template(`<svg xmlns=http
10974
11113
  function $ffb84ffbf4014982$export$8d8893ffd8aca39a() {
10975
11114
  return $9HbP8$getNextElement($ffb84ffbf4014982$var$_tmpl$);
10976
11115
  }
10977
- function $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_) {
11116
+ function $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal, search_query_url_param_name_, router_) {
10978
11117
  const [exit_iterable, exitable_iterable] = $9HbP8$make_asyncIterable_exiter((0, $9HbP8$href_change_ipns));
11118
+ const {
11119
+ navigation_to_same_href_listeners_: navigation_to_same_href_listeners_
11120
+ } = router_;
10979
11121
  let set_initial_values = false;
10980
11122
  let old_pathname;
10981
11123
  let old_param_value;
11124
+ // A card linking to the page one is already on doesn't change the href, close the modal anyway so the click does something
11125
+ navigation_to_same_href_listeners_.add(close_modal);
11126
+ $9HbP8$onCleanup(() => navigation_to_same_href_listeners_.delete(close_modal));
10982
11127
  $9HbP8$onCleanup($9HbP8$catchify1(exit_iterable));
10983
11128
  $9HbP8$catchify1(async () => {
10984
11129
  for await (const href of exitable_iterable) {
@@ -11134,7 +11279,7 @@ function $42912431795f188e$var$RawClassicSearchModal({
11134
11279
  return _el$;
11135
11280
  })();
11136
11281
  if (disable_scrolling_) $aea1edf92433e81e$export$fe334901e8bebd57();
11137
- $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
11282
+ $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
11138
11283
  window.addEventListener("keydown", close_on_escape_handler);
11139
11284
  $9HbP8$onCleanup($9HbP8$catchify1(() => window.removeEventListener("keydown", close_on_escape_handler)));
11140
11285
  if (closing_animation_) register_closing_animation_(closing_animation_);
@@ -12013,7 +12158,7 @@ function $500a16dd18ee5a71$var$RawSearchModalV2({
12013
12158
  });
12014
12159
  const noResults = $9HbP8$createMemo(prev => (get_modal_search_results_()?.loading === false && !suggestions_response_.loading || !!prev) && totalNumberResults() === 0 && previous_searches_matching_query().length === 0);
12015
12160
  if (disable_scrolling_) $aea1edf92433e81e$export$fe334901e8bebd57();
12016
- $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_);
12161
+ $512b994d501f412b$export$93d9bfd0c9c1a5b8(close_modal_, search_query_url_param_name_, router_);
12017
12162
  addEventListener("keydown", close_on_escape_handler);
12018
12163
  $9HbP8$onCleanup(() => removeEventListener("keydown", close_on_escape_handler));
12019
12164
  $9HbP8$createEffect($9HbP8$on(get_search_field_value_, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@depict-ai/ui",
3
- "version": "4.10.20",
3
+ "version": "4.10.22",
4
4
  "description": "Base package containing the actual UI of Depict UI along with translations, with a not-simplified API. We recommend using @depict-ai/js-ui or @depict-ai/react-ui instead.",
5
5
  "packageManager": "yarn@3.2.2",
6
6
  "sass": "styles.scss",
@@ -236,9 +236,9 @@
236
236
  "@babel/plugin-transform-typescript": "^7.22.9",
237
237
  "@babel/runtime": "^7.22.6",
238
238
  "@babel/runtime-corejs3": "^7.22.6",
239
- "@depict-ai/plp-styling": "^4.6.12",
240
- "@depict-ai/types": "^5.6.10",
241
- "@depict-ai/utilishared": "^2.6.10",
239
+ "@depict-ai/plp-styling": "^4.6.14",
240
+ "@depict-ai/types": "^5.6.12",
241
+ "@depict-ai/utilishared": "^2.6.12",
242
242
  "solid-js": "^1.9.10"
243
243
  },
244
244
  "type": "module",