@occluze/tether 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +5 -5
  2. package/dist/esm/astro.js +1 -1
  3. package/dist/esm/chunks/account-FKLGF264.js +1 -0
  4. package/dist/esm/chunks/{account-pages-XR6QZ3DX.js → account-pages-PNORWX72.js} +1 -1
  5. package/dist/esm/chunks/analytics-V24XX2J7.js +1 -0
  6. package/dist/esm/chunks/{chunk-255K6OZ7.js → chunk-2IHA3PVC.js} +1 -1
  7. package/dist/esm/chunks/chunk-2LYWZ72A.js +1 -0
  8. package/dist/esm/chunks/{chunk-3OEIEDTK.js → chunk-2NV5PSJ2.js} +1 -1
  9. package/dist/esm/chunks/chunk-2X5U24IC.js +1 -0
  10. package/dist/esm/chunks/chunk-5B36ISJF.js +1 -0
  11. package/dist/esm/chunks/chunk-6WE7XCHH.js +1 -0
  12. package/dist/esm/chunks/chunk-6ZIOAYKF.js +1 -0
  13. package/dist/esm/chunks/{chunk-FM7ZLLVP.js → chunk-CR5IGHCO.js} +1 -1
  14. package/dist/esm/chunks/{chunk-KNJB72OO.js → chunk-E3JL6WS3.js} +1 -1
  15. package/dist/esm/chunks/{chunk-KVPLRK6Y.js → chunk-F5CLIGSC.js} +1 -1
  16. package/dist/esm/chunks/{chunk-O4G2PAQC.js → chunk-GOKJQGLG.js} +1 -1
  17. package/dist/esm/chunks/chunk-RW23CJDZ.js +1 -0
  18. package/dist/esm/chunks/chunk-TW34GYSL.js +1 -0
  19. package/dist/esm/chunks/chunk-VKKKH75V.js +1 -0
  20. package/dist/esm/chunks/{chunk-A3GXCQJ4.js → chunk-YWQWLKFW.js} +1 -1
  21. package/dist/esm/chunks/chunk-YYNBHXQF.js +1 -0
  22. package/dist/esm/chunks/{diagnose-XQQTDHLQ.js → diagnose-3RTKEBNW.js} +1 -1
  23. package/dist/esm/chunks/extras-O54VUXK6.js +1 -0
  24. package/dist/esm/chunks/lists-MRTI5RXY.js +1 -0
  25. package/dist/esm/chunks/markets-ui-P7G33T6Y.js +1 -0
  26. package/dist/esm/chunks/search-YSRDFVD7.js +1 -0
  27. package/dist/esm/index.js +1 -1
  28. package/dist/esm/server.js +2 -2
  29. package/dist/tether.account-pages.js +2 -2
  30. package/dist/tether.account-pages.min.js +1 -1
  31. package/dist/tether.account.js +2 -2
  32. package/dist/tether.account.min.js +1 -1
  33. package/dist/tether.analytics.js +2 -2
  34. package/dist/tether.analytics.min.js +1 -1
  35. package/dist/tether.diagnose.js +4 -4
  36. package/dist/tether.diagnose.min.js +2 -2
  37. package/dist/tether.extras.js +1 -1
  38. package/dist/tether.extras.min.js +1 -1
  39. package/dist/tether.js +83 -206
  40. package/dist/tether.lists.js +268 -0
  41. package/dist/tether.lists.min.js +1 -0
  42. package/dist/tether.markets.js +3 -3
  43. package/dist/tether.markets.min.js +1 -1
  44. package/dist/tether.min.js +1 -4
  45. package/dist/tether.min.js.map +4 -4
  46. package/dist/tether.search.js +2 -2
  47. package/dist/tether.search.min.js +1 -1
  48. package/dist/types/src/client.d.ts +6 -6
  49. package/dist/types/src/collection.d.ts +7 -23
  50. package/dist/types/src/config-sections.d.ts +7 -0
  51. package/dist/types/src/config.d.ts +6 -0
  52. package/dist/types/src/index.d.ts +6 -5
  53. package/dist/types/src/lazy.d.ts +1 -0
  54. package/dist/types/src/lists.d.ts +20 -0
  55. package/dist/types/src/loader.d.ts +7 -27
  56. package/dist/types/src/mounting.d.ts +1 -6
  57. package/dist/types/src/product-shape.d.ts +13 -36
  58. package/dist/types/src/server.d.ts +5 -4
  59. package/dist/types/src/shopify-values.d.ts +6 -0
  60. package/dist/types/src/storefront/catalog.d.ts +2 -4
  61. package/dist/types/src/version.d.ts +1 -0
  62. package/package.json +5 -5
  63. package/dist/esm/chunks/account-5X563BON.js +0 -1
  64. package/dist/esm/chunks/analytics-JSFHDBU3.js +0 -1
  65. package/dist/esm/chunks/chunk-55OHYO66.js +0 -1
  66. package/dist/esm/chunks/chunk-EW3GKRT4.js +0 -1
  67. package/dist/esm/chunks/chunk-GXCCTPDF.js +0 -1
  68. package/dist/esm/chunks/chunk-J7GYROV4.js +0 -4
  69. package/dist/esm/chunks/chunk-WNFEYPHV.js +0 -1
  70. package/dist/esm/chunks/chunk-WXF4L42L.js +0 -1
  71. package/dist/esm/chunks/extras-6WLNUEGD.js +0 -1
  72. package/dist/esm/chunks/markets-ui-OAJW4PFU.js +0 -1
  73. package/dist/esm/chunks/search-LUECIYJR.js +0 -1
package/dist/tether.js CHANGED
@@ -240,6 +240,7 @@
240
240
  sellingPlansFirst: 25,
241
241
  collectionFirst: 24,
242
242
  retries: 2,
243
+ requestTimeout: 15e3,
243
244
  revalidate: true,
244
245
  storageKey: "",
245
246
  cartDrawerOpenClass: "is-open",
@@ -304,6 +305,7 @@
304
305
  "sellingPlansFirst",
305
306
  "collectionFirst",
306
307
  "retries",
308
+ "requestTimeout",
307
309
  "maxAge",
308
310
  "lowStock",
309
311
  "searchDelay"
@@ -338,8 +340,11 @@
338
340
  dropped(name, value);
339
341
  }
340
342
  }
343
+ var DELAY_OPTIONS = /* @__PURE__ */ new Set(["requestTimeout", "searchDelay"]);
344
+ var MAX_TIMER_MS = 2147483647;
341
345
  function countOption(name, value) {
342
- if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
346
+ const maximum = DELAY_OPTIONS.has(name) ? MAX_TIMER_MS : Number.POSITIVE_INFINITY;
347
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= maximum) return value;
343
348
  return invalidOption(name, value, DEFAULTS[name]);
344
349
  }
345
350
  function switchOption(name, value) {
@@ -545,13 +550,27 @@
545
550
  const unclear = res.ok && (serverError || operation === "mutation" && data == null);
546
551
  throw graphQLFailure(res, errors, conf, unclear ? "SERVER_ERROR" : void 0);
547
552
  }
548
- async function attempt(conf, query2, variables, isData, retry, operation) {
553
+ async function withTimeout(ms, send2) {
554
+ if (!ms) return send2(null);
555
+ const controller = new AbortController();
556
+ const timer = setTimeout(() => {
557
+ controller.abort(new DOMException("signal timed out", "TimeoutError"));
558
+ }, ms);
559
+ try {
560
+ return await send2(controller.signal);
561
+ } finally {
562
+ clearTimeout(timer);
563
+ }
564
+ }
565
+ async function attempt(conf, query2, variables, isData, retry, operation, signal) {
549
566
  let res;
550
567
  try {
551
568
  res = await (conf.fetch ?? fetch)(endpoint(conf), {
552
569
  method: "POST",
553
570
  headers: { "Content-Type": "application/json", "X-Shopify-Storefront-Access-Token": conf.token },
554
- body: JSON.stringify({ query: query2, variables })
571
+ body: JSON.stringify({ query: query2, variables }),
572
+ // A `fetch` given to the server helpers has to honour the signal, as the platform's does.
573
+ signal
555
574
  });
556
575
  } catch (error) {
557
576
  return networkFailure(error, retry);
@@ -588,7 +607,10 @@
588
607
  const access = { inventory: !storesWithoutInventory.has(store) };
589
608
  const sent = queryText(conf, query2, access, { context, operation });
590
609
  const retry = retries < conf.retries ? retryWhileLeft : "none";
591
- const outcome = await attempt(conf, sent, variables, isData, retry, operation);
610
+ const outcome = await withTimeout(
611
+ conf.requestTimeout,
612
+ (signal) => attempt(conf, sent, variables, isData, retry, operation, signal)
613
+ );
592
614
  switch (outcome.kind) {
593
615
  case "data":
594
616
  return outcome.data;
@@ -598,9 +620,9 @@
598
620
  storesWithoutInventory.add(store);
599
621
  break;
600
622
  case "retry":
623
+ await sleep(outcome.backoffMs * 2 ** retries);
601
624
  retries += 1;
602
625
  log("retry", retries);
603
- await sleep(outcome.backoffMs * 2 ** retries);
604
626
  break;
605
627
  }
606
628
  }
@@ -1220,10 +1242,7 @@
1220
1242
  return ` fragment TetherCart on Cart{id checkoutUrl totalQuantity note createdAt updatedAt attributes{key value}buyerIdentity{countryCode email}discountCodes{code applicable}cost{subtotalAmount{${MONEY}}totalAmount{${MONEY}}checkoutChargeAmount{${MONEY}}}lines(first:100){nodes{id quantity attributes{key value}cost{totalAmount{${MONEY}}subtotalAmount{${MONEY}}amountPerQuantity{${MONEY}}compareAtAmountPerQuantity{${MONEY}}}discountAllocations{discountedAmount{${MONEY}}}sellingPlanAllocation{sellingPlan{id name description recurringDeliveries options{name value}}checkoutChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}}}merchandise{... on ProductVariant{id title sku availableForSale requiresShipping ${inventoryField(access)} image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}selectedOptions{name value}product{id title handle vendor featuredImage{${IMAGE}}}}}... on ComponentizableCartLine{lineComponents{id quantity merchandise{... on ProductVariant{id title product{title handle}}}}}}}}`;
1221
1243
  }
1222
1244
  function collectionFields() {
1223
- return `
1224
- id handle title description descriptionHtml
1225
- image { ${IMAGE} }
1226
- seo { title description }`;
1245
+ return ` id handle title description descriptionHtml image{${IMAGE}}seo{title description}`;
1227
1246
  }
1228
1247
 
1229
1248
  // src/selling-plans.ts
@@ -1372,6 +1391,13 @@
1372
1391
  };
1373
1392
  var isProductShape = (value) => field(value, "id") !== "" && field(value, "handle") !== "" && matches(value, PRODUCT);
1374
1393
  var PRODUCT_NODE = (node) => isRecord(node) && (node["id"] === void 0 || matches(node, PRODUCT));
1394
+ var isCollectionAnswer = shaped({
1395
+ collection: orNull({ handle: "string", products: PRODUCT_CONNECTION })
1396
+ });
1397
+ var isSearchAnswer = shaped({ products: PRODUCT_CONNECTION });
1398
+ var isRecommendationsAnswer = shaped({
1399
+ productRecommendations: orNull([PRODUCT])
1400
+ });
1375
1401
 
1376
1402
  // src/cart/cart-shape.ts
1377
1403
  var LINE = {
@@ -1523,12 +1549,12 @@
1523
1549
  if (ids.length) {
1524
1550
  definitions.push("$ids: [ID!]!");
1525
1551
  variables["ids"] = ids;
1526
- selections.push("byId: nodes(ids: $ids) { ... on Product { ...TetherProduct } }");
1552
+ selections.push(`byId:nodes(ids:$ids){... on Product{...TetherProduct}}`);
1527
1553
  }
1528
1554
  handles.forEach((handle, index) => {
1529
1555
  definitions.push(`$h${index}: String!`);
1530
1556
  variables[`h${index}`] = handle;
1531
- selections.push(`h${index}: product(handle: $h${index}) { ...TetherProduct }`);
1557
+ selections.push(`h${index}:product(handle:$h${index}){...TetherProduct}`);
1532
1558
  });
1533
1559
  const query2 = withProductFragment(`query TetherProducts(${definitions.join(",")}){${selections.join(" ")}}`);
1534
1560
  return { query: query2, variables, handles };
@@ -1626,64 +1652,6 @@
1626
1652
  if (normalized) productCache.delete(normalized);
1627
1653
  return getProduct(key);
1628
1654
  }
1629
- var isCollectionAnswer = shaped({
1630
- collection: orNull({ handle: "string", products: PRODUCT_CONNECTION })
1631
- });
1632
- var isSearchAnswer = shaped({ products: PRODUCT_CONNECTION });
1633
- var isRecommendationsAnswer = shaped({
1634
- productRecommendations: orNull([PRODUCT])
1635
- });
1636
- var COLLECTION_QUERY = `query TetherCollection($handle:String!,$first:Int!,$after:String,$sortKey:ProductCollectionSortKeys,$reverse:Boolean,$filters:[ProductFilter!]){collection(handle:$handle){${collectionFields()} products(first:$first,after:$after,sortKey:$sortKey,reverse:$reverse,filters:$filters){nodes{...TetherProduct}pageInfo{hasNextPage endCursor}}}}`;
1637
- var SEARCH_QUERY = `query TetherSearch($first:Int!,$after:String,$query:String,$sortKey:ProductSortKeys,$reverse:Boolean){products(first:$first,after:$after,query:$query,sortKey:$sortKey,reverse:$reverse){nodes{...TetherProduct}pageInfo{hasNextPage endCursor}}}`;
1638
- async function getProducts({
1639
- collection,
1640
- query: query2,
1641
- first = cfg.collectionFirst,
1642
- after = null,
1643
- sortKey,
1644
- reverse = false,
1645
- filters
1646
- } = {}) {
1647
- if (collection) {
1648
- const data2 = await gql(
1649
- withProductFragment(COLLECTION_QUERY),
1650
- {
1651
- handle: collection,
1652
- first,
1653
- after,
1654
- sortKey: sortKey || void 0,
1655
- reverse,
1656
- filters: filters ?? void 0
1657
- },
1658
- isCollectionAnswer
1659
- );
1660
- if (!data2.collection) throw new TetherError(`Collection not found: ${collection}`, { code: "NOT_FOUND" });
1661
- const { products, ...rest } = data2.collection;
1662
- products.nodes.forEach(primeProduct);
1663
- return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
1664
- }
1665
- const data = await gql(
1666
- withProductFragment(SEARCH_QUERY),
1667
- { first, after, query: query2 || void 0, sortKey: sortKey || void 0, reverse },
1668
- isSearchAnswer
1669
- );
1670
- data.products.nodes.forEach(primeProduct);
1671
- return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
1672
- }
1673
- async function getRecommendations(product, intent = "RELATED") {
1674
- const key = normalizeProductKey(typeof product === "object" ? product?.id : product);
1675
- const byId = key?.startsWith("gid://") ?? false;
1676
- const data = await gql(
1677
- withProductFragment(
1678
- `query TetherRecs(${byId ? "$id: ID!" : "$handle: String!"},$intent:ProductRecommendationIntent){productRecommendations(${byId ? "productId: $id" : "productHandle: $handle"},intent:$intent){...TetherProduct}}`
1679
- ),
1680
- byId ? { id: key, intent } : { handle: key, intent },
1681
- isRecommendationsAnswer
1682
- );
1683
- const list = data.productRecommendations ?? [];
1684
- list.forEach(primeProduct);
1685
- return list;
1686
- }
1687
1655
 
1688
1656
  // src/product-render.ts
1689
1657
  var NTH_IMAGE_BIND = /^image:(\d+)$/;
@@ -2234,7 +2202,7 @@
2234
2202
  }
2235
2203
  async function fetchCheckoutUrl(id) {
2236
2204
  const data = await gql(
2237
- "query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }",
2205
+ `query TetherCheckout($id:ID!){cart(id:$id){checkoutUrl}}`,
2238
2206
  { id },
2239
2207
  shaped({ cart: orNull({ checkoutUrl: "string" }) })
2240
2208
  );
@@ -3220,128 +3188,6 @@
3220
3188
  }
3221
3189
  };
3222
3190
 
3223
- // src/collection.ts
3224
- var URL_QUERY_SOURCE = "url:";
3225
- var listSelector = () => `${sel("collection")},${sel("products")},${sel("recommendations")}`;
3226
- var isElement2 = (target) => target instanceof window.Element;
3227
- var toLimit = (raw) => parseInt(raw ?? "", 10) || void 0;
3228
- var toIntent = (raw) => raw === "COMPLEMENTARY" ? "COMPLEMENTARY" : "RELATED";
3229
- var ListScope = class {
3230
- constructor(root) {
3231
- __publicField(this, "root");
3232
- __publicField(this, "products", []);
3233
- __publicField(this, "pageInfo", null);
3234
- __publicField(this, "collection", null);
3235
- /** Counts loads, so only the latest one renders. */
3236
- __publicField(this, "loadToken", 0);
3237
- this.root = root;
3238
- setScopeStatus(root, "loading");
3239
- root.addEventListener("click", (event) => {
3240
- if (!isElement2(event.target)) return;
3241
- const more = event.target.closest(sel("load-more"));
3242
- if (more?.closest(listSelector()) !== root) return;
3243
- event.preventDefault();
3244
- void busy(more, () => this.load(true));
3245
- });
3246
- }
3247
- $(selector) {
3248
- return scoped(this.root, selector, listSelector());
3249
- }
3250
- /** The collection or search query of the list's attributes. */
3251
- options() {
3252
- const { root } = this;
3253
- const query2 = { reverse: get(root, "reverse") === "true" };
3254
- const first = toLimit(get(root, "limit"));
3255
- if (first) query2.first = first;
3256
- const sortKey = get(root, "sort");
3257
- if (sortKey) query2.sortKey = sortKey;
3258
- if (has(root, "collection")) {
3259
- query2.collection = get(root, "collection") ?? "";
3260
- return query2;
3261
- }
3262
- const raw = get(root, "products") ?? "";
3263
- query2.query = raw.startsWith(URL_QUERY_SOURCE) ? new URL(window.location.href).searchParams.get(raw.slice(URL_QUERY_SOURCE.length)) || "" : raw;
3264
- return query2;
3265
- }
3266
- /**
3267
- * The product of a recommendations list: a handle, `url:…` (one dynamic product page), or empty inside a product
3268
- * scope for that product.
3269
- *
3270
- * @throws {TetherError} `NOT_FOUND` when there is no product to recommend for.
3271
- */
3272
- async loadRecommendations() {
3273
- let { key } = resolveProductKey(this.root, window.location, "recommendations");
3274
- const scope = this.root.closest(sel("product"));
3275
- if (!key && scope) ({ key } = resolveProductKey(scope));
3276
- if (!key) throw new TetherError("No product for recommendations", { code: "NOT_FOUND" });
3277
- return getRecommendations(key, toIntent(get(this.root, "intent")));
3278
- }
3279
- /** @param more Appends the next page instead of replacing the products. */
3280
- async load(more = false) {
3281
- this.loadToken += 1;
3282
- const token = this.loadToken;
3283
- const stale2 = () => token !== this.loadToken;
3284
- try {
3285
- if (has(this.root, "recommendations")) {
3286
- const recommendations = await this.loadRecommendations();
3287
- if (stale2()) return;
3288
- const limit = toLimit(get(this.root, "limit"));
3289
- this.products = limit ? recommendations.slice(0, limit) : recommendations;
3290
- } else {
3291
- const page = await getProducts({ ...this.options(), after: more ? this.pageInfo?.endCursor ?? null : null });
3292
- if (stale2()) return;
3293
- this.collection = page.collection;
3294
- this.pageInfo = page.pageInfo;
3295
- this.products = more ? [...this.products, ...page.products] : page.products;
3296
- }
3297
- this.render();
3298
- setScopeStatus(this.root, "ready");
3299
- emit("list-ready", { products: this.products, collection: this.collection }, this.root);
3300
- } catch (error) {
3301
- if (stale2()) return;
3302
- if (!more) setScopeStatus(this.root, isNotFound(error) ? "not-found" : "error", error);
3303
- if (!(isNotFound(error) && has(this.root, "recommendations"))) reportError(error, "list load failed");
3304
- emit("error", { error, context: "list" }, this.root);
3305
- }
3306
- }
3307
- render() {
3308
- const { collection } = this;
3309
- this.$(sel("collection-bind")).forEach((el) => {
3310
- switch (get(el, "collection-bind") ?? "") {
3311
- case "title":
3312
- setText(el, collection?.title ?? "");
3313
- break;
3314
- case "description":
3315
- setStoreHtml(el, collection?.descriptionHtml);
3316
- break;
3317
- case "image":
3318
- setImage(el, collection?.image?.url, collection?.image?.altText || collection?.title);
3319
- break;
3320
- case "count":
3321
- setText(el, this.products.length);
3322
- break;
3323
- }
3324
- });
3325
- this.$(sel("list")).forEach((container) => {
3326
- repeat(container, this.products, (node, product) => {
3327
- node.setAttribute(attr("product"), product.handle);
3328
- node.removeAttribute(attr("state"));
3329
- });
3330
- emit("list-rendered", { container }, this.root);
3331
- });
3332
- const empty = this.products.length === 0;
3333
- this.$(sel("show")).forEach((el) => {
3334
- const key = get(el, "show");
3335
- if (key === "list-empty") show(el, empty);
3336
- if (key === "list-filled") show(el, !empty);
3337
- });
3338
- this.$(sel("load-more")).forEach((el) => {
3339
- show(el, Boolean(this.pageInfo?.hasNextPage));
3340
- });
3341
- notifyLayoutChanged("list");
3342
- }
3343
- };
3344
-
3345
3191
  // src/mounting.ts
3346
3192
  var HYDRATE_SELECTOR = 'script[type="application/json"][data-tether-hydrate]';
3347
3193
  var mountedProducts = /* @__PURE__ */ new WeakSet();
@@ -3581,26 +3427,30 @@
3581
3427
  }
3582
3428
 
3583
3429
  // src/part-integrity.ts
3584
- var PART_INTEGRITY = { "tether.diagnose.js": "sha384-exWttcEw4tQTiJ4OU0r/Y+0T5vVIYIo3kcmieqDPkUUmJQGr2HZzWxZCOmMhsvaY", "tether.markets.js": "sha384-hn9741hCQun49mIj7A2bazMG2wXxTpMDScOQK+8wMeY6KT1p+ovXcKDi3m6BK04C", "tether.search.js": "sha384-ZrByfRsWNOHbbWOocY4WOD2RWe4QI2qQRZ4Eb+fM5v/UHIQ8mlWxbSwrxJt8yLbc", "tether.extras.js": "sha384-lt7I8DOnCx8wwVHQM4ZT39+Jlv95WVED5eVDezUKchNXo4+j1oBXYtxzPFiPyxgm", "tether.account.js": "sha384-UF36poTg1vnXE7Pg+FYFGMJ4qjKhx9mbOug9bqTYhzgqRppOKS9M/1uY5D/cz9KF", "tether.account-pages.js": "sha384-jJYvZswrRw+pGDYrk0FswWcYqGpZkbR6ooxJGFu0lTYkUlVOcsCXuLLLU6M5kO54", "tether.analytics.js": "sha384-10v5pBMzgfF7BuLGQBg3cdbgMk701Uto5rGjhByiRD2jI9+/WF8DFcSWplIcdRNg" };
3430
+ var PART_INTEGRITY = { "tether.diagnose.js": "sha384-6b98ZqhqrOFydwTyklda6rjvyWrgz1OnmffApLQ3+gg6VPSU1Yy3LhR1OT4lprfa", "tether.markets.js": "sha384-f9ZeUfc/tMICwVFPrC44Sa1oxm9c6jTQASJdNhHWDk55thsEVScodDBKSYUiuDgr", "tether.search.js": "sha384-kNQI7aVmvUyNgV2e6+PRy53xmbKiZi2n1hhYmHUDFCGU3BR6SoGpGKVKVbPE0ZtG", "tether.lists.js": "sha384-MfQgl9RjiTn6vr80fQymnFikrsDXh3wcmT8cu858s7z9p/oByQaJUKZo92qhjX1P", "tether.extras.js": "sha384-H4CWsEibHw/99UiBAi366ctyaNZrVMkK6Y1YJYYcGDbNthy5eQdS+RRcrlm1clmQ", "tether.account.js": "sha384-UeOp/wddJlVjmlYcVqqO9hU7ANLYEtY/TtK/y5cFkQ6J+SPBmqI/VReaAh+reCMC", "tether.account-pages.js": "sha384-jBeU+vF/U13v+9LZxg5R8lQb/PDHTXPVvu1EmsdDUrlh7BmIntbZJh2+iT6YykTy", "tether.analytics.js": "sha384-4gJ/lcu+S+sfpEHu64+0m2zbXpvNsyOxKDQchPEIb1SFg3ky1JlXODNSdRoodST9" };
3585
3431
 
3586
3432
  // src/part-registry.ts
3587
3433
  function isTetherInternals(value) {
3588
3434
  return isRecord(value) && isRecord(value["__core"]) && isRecord(value["__parts"]);
3589
3435
  }
3590
3436
 
3437
+ // src/version.ts
3438
+ var VERSION = "0.9.0";
3439
+
3591
3440
  // src/lazy-iife.ts
3592
3441
  var coreModules = {
3593
3442
  get "config.ts"() {
3594
3443
  return [cfg, isConfigured, log, MARKET_SOURCES, parseMarkets, reportError, warn];
3595
3444
  },
3596
3445
  get "client.ts"() {
3597
- return [endpoint, gql, orNull, shaped, TetherError];
3446
+ return [endpoint, gql, matches, orNull, shaped, TetherError];
3598
3447
  },
3599
3448
  get "dom.ts"() {
3600
3449
  return [
3601
3450
  ANALYTICS_EVENTS,
3602
3451
  attr,
3603
3452
  attributesFromJson,
3453
+ busy,
3604
3454
  emit,
3605
3455
  formatDate,
3606
3456
  formatMoney2,
@@ -3608,24 +3458,30 @@
3608
3458
  has,
3609
3459
  hasTemplate,
3610
3460
  isOwnEvent,
3461
+ notifyLayoutChanged,
3611
3462
  parseJSONAttr,
3612
3463
  productUrl2,
3613
3464
  qsa,
3614
3465
  repeat,
3466
+ scoped,
3615
3467
  sel,
3616
3468
  setImage,
3469
+ setStoreHtml,
3617
3470
  setText,
3618
3471
  show
3619
3472
  ];
3620
3473
  },
3621
3474
  get "loader.ts"() {
3622
- return [clearProductCache, getProduct];
3475
+ return [clearProductCache, getProduct, normalizeProductKey, primeProduct, withProductFragment];
3476
+ },
3477
+ get "fragments.ts"() {
3478
+ return [collectionFields];
3623
3479
  },
3624
3480
  get "selling-plans.ts"() {
3625
3481
  return [allPlans];
3626
3482
  },
3627
3483
  get "status.ts"() {
3628
- return [boundary, resolveProductKey];
3484
+ return [boundary, isNotFound, resolveProductKey, setScopeStatus];
3629
3485
  },
3630
3486
  get "cart.ts"() {
3631
3487
  return [Cart];
@@ -3656,11 +3512,18 @@
3656
3512
  var MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
3657
3513
  var coreScript = typeof document === "undefined" ? "" : document.currentScript?.getAttribute("src") ?? "";
3658
3514
  var coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : "";
3515
+ var JSDELIVR_ALIAS = /^(https:\/\/cdn\.jsdelivr\.net\/(gh|npm)\/@?[^/@]+\/[^/@]+)(@(latest|\d+(\.\d+)?))?\//;
3516
+ function exactReleaseFolder(folder) {
3517
+ return folder.replace(
3518
+ JSDELIVR_ALIAS,
3519
+ (_alias, path, source) => `${path}@${source === "gh" ? "v" : ""}${VERSION}/`
3520
+ );
3521
+ }
3659
3522
  var parts = /* @__PURE__ */ new Map();
3660
3523
  function partsFolder() {
3661
3524
  if (cfg.partsUrl) return new URL(cfg.partsUrl, document.baseURI).href;
3662
3525
  if (!coreScriptUrl) return null;
3663
- return coreScriptUrl.replace(/[^/]*$/, "");
3526
+ return exactReleaseFolder(coreScriptUrl.replace(/[^/]*$/, ""));
3664
3527
  }
3665
3528
  function applyIntegrity(script, integrity) {
3666
3529
  script.integrity = integrity;
@@ -3716,6 +3579,19 @@
3716
3579
  return loading;
3717
3580
  }
3718
3581
 
3582
+ // src/collection.ts
3583
+ var listSelector = () => `${sel("collection")},${sel("products")},${sel("recommendations")}`;
3584
+ function mountedList(root) {
3585
+ setScopeStatus(root, "loading");
3586
+ return {
3587
+ root,
3588
+ load: () => loadPart("lists").then((part) => part.loadList(root)).catch((error) => {
3589
+ setScopeStatus(root, "error", error);
3590
+ reportError(error, "lists");
3591
+ })
3592
+ };
3593
+ }
3594
+
3719
3595
  // src/account-marks.ts
3720
3596
  var hintKey = () => `${cfg.prefix}:account-hint:${cfg.domain}`;
3721
3597
  function removeFromBothAreas(key) {
@@ -3778,7 +3654,7 @@
3778
3654
 
3779
3655
  // src/controls/cart-controls.ts
3780
3656
  var lineKeyAttribute = () => `data-${cfg.prefix}-key`;
3781
- var isElement3 = (target) => target instanceof window.Element;
3657
+ var isElement2 = (target) => target instanceof window.Element;
3782
3658
  function lineOf(target) {
3783
3659
  const line = target.closest(`[${lineKeyAttribute()}]`);
3784
3660
  const id = line?.getAttribute(lineKeyAttribute());
@@ -3811,7 +3687,7 @@
3811
3687
  }
3812
3688
  function onClick2(event) {
3813
3689
  const { target } = event;
3814
- if (!isElement3(target)) return;
3690
+ if (!isElement2(target)) return;
3815
3691
  if (target.closest(sel("cart-open"))) {
3816
3692
  event.preventDefault();
3817
3693
  Cart.toggle();
@@ -3848,7 +3724,7 @@
3848
3724
  }
3849
3725
  function onChange2(event) {
3850
3726
  const { target } = event;
3851
- if (!isElement3(target)) return;
3727
+ if (!isElement2(target)) return;
3852
3728
  const found = lineOf(target);
3853
3729
  if (found && target instanceof window.HTMLInputElement && target.matches(sel("line", "quantity"))) {
3854
3730
  showLineBusy(found.line, Cart.updateLine(found.id, parseInt(target.value, 10) || 0), "cart line");
@@ -3874,7 +3750,7 @@
3874
3750
  }
3875
3751
 
3876
3752
  // src/controls/discount.ts
3877
- var isElement4 = (target) => target instanceof window.Element;
3753
+ var isElement3 = (target) => target instanceof window.Element;
3878
3754
  function codeInputOf(form) {
3879
3755
  const input = form.querySelector(sel("discount-input")) ?? form.querySelector("input");
3880
3756
  return input instanceof window.HTMLInputElement ? input : null;
@@ -3898,7 +3774,7 @@
3898
3774
  }
3899
3775
  function onSubmit(event) {
3900
3776
  const { target } = event;
3901
- const form = isElement4(target) ? target.closest(sel("discount-form")) : null;
3777
+ const form = isElement3(target) ? target.closest(sel("discount-form")) : null;
3902
3778
  if (!form) return;
3903
3779
  event.preventDefault();
3904
3780
  const input = codeInputOf(form);
@@ -3908,7 +3784,7 @@
3908
3784
  }
3909
3785
  function onInput(event) {
3910
3786
  const { target } = event;
3911
- if (!isElement4(target)) return;
3787
+ if (!isElement3(target)) return;
3912
3788
  const form = target.closest(sel("discount-form"));
3913
3789
  if (!form) return;
3914
3790
  form.classList.remove("is-invalid");
@@ -3916,7 +3792,7 @@
3916
3792
  }
3917
3793
  function onClick3(event) {
3918
3794
  const { target } = event;
3919
- const remove = isElement4(target) ? target.closest(sel("discount-remove")) : null;
3795
+ const remove = isElement3(target) ? target.closest(sel("discount-remove")) : null;
3920
3796
  if (!remove) return;
3921
3797
  event.preventDefault();
3922
3798
  const code = remove.closest(sel("discount-code"))?.getAttribute(attr("discount-code"));
@@ -4015,7 +3891,6 @@
4015
3891
  }
4016
3892
 
4017
3893
  // src/index.ts
4018
- var VERSION = "0.8.0";
4019
3894
  var OBSERVE_DEBOUNCE_MS = 50;
4020
3895
  function pageConfig() {
4021
3896
  const config = pageGlobal("TETHER_CONFIG");
@@ -4031,7 +3906,7 @@
4031
3906
  if (mountedLists.has(el)) return;
4032
3907
  mountedLists.add(el);
4033
3908
  count++;
4034
- const list = new ListScope(el);
3909
+ const list = mountedList(el);
4035
3910
  liveLists.add(list);
4036
3911
  el.addEventListener(`${cfg.prefix}:list-rendered`, (event) => {
4037
3912
  if (isOwnEvent(event, "list-rendered")) mount(event.detail.container);
@@ -4163,6 +4038,8 @@
4163
4038
  observer.observe(document.body, { childList: true, subtree: true });
4164
4039
  }
4165
4040
  var detectMarket = (markets) => loadPart("markets").then((part) => part.detectMarket(markets ?? cfg.markets));
4041
+ var getProducts = (query2) => loadPart("lists").then((part) => part.getProducts(query2));
4042
+ var getRecommendations = (product, intent) => loadPart("lists").then((part) => part.getRecommendations(product, intent));
4166
4043
  var getLocalization = () => loadPart("markets").then((part) => part.getLocalization());
4167
4044
  var diagnose = (options) => loadPart("diagnose").then((part) => part.diagnose({ ...options, version: VERSION }));
4168
4045
  function on(name, listener) {