@occluze/tether 0.3.6 → 0.5.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 (45) hide show
  1. package/README.md +12 -7
  2. package/dist/esm/astro.js +2 -0
  3. package/dist/esm/chunks/chunk-HHMOPDZG.js +1 -0
  4. package/dist/esm/chunks/chunk-HXKV6QGB.js +4 -0
  5. package/dist/esm/chunks/chunk-RBVRRQIU.js +1 -0
  6. package/dist/esm/chunks/chunk-RNZMTQTX.js +1 -0
  7. package/dist/esm/chunks/chunk-VC46IEJQ.js +1 -0
  8. package/dist/esm/chunks/diagnose-J667GHGS.js +1 -0
  9. package/dist/esm/chunks/markets-ui-EGWIEUOZ.js +1 -0
  10. package/dist/esm/index.js +1 -0
  11. package/dist/esm/server.js +1 -0
  12. package/dist/presets/barba.esm.js +4 -5
  13. package/dist/tether.diagnose.js +320 -0
  14. package/dist/tether.diagnose.min.js +1 -0
  15. package/dist/tether.esm.js +2 -2608
  16. package/dist/tether.js +637 -833
  17. package/dist/tether.markets.js +213 -0
  18. package/dist/tether.markets.min.js +1 -0
  19. package/dist/tether.min.js +3 -132
  20. package/dist/tether.min.js.map +4 -4
  21. package/package.json +52 -11
  22. package/src/astro.js +73 -0
  23. package/src/cart.js +79 -7
  24. package/src/client.js +25 -15
  25. package/src/config.js +4 -0
  26. package/src/diagnose.js +1 -1
  27. package/src/dom.js +32 -9
  28. package/src/fragments.js +12 -11
  29. package/src/index.js +80 -160
  30. package/src/initial.js +66 -0
  31. package/src/lazy-iife.js +43 -0
  32. package/src/lazy.js +9 -0
  33. package/src/loader.js +50 -7
  34. package/src/markets-ui.js +89 -0
  35. package/src/markets.js +1 -1
  36. package/src/mounting.js +129 -0
  37. package/src/parts/diagnose.js +3 -0
  38. package/src/parts/markets.js +3 -0
  39. package/src/product.js +86 -61
  40. package/src/server.js +179 -0
  41. package/src/status.js +2 -6
  42. package/types/astro.d.ts +15 -0
  43. package/types/barba.d.ts +16 -0
  44. package/types/index.d.ts +347 -0
  45. package/types/server.d.ts +94 -0
package/dist/tether.js CHANGED
@@ -6,6 +6,16 @@
6
6
  };
7
7
 
8
8
  // src/config.js
9
+ var config_exports = {};
10
+ __export(config_exports, {
11
+ DEFAULTS: () => DEFAULTS,
12
+ cfg: () => cfg,
13
+ configure: () => configure,
14
+ isConfigured: () => isConfigured,
15
+ log: () => log,
16
+ reportError: () => reportError,
17
+ warn: () => warn
18
+ });
9
19
  var DEFAULTS = {
10
20
  // Shopify Storefront API
11
21
  domain: "",
@@ -36,11 +46,15 @@
36
46
  sellingPlansFirst: 25,
37
47
  collectionFirst: 24,
38
48
  retries: 2,
49
+ revalidate: true,
50
+ // products from the server (tether/server hydrate()): refresh once in the background
39
51
  // Cart
40
52
  storageKey: null,
41
53
  // defaults to `tether:cart:<domain>`
42
54
  cartDrawerOpenClass: "is-open",
43
55
  openCartOnAdd: true,
56
+ cartFocus: true,
57
+ // move focus into the open cart, keep Tab inside it, restore it on close
44
58
  checkoutTarget: "_self",
45
59
  // '_self' or '_blank'
46
60
  // Subscriptions
@@ -51,6 +65,10 @@
51
65
  // mount document on DOMContentLoaded
52
66
  observe: false,
53
67
  // auto-mount nodes added later (CMS load-more, filters)
68
+ astro: true,
69
+ // mount on Astro View Transitions (astro:page-load); false if you do it yourself
70
+ partsUrl: null,
71
+ // folder of tether.markets/diagnose(.min).js for the <script> build (default: next to it)
54
72
  // Formatting
55
73
  formatMoney: null,
56
74
  // (money, locale) => string
@@ -98,6 +116,15 @@
98
116
  }
99
117
 
100
118
  // src/client.js
119
+ var client_exports = {};
120
+ __export(client_exports, {
121
+ TetherError: () => TetherError,
122
+ endpoint: () => endpoint,
123
+ gql: () => gql,
124
+ request: () => request,
125
+ unwrap: () => unwrap,
126
+ withContext: () => withContext
127
+ });
101
128
  var TetherError = class extends Error {
102
129
  constructor(message, { status, errors, userErrors, code } = {}) {
103
130
  super(message);
@@ -108,40 +135,38 @@
108
135
  this.userErrors = userErrors;
109
136
  }
110
137
  };
111
- function endpoint() {
112
- return `https://${cfg.domain}/api/${cfg.apiVersion}/graphql.json`;
138
+ function endpoint(conf = cfg) {
139
+ return `https://${conf.domain}/api/${conf.apiVersion}/graphql.json`;
113
140
  }
114
141
  function withContext(query, ctx = cfg) {
115
142
  const args = [];
116
143
  if (ctx.country) args.push(`country: ${String(ctx.country).toUpperCase()}`);
117
144
  if (ctx.language) args.push(`language: ${String(ctx.language).toUpperCase().replace("-", "_")}`);
118
145
  if (!args.length || /@inContext/.test(query)) return query;
119
- return query.replace(
120
- /^(\s*(?:query|mutation)\s+\w+\s*(?:\([^)]*\))?)/,
121
- `$1 @inContext(${args.join(", ")})`
122
- );
146
+ const tag = `@inContext(${args.join(", ")})`;
147
+ if (/^\s*\{/.test(query)) return `query ${tag} ${query.trimStart()}`;
148
+ return query.replace(/^(\s*(?:query|mutation)(?:\s+\w+)?\s*(?:\([^)]*\))?)/, `$1 ${tag}`);
123
149
  }
124
150
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
125
- async function gql(query, variables = {}, { context = true } = {}) {
126
- var _a;
127
- if (!cfg.domain || !cfg.token) {
151
+ async function request(conf, query, variables = {}, { context = true } = {}) {
152
+ if (!conf.domain || !conf.token) {
128
153
  throw new TetherError("Tether is not configured: domain and token are required", { code: "NOT_CONFIGURED" });
129
154
  }
130
- const body = JSON.stringify({ query: context ? withContext(query) : query, variables });
155
+ const body = JSON.stringify({ query: context ? withContext(query, conf) : query, variables });
131
156
  let attempt = 0;
132
157
  for (; ; ) {
133
158
  let res;
134
159
  try {
135
- res = await fetch(endpoint(), {
160
+ res = await (conf.fetch || fetch)(endpoint(conf), {
136
161
  method: "POST",
137
162
  headers: {
138
163
  "Content-Type": "application/json",
139
- "X-Shopify-Storefront-Access-Token": cfg.token
164
+ "X-Shopify-Storefront-Access-Token": conf.token
140
165
  },
141
166
  body
142
167
  });
143
168
  } catch (err) {
144
- if (attempt++ < cfg.retries) {
169
+ if (attempt++ < conf.retries) {
145
170
  await sleep(300 * 2 ** attempt);
146
171
  continue;
147
172
  }
@@ -149,7 +174,7 @@
149
174
  }
150
175
  if (res.status === 430) throw new TetherError("Shopify security rejection (430)", { status: 430 });
151
176
  const retryable = res.status === 429 || res.status >= 500;
152
- if (retryable && attempt++ < cfg.retries) {
177
+ if (retryable && attempt++ < conf.retries) {
153
178
  log("retry", res.status, attempt);
154
179
  await sleep(300 * 2 ** attempt);
155
180
  continue;
@@ -160,17 +185,17 @@
160
185
  } catch {
161
186
  throw new TetherError(`Invalid response (${res.status})`, { status: res.status });
162
187
  }
163
- if ((_a = json.errors) == null ? void 0 : _a.length) {
188
+ if (json.errors?.length) {
164
189
  const inventoryDenied = json.errors.some((e) => /quantityAvailable/.test(e.message || ""));
165
190
  if (inventoryDenied && /\bquantityAvailable\b/.test(body)) {
166
191
  log("no inventory permission, retrying without quantityAvailable");
167
- return gql(query.replace(/\bquantityAvailable\b/g, ""), variables, { context });
192
+ return request(conf, query.replace(/\bquantityAvailable\b/g, ""), variables, { context });
168
193
  }
169
- const throttled = json.errors.some((e) => {
170
- var _a2;
171
- return ((_a2 = e.extensions) == null ? void 0 : _a2.code) === "THROTTLED";
172
- });
173
- if (throttled && attempt++ < cfg.retries) {
194
+ if (json.errors.some((e) => /'warnings'/.test(e.message || "")) && /warnings \{ code message \}/.test(query)) {
195
+ return request(conf, query.replace(/ warnings \{ code message \}/g, ""), variables, { context });
196
+ }
197
+ const throttled = json.errors.some((e) => e.extensions?.code === "THROTTLED");
198
+ if (throttled && attempt++ < conf.retries) {
174
199
  await sleep(500 * 2 ** attempt);
175
200
  continue;
176
201
  }
@@ -183,6 +208,7 @@
183
208
  return json.data;
184
209
  }
185
210
  }
211
+ var gql = (query, variables, opts) => request(cfg, query, variables, opts);
186
212
  function unwrap(payload, name) {
187
213
  if (!payload) throw new TetherError(`${name}: empty response`);
188
214
  const errs = payload.userErrors || [];
@@ -192,11 +218,28 @@
192
218
  return payload;
193
219
  }
194
220
 
221
+ // src/cart.js
222
+ var cart_exports = {};
223
+ __export(cart_exports, {
224
+ Cart: () => Cart,
225
+ focusables: () => focusables,
226
+ toLineInput: () => toLineInput
227
+ });
228
+
195
229
  // src/fragments.js
230
+ var fragments_exports = {};
231
+ __export(fragments_exports, {
232
+ ALLOCATION_FIELDS: () => ALLOCATION_FIELDS,
233
+ SELLING_PLAN_FIELDS: () => SELLING_PLAN_FIELDS,
234
+ cartFragment: () => cartFragment,
235
+ collectionFields: () => collectionFields,
236
+ metafieldIdentifiers: () => metafieldIdentifiers,
237
+ productFragment: () => productFragment
238
+ });
196
239
  var MONEY = "amount currencyCode";
197
240
  var IMAGE = "url altText width height";
198
- function metafieldIdentifiers() {
199
- return (cfg.metafields || []).map((m) => {
241
+ function metafieldIdentifiers(o = cfg) {
242
+ return (o.metafields || []).map((m) => {
200
243
  if (typeof m === "string") {
201
244
  const i = m.indexOf(".");
202
245
  return { namespace: m.slice(0, i), key: m.slice(i + 1) };
@@ -204,116 +247,19 @@
204
247
  return m;
205
248
  }).filter((m) => m.namespace && m.key);
206
249
  }
207
- function metafieldsField() {
208
- const ids = metafieldIdentifiers();
250
+ function metafieldsField(o = cfg) {
251
+ const ids = metafieldIdentifiers(o);
209
252
  if (!ids.length) return "";
210
253
  const list = ids.map((m) => `{namespace: ${JSON.stringify(m.namespace)}, key: ${JSON.stringify(m.key)}}`).join(", ");
211
- return `metafields(identifiers: [${list}]) {
212
- namespace key type value
213
- reference { ... on MediaImage { image { ${IMAGE} } } }
214
- }`;
215
- }
216
- var SELLING_PLAN_FIELDS = `
217
- id name description recurringDeliveries
218
- options { name value }
219
- priceAdjustments {
220
- orderCount
221
- adjustmentValue {
222
- __typename
223
- ... on SellingPlanPercentagePriceAdjustment { adjustmentPercentage }
224
- ... on SellingPlanFixedAmountPriceAdjustment { adjustmentAmount { ${MONEY} } }
225
- ... on SellingPlanFixedPriceAdjustment { price { ${MONEY} } }
226
- }
227
- }`;
228
- var ALLOCATION_FIELDS = `
229
- sellingPlan { id }
230
- checkoutChargeAmount { ${MONEY} }
231
- remainingBalanceChargeAmount { ${MONEY} }
232
- priceAdjustments {
233
- price { ${MONEY} }
234
- compareAtPrice { ${MONEY} }
235
- perDeliveryPrice { ${MONEY} }
236
- unitPrice { ${MONEY} }
237
- }`;
238
- function productFragment() {
239
- return `
240
- fragment TetherProduct on Product {
241
- id handle title description descriptionHtml productType vendor tags
242
- availableForSale publishedAt onlineStoreUrl requiresSellingPlan
243
- seo { title description }
244
- featuredImage { ${IMAGE} }
245
- images(first: ${cfg.imagesFirst}) { nodes { ${IMAGE} } }
246
- options { name optionValues { name swatch { color image { previewImage { ${IMAGE} } } } } }
247
- priceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
248
- compareAtPriceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
249
- ${metafieldsField()}
250
- sellingPlanGroups(first: ${cfg.sellingPlanGroupsFirst}) {
251
- nodes {
252
- name appName
253
- options { name values }
254
- sellingPlans(first: ${cfg.sellingPlansFirst}) { nodes { ${SELLING_PLAN_FIELDS} } }
255
- }
256
- }
257
- variants(first: ${cfg.variantsFirst}) {
258
- nodes {
259
- id title availableForSale currentlyNotInStock quantityAvailable sku barcode
260
- selectedOptions { name value }
261
- image { ${IMAGE} }
262
- price { ${MONEY} }
263
- compareAtPrice { ${MONEY} }
264
- unitPrice { ${MONEY} }
265
- sellingPlanAllocations(first: ${cfg.sellingPlansFirst}) { nodes { ${ALLOCATION_FIELDS} } }
266
- }
267
- }
268
- }`;
254
+ return `metafields(identifiers:[${list}]){namespace key type value reference{... on MediaImage{image{${IMAGE}}}}}`;
269
255
  }
270
- function cartFragment() {
271
- return `
272
- fragment TetherCart on Cart {
273
- id checkoutUrl totalQuantity note createdAt updatedAt
274
- attributes { key value }
275
- buyerIdentity { countryCode email }
276
- discountCodes { code applicable }
277
- cost {
278
- subtotalAmount { ${MONEY} }
279
- totalAmount { ${MONEY} }
280
- checkoutChargeAmount { ${MONEY} }
281
- }
282
- lines(first: 100) {
283
- nodes {
284
- id quantity
285
- attributes { key value }
286
- cost {
287
- totalAmount { ${MONEY} }
288
- subtotalAmount { ${MONEY} }
289
- amountPerQuantity { ${MONEY} }
290
- compareAtAmountPerQuantity { ${MONEY} }
291
- }
292
- discountAllocations { discountedAmount { ${MONEY} } }
293
- sellingPlanAllocation {
294
- sellingPlan { id name description recurringDeliveries options { name value } }
295
- checkoutChargeAmount { ${MONEY} }
296
- priceAdjustments { price { ${MONEY} } compareAtPrice { ${MONEY} } perDeliveryPrice { ${MONEY} } }
297
- }
298
- merchandise {
299
- ... on ProductVariant {
300
- id title sku availableForSale requiresShipping
301
- image { ${IMAGE} }
302
- price { ${MONEY} }
303
- compareAtPrice { ${MONEY} }
304
- selectedOptions { name value }
305
- product { id title handle vendor featuredImage { ${IMAGE} } }
306
- }
307
- }
308
- ... on ComponentizableCartLine {
309
- lineComponents {
310
- id quantity
311
- merchandise { ... on ProductVariant { id title product { title handle } } }
312
- }
313
- }
314
- }
256
+ var SELLING_PLAN_FIELDS = ` id name description recurringDeliveries options{name value}priceAdjustments{orderCount adjustmentValue{__typename ... on SellingPlanPercentagePriceAdjustment{adjustmentPercentage}... on SellingPlanFixedAmountPriceAdjustment{adjustmentAmount{${MONEY}}}... on SellingPlanFixedPriceAdjustment{price{${MONEY}}}}}`;
257
+ var ALLOCATION_FIELDS = ` sellingPlan{id}checkoutChargeAmount{${MONEY}}remainingBalanceChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}unitPrice{${MONEY}}}`;
258
+ function productFragment(o = cfg) {
259
+ return ` fragment TetherProduct on Product{id handle title description descriptionHtml productType vendor tags availableForSale publishedAt onlineStoreUrl requiresSellingPlan seo{title description}featuredImage{${IMAGE}}images(first:${o.imagesFirst}){nodes{${IMAGE}}}options{name optionValues{name swatch{color image{previewImage{${IMAGE}}}}}}priceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}compareAtPriceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}${metafieldsField(o)} sellingPlanGroups(first:${o.sellingPlanGroupsFirst}){nodes{name appName options{name values}sellingPlans(first:${o.sellingPlansFirst}){nodes{${SELLING_PLAN_FIELDS}}}}}variants(first:${o.variantsFirst}){nodes{id title availableForSale currentlyNotInStock quantityAvailable sku barcode selectedOptions{name value}image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}unitPrice{${MONEY}}sellingPlanAllocations(first:${o.sellingPlansFirst}){nodes{${ALLOCATION_FIELDS}}}}}}`;
315
260
  }
316
- }`;
261
+ function cartFragment() {
262
+ 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 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}}}}}}}}`;
317
263
  }
318
264
  function collectionFields() {
319
265
  return `
@@ -323,6 +269,18 @@ fragment TetherCart on Cart {
323
269
  }
324
270
 
325
271
  // src/loader.js
272
+ var loader_exports = {};
273
+ __export(loader_exports, {
274
+ clearProductCache: () => clearProductCache,
275
+ getProduct: () => getProduct,
276
+ getProducts: () => getProducts,
277
+ getRecommendations: () => getRecommendations,
278
+ hydrateProducts: () => hydrateProducts,
279
+ normalizeProductKey: () => normalizeProductKey,
280
+ primeProduct: () => primeProduct,
281
+ refreshProducts: () => refreshProducts,
282
+ takeStale: () => takeStale
283
+ });
326
284
  var productCache = /* @__PURE__ */ new Map();
327
285
  var pending = [];
328
286
  var resolvers = /* @__PURE__ */ new Map();
@@ -330,7 +288,7 @@ fragment TetherCart on Cart {
330
288
  var generation = 0;
331
289
  var isGid = (v) => typeof v === "string" && v.startsWith("gid://");
332
290
  function normalizeProductKey(raw) {
333
- const v = String(raw != null ? raw : "").trim();
291
+ const v = String(raw ?? "").trim();
334
292
  if (!v) return null;
335
293
  if (isGid(v)) return v;
336
294
  if (/^\d+$/.test(v)) return `gid://shopify/Product/${v}`;
@@ -366,22 +324,19 @@ fragment TetherCart on Cart {
366
324
  const ids = keys.filter(isGid);
367
325
  const handles = keys.filter((k) => !isGid(k));
368
326
  const defs = [];
369
- const parts = [];
327
+ const parts2 = [];
370
328
  const vars = {};
371
329
  if (ids.length) {
372
330
  defs.push("$ids: [ID!]!");
373
331
  vars.ids = ids;
374
- parts.push("byId: nodes(ids: $ids) { ... on Product { ...TetherProduct } }");
332
+ parts2.push("byId: nodes(ids: $ids) { ... on Product { ...TetherProduct } }");
375
333
  }
376
334
  handles.forEach((h, i) => {
377
335
  defs.push(`$h${i}: String!`);
378
336
  vars[`h${i}`] = h;
379
- parts.push(`h${i}: product(handle: $h${i}) { ...TetherProduct }`);
337
+ parts2.push(`h${i}: product(handle: $h${i}) { ...TetherProduct }`);
380
338
  });
381
- const query = `query TetherProducts(${defs.join(", ")}) {
382
- ${parts.join("\n ")}
383
- }
384
- ${productFragment()}`;
339
+ const query = `query TetherProducts(${defs.join(",")}){${parts2.join(" ")}}${productFragment()}`;
385
340
  log("fetch products", keys);
386
341
  const settle = (k, fn) => {
387
342
  const r = resolvers.get(k);
@@ -399,8 +354,13 @@ ${productFragment()}`;
399
354
  });
400
355
  keys.forEach((k) => settle(k, (r) => {
401
356
  if (got.has(k)) {
402
- if (gen === generation) primeProduct(got.get(k));
403
- r.resolve(got.get(k));
357
+ const p = got.get(k);
358
+ if (gen === generation) {
359
+ primeProduct(p);
360
+ freshAt.set(p.handle, Date.now());
361
+ freshAt.set(p.id, Date.now());
362
+ }
363
+ r.resolve(p);
404
364
  } else {
405
365
  if (gen === generation) productCache.delete(k);
406
366
  r.reject(new TetherError(`Product not found: ${k}`, { code: "NOT_FOUND" }));
@@ -416,6 +376,35 @@ ${productFragment()}`;
416
376
  function clearProductCache() {
417
377
  productCache.clear();
418
378
  generation++;
379
+ stale.clear();
380
+ freshAt.clear();
381
+ }
382
+ var stale = /* @__PURE__ */ new Set();
383
+ var freshAt = /* @__PURE__ */ new Map();
384
+ var FRESH_MS = 6e4;
385
+ function hydrateProducts(products) {
386
+ (products || []).forEach((p) => {
387
+ if (!p?.id || !p.handle) return;
388
+ if (Date.now() - (freshAt.get(p.handle) || 0) < FRESH_MS && productCache.has(p.handle)) return;
389
+ primeProduct(p);
390
+ stale.add(p.handle);
391
+ stale.add(p.id);
392
+ });
393
+ }
394
+ function takeStale(key) {
395
+ const k = normalizeProductKey(key);
396
+ if (!stale.has(k)) return false;
397
+ const p = productCache.get(k);
398
+ stale.delete(k);
399
+ p?.then?.((x) => {
400
+ stale.delete(x.handle);
401
+ stale.delete(x.id);
402
+ });
403
+ return true;
404
+ }
405
+ function refreshProducts(keys) {
406
+ keys.forEach((k) => productCache.delete(normalizeProductKey(k)));
407
+ return keys.map((k) => getProduct(k));
419
408
  }
420
409
  async function getProducts({
421
410
  collection,
@@ -428,16 +417,7 @@ ${productFragment()}`;
428
417
  } = {}) {
429
418
  const fragment = productFragment();
430
419
  if (collection) {
431
- const data2 = await gql(`query TetherCollection($handle: String!, $first: Int!, $after: String, $sortKey: ProductCollectionSortKeys, $reverse: Boolean, $filters: [ProductFilter!]) {
432
- collection(handle: $handle) {
433
- ${collectionFields()}
434
- products(first: $first, after: $after, sortKey: $sortKey, reverse: $reverse, filters: $filters) {
435
- nodes { ...TetherProduct }
436
- pageInfo { hasNextPage endCursor }
437
- }
438
- }
439
- }
440
- ${fragment}`, {
420
+ const data2 = await gql(`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}}}}${fragment}`, {
441
421
  handle: collection,
442
422
  first,
443
423
  after,
@@ -450,21 +430,14 @@ ${fragment}`, {
450
430
  products.nodes.forEach(primeProduct);
451
431
  return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
452
432
  }
453
- const data = await gql(`query TetherSearch($first: Int!, $after: String, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
454
- products(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
455
- nodes { ...TetherProduct }
456
- pageInfo { hasNextPage endCursor }
457
- }
458
- }
459
- ${fragment}`, { first, after, query: query || void 0, sortKey: sortKey || void 0, reverse });
433
+ const data = await gql(`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}}}${fragment}`, { first, after, query: query || void 0, sortKey: sortKey || void 0, reverse });
460
434
  data.products.nodes.forEach(primeProduct);
461
435
  return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
462
436
  }
463
- async function getRecommendations(productId, intent = "RELATED") {
464
- const data = await gql(`query TetherRecs($id: ID!, $intent: ProductRecommendationIntent) {
465
- productRecommendations(productId: $id, intent: $intent) { ...TetherProduct }
466
- }
467
- ${productFragment()}`, { id: productId, intent });
437
+ async function getRecommendations(product, intent = "RELATED") {
438
+ const key = normalizeProductKey(typeof product === "object" ? product?.id : product);
439
+ const byId = key?.startsWith("gid://");
440
+ const data = await gql(`query TetherRecs(${byId ? "$id: ID!" : "$handle: String!"},$intent:ProductRecommendationIntent){productRecommendations(${byId ? "productId: $id" : "productHandle: $handle"},intent:$intent){...TetherProduct}}${productFragment()}`, byId ? { id: key, intent } : { handle: key, intent });
468
441
  const list = data.productRecommendations || [];
469
442
  list.forEach(primeProduct);
470
443
  return list;
@@ -491,49 +464,38 @@ ${productFragment()}`, { id: productId, intent });
491
464
  sellingPlan: (v) => toGid(v, "SellingPlan")
492
465
  };
493
466
  function toGid(value, type) {
494
- const v = String(value != null ? value : "").trim();
467
+ const v = String(value ?? "").trim();
495
468
  if (!v) return null;
496
469
  if (v.startsWith("gid://")) return v;
497
470
  if (/^\d+$/.test(v)) return `gid://shopify/${type}/${v}`;
498
471
  return v;
499
472
  }
500
473
  function allPlans(product) {
501
- var _a;
502
- const groups = ((_a = product == null ? void 0 : product.sellingPlanGroups) == null ? void 0 : _a.nodes) || [];
503
- return groups.flatMap((g) => {
504
- var _a2;
505
- return (((_a2 = g.sellingPlans) == null ? void 0 : _a2.nodes) || []).map((p) => ({ ...p, group: g }));
506
- });
474
+ const groups = product?.sellingPlanGroups?.nodes || [];
475
+ return groups.flatMap((g) => (g.sellingPlans?.nodes || []).map((p) => ({ ...p, group: g })));
507
476
  }
508
477
  function hasPlans(product) {
509
478
  return allPlans(product).length > 0;
510
479
  }
511
480
  function allocationFor(variant, planId) {
512
- var _a;
513
481
  if (!variant || !planId) return null;
514
- return (((_a = variant.sellingPlanAllocations) == null ? void 0 : _a.nodes) || []).find((a) => {
515
- var _a2;
516
- return ((_a2 = a.sellingPlan) == null ? void 0 : _a2.id) === planId;
517
- }) || null;
482
+ return (variant.sellingPlanAllocations?.nodes || []).find((a) => a.sellingPlan?.id === planId) || null;
518
483
  }
519
484
  function plansForVariant(product, variant) {
520
485
  return allPlans(product).filter((p) => allocationFor(variant, p.id));
521
486
  }
522
487
  function planPrice(variant, planId) {
523
- var _a;
524
488
  const a = allocationFor(variant, planId);
525
- const adj = (_a = a == null ? void 0 : a.priceAdjustments) == null ? void 0 : _a[0];
526
- return (adj == null ? void 0 : adj.perDeliveryPrice) || (adj == null ? void 0 : adj.price) || (variant == null ? void 0 : variant.price) || null;
489
+ const adj = a?.priceAdjustments?.[0];
490
+ return adj?.perDeliveryPrice || adj?.price || variant?.price || null;
527
491
  }
528
492
  function planCheckoutCharge(variant, planId) {
529
- var _a, _b;
530
493
  const a = allocationFor(variant, planId);
531
- return (a == null ? void 0 : a.checkoutChargeAmount) || ((_b = (_a = a == null ? void 0 : a.priceAdjustments) == null ? void 0 : _a[0]) == null ? void 0 : _b.price) || (variant == null ? void 0 : variant.price) || null;
494
+ return a?.checkoutChargeAmount || a?.priceAdjustments?.[0]?.price || variant?.price || null;
532
495
  }
533
496
  function planSavings(variant, planId) {
534
- var _a, _b;
535
- const base = parseFloat((_a = variant == null ? void 0 : variant.price) == null ? void 0 : _a.amount);
536
- const sub = parseFloat((_b = planPrice(variant, planId)) == null ? void 0 : _b.amount);
497
+ const base = parseFloat(variant?.price?.amount);
498
+ const sub = parseFloat(planPrice(variant, planId)?.amount);
537
499
  if (!Number.isFinite(base) || !Number.isFinite(sub) || base <= 0 || sub >= base) {
538
500
  return { amount: null, percentage: 0 };
539
501
  }
@@ -543,8 +505,7 @@ ${productFragment()}`, { id: productId, intent });
543
505
  };
544
506
  }
545
507
  function planAdjustment(plan) {
546
- var _a, _b;
547
- const v = (_b = (_a = plan == null ? void 0 : plan.priceAdjustments) == null ? void 0 : _a[0]) == null ? void 0 : _b.adjustmentValue;
508
+ const v = plan?.priceAdjustments?.[0]?.adjustmentValue;
548
509
  if (!v) return null;
549
510
  switch (v.__typename) {
550
511
  case "SellingPlanPercentagePriceAdjustment":
@@ -566,6 +527,37 @@ ${productFragment()}`, { id: productId, intent });
566
527
  }
567
528
 
568
529
  // src/dom.js
530
+ var dom_exports = {};
531
+ __export(dom_exports, {
532
+ attr: () => attr,
533
+ busy: () => busy,
534
+ cdnUrl: () => cdnUrl,
535
+ dataKey: () => dataKey,
536
+ emit: () => emit,
537
+ formatDate: () => formatDate,
538
+ formatMoney: () => formatMoney,
539
+ get: () => get,
540
+ has: () => has,
541
+ hasTemplate: () => hasTemplate,
542
+ imageUrl: () => imageUrl,
543
+ notifyLayoutChanged: () => notifyLayoutChanged,
544
+ parseJSONAttr: () => parseJSONAttr,
545
+ productUrl: () => productUrl,
546
+ qsa: () => qsa,
547
+ readText: () => readText,
548
+ repeat: () => repeat,
549
+ repeatKeyed: () => repeatKeyed,
550
+ sameImage: () => sameImage,
551
+ scoped: () => scoped,
552
+ sel: () => sel,
553
+ setImage: () => setImage,
554
+ setSelected: () => setSelected,
555
+ setText: () => setText,
556
+ setTextTracked: () => setTextTracked,
557
+ show: () => show,
558
+ toAttributes: () => toAttributes,
559
+ watchImages: () => watchImages
560
+ });
569
561
  var attr = (name) => `${cfg.prefix}-${name}`;
570
562
  var sel = (name, value) => value === void 0 ? `[${attr(name)}]` : `[${attr(name)}="${value}"]`;
571
563
  var get = (el, name) => el.getAttribute(attr(name));
@@ -604,25 +596,31 @@ ${productFragment()}`, { id: productId, intent });
604
596
  return "";
605
597
  }
606
598
  }
607
- function cdnUrl(url, el) {
608
- if (!url || !el) return url;
609
- const w = el.getAttribute("max-width");
610
- const h = el.getAttribute("max-height");
611
- const crop = el.getAttribute("crop");
612
- if (!w && !h) return url;
599
+ function imageUrl(url, { width, height, crop } = {}) {
600
+ if (!url) return url;
613
601
  try {
614
602
  const u = new URL(url);
615
- if (w) u.searchParams.set("width", w);
616
- if (h) u.searchParams.set("height", h);
603
+ if (width) u.searchParams.set("width", String(width));
604
+ if (height) u.searchParams.set("height", String(height));
617
605
  if (crop) u.searchParams.set("crop", crop);
618
606
  return u.toString();
619
607
  } catch {
620
608
  return url;
621
609
  }
622
610
  }
611
+ function cdnUrl(url, el) {
612
+ if (!url || !el) return url;
613
+ const g = (k) => el.getAttribute(k);
614
+ return imageUrl(url, { width: g("max-width"), height: g("max-height"), crop: g("max-width") || g("max-height") ? g("crop") : null });
615
+ }
616
+ var sameImage = (a, b) => Boolean(a && b) && a.split("?")[0] === b.split("?")[0];
623
617
  function setImage(el, url, alt) {
624
618
  if (!url) return;
625
619
  const src = cdnUrl(url, el);
620
+ if (el.tagName === "IMG" && sameImage(el.getAttribute("src"), src)) {
621
+ if (alt != null && !el.getAttribute("alt")) el.setAttribute("alt", alt);
622
+ return;
623
+ }
626
624
  if (el.tagName === "IMG") {
627
625
  el.removeAttribute("srcset");
628
626
  el.removeAttribute("sizes");
@@ -654,31 +652,36 @@ ${productFragment()}`, { id: productId, intent });
654
652
  function show(el, visible) {
655
653
  const k = "tetherPrevDisplay";
656
654
  if (visible) {
655
+ if (el.dataset.tetherShown !== void 0) return;
656
+ el.dataset.tetherShown = "";
657
+ el.removeAttribute("hidden");
657
658
  if (el.dataset[k] !== void 0) {
658
659
  el.style.display = el.dataset[k];
659
660
  delete el.dataset[k];
660
661
  } else if (el.style.display === "none") {
661
662
  el.style.display = "";
662
663
  }
664
+ if (typeof getComputedStyle === "function" && getComputedStyle(el).display === "none") {
665
+ el.style.display = el.getAttribute(`${cfg.prefix}-display`) || "block";
666
+ }
663
667
  } else {
664
- if (el.dataset[k] === void 0) el.dataset[k] = el.style.display || "";
668
+ delete el.dataset.tetherShown;
669
+ if (el.dataset[k] === void 0 && el.style.display !== "none") el.dataset[k] = el.style.display || "";
665
670
  el.style.display = "none";
666
671
  }
667
672
  }
668
673
  function setSelected(el, selected) {
669
- var _a;
670
674
  el.classList.toggle("is-selected", selected);
671
675
  el.setAttribute("aria-pressed", selected ? "true" : "false");
672
676
  if (el.tagName === "INPUT" && (el.type === "radio" || el.type === "checkbox")) el.checked = selected;
673
- const input = (_a = el.querySelector) == null ? void 0 : _a.call(el, 'input[type="radio"],input[type="checkbox"]');
677
+ const input = el.querySelector?.('input[type="radio"],input[type="checkbox"]');
674
678
  if (input) input.checked = selected;
675
679
  }
676
680
  var templates = /* @__PURE__ */ new WeakMap();
677
681
  var hasTemplate = (container) => Boolean(templates.get(container));
678
682
  function template(container) {
679
- var _a, _b;
680
683
  if (!templates.has(container)) {
681
- const tpl = (_b = (_a = container.firstElementChild) == null ? void 0 : _a.cloneNode(true)) != null ? _b : null;
684
+ const tpl = container.firstElementChild?.cloneNode(true) ?? null;
682
685
  templates.set(container, tpl);
683
686
  container.innerHTML = "";
684
687
  }
@@ -760,9 +763,8 @@ ${productFragment()}`, { id: productId, intent });
760
763
  function notifyLayoutChanged(reason) {
761
764
  clearTimeout(layoutTimer);
762
765
  layoutTimer = setTimeout(() => {
763
- var _a;
764
766
  if (typeof cfg.onLayoutChange === "function") cfg.onLayoutChange(reason);
765
- else if ((_a = window.ScrollTrigger) == null ? void 0 : _a.refresh) window.ScrollTrigger.refresh();
767
+ else if (window.ScrollTrigger?.refresh) window.ScrollTrigger.refresh();
766
768
  emit("layout-changed", { reason });
767
769
  }, 120);
768
770
  }
@@ -795,20 +797,16 @@ ${productFragment()}`, { id: productId, intent });
795
797
  }
796
798
  function toAttributes(obj) {
797
799
  if (!obj) return [];
798
- if (Array.isArray(obj)) return obj.map((a) => {
799
- var _a;
800
- return { key: String(a.key), value: String((_a = a.value) != null ? _a : "") };
801
- });
802
- return Object.entries(obj).map(([key, value]) => ({ key, value: String(value != null ? value : "") }));
800
+ if (Array.isArray(obj)) return obj.map((a) => ({ key: String(a.key), value: String(a.value ?? "") }));
801
+ return Object.entries(obj).map(([key, value]) => ({ key, value: String(value ?? "") }));
803
802
  }
804
803
 
805
804
  // src/cart.js
806
805
  function toLineInput(input, quantity = 1, opts = {}) {
807
- var _a;
808
806
  const src = typeof input === "object" && input !== null ? input : { variantId: input, quantity, ...opts };
809
807
  const merchandiseId = GID.variant(src.merchandiseId || src.variantId || src.id);
810
808
  if (!merchandiseId) throw new TetherError("add(): variantId is required");
811
- const line = { merchandiseId, quantity: Math.max(1, parseInt((_a = src.quantity) != null ? _a : 1, 10) || 1) };
809
+ const line = { merchandiseId, quantity: Math.max(1, parseInt(src.quantity ?? 1, 10) || 1) };
812
810
  const plan = src.sellingPlanId || src.sellingPlan;
813
811
  if (plan) line.sellingPlanId = GID.sellingPlan(plan);
814
812
  const attrs = toAttributes(src.attributes);
@@ -816,12 +814,21 @@ ${productFragment()}`, { id: productId, intent });
816
814
  return line;
817
815
  }
818
816
  function mutation(name, args, inner) {
819
- return `mutation Tether_${name}(${args}) {
820
- ${name}(${inner}) { cart { ...TetherCart } userErrors { field message } }
821
- }
822
- ${cartFragment()}`;
817
+ return `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}warnings{code message}}}${cartFragment()}`;
818
+ }
819
+ var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
820
+ var returnFocus = null;
821
+ var focusables = (d) => qsa(d, FOCUSABLE).filter((el) => el.offsetParent !== null || el === document.activeElement);
822
+ function ariaState(open) {
823
+ qsa(document, sel("cart-open")).forEach((b) => b.setAttribute("aria-expanded", String(open)));
824
+ qsa(document, sel("cart-drawer")).forEach((d) => {
825
+ if (!d.hasAttribute("role")) d.setAttribute("role", "dialog");
826
+ if (!d.hasAttribute("aria-label") && !d.hasAttribute("aria-labelledby")) d.setAttribute("aria-label", get(d, "cart-label") || "Cart");
827
+ d.setAttribute("aria-modal", String(open));
828
+ if (!d.classList.contains(cfg.cartDrawerOpenClass)) d.setAttribute("aria-hidden", "true");
829
+ });
823
830
  }
824
- var isMissingCart = (err) => /does not exist|not found|invalid id/i.test((err == null ? void 0 : err.message) || "");
831
+ var isMissingCart = (err) => /does not exist|not found|invalid id/i.test(err?.message || "");
825
832
  var Cart = {
826
833
  state: null,
827
834
  _chain: Promise.resolve(),
@@ -840,14 +847,22 @@ ${cartFragment()}`;
840
847
  }
841
848
  },
842
849
  get lines() {
843
- var _a, _b;
844
- return ((_b = (_a = this.state) == null ? void 0 : _a.lines) == null ? void 0 : _b.nodes) || [];
850
+ return this.state?.lines?.nodes || [];
845
851
  },
846
852
  get count() {
847
- var _a;
848
- return ((_a = this.state) == null ? void 0 : _a.totalQuantity) || 0;
853
+ return this.state?.totalQuantity || 0;
849
854
  },
850
855
  lastError: null,
856
+ /** Shopify's notes on the last change, e.g. less stock than asked: [{ code, message }]. */
857
+ warnings: [],
858
+ /** true once the saved cart (or the absence of one) is known. */
859
+ loaded: false,
860
+ _take(payload, name) {
861
+ const p = unwrap(payload, name);
862
+ this.warnings = p.warnings || [];
863
+ if (this.warnings.length) emit("cart-warning", { warnings: this.warnings });
864
+ return p.cart;
865
+ },
851
866
  _queue(fn) {
852
867
  const run = this._chain.then(fn, fn).then((value) => {
853
868
  if (this.lastError) {
@@ -867,7 +882,7 @@ ${cartFragment()}`;
867
882
  },
868
883
  _commit(cart, reason) {
869
884
  this.state = cart;
870
- if (cart == null ? void 0 : cart.id) this._setId(cart.id);
885
+ if (cart?.id) this._setId(cart.id);
871
886
  this.render();
872
887
  emit("cart-updated", { cart, reason });
873
888
  return cart;
@@ -876,12 +891,13 @@ ${cartFragment()}`;
876
891
  const id = this.id();
877
892
  if (!id) {
878
893
  this.state = null;
894
+ this.loaded = true;
879
895
  this.render();
896
+ emit("cart-updated", { cart: null, reason: "load" });
880
897
  return null;
881
898
  }
882
899
  try {
883
- const data = await gql(`query TetherCart($id: ID!) { cart(id: $id) { ...TetherCart } }
884
- ${cartFragment()}`, { id });
900
+ const data = await gql(`query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${cartFragment()}`, { id });
885
901
  if (!data.cart) {
886
902
  this._setId(null);
887
903
  this.state = null;
@@ -892,7 +908,9 @@ ${cartFragment()}`, { id });
892
908
  if (isMissingCart(err)) this._setId(null);
893
909
  this.state = null;
894
910
  }
911
+ this.loaded = true;
895
912
  this.render();
913
+ emit("cart-updated", { cart: this.state, reason: "load" });
896
914
  return this.state;
897
915
  },
898
916
  async _create(input = {}) {
@@ -901,7 +919,7 @@ ${cartFragment()}`, { id });
901
919
  mutation("cartCreate", "$input: CartInput!", "input: $input"),
902
920
  { input: { buyerIdentity, ...input } }
903
921
  );
904
- return unwrap(data.cartCreate, "cartCreate").cart;
922
+ return this._take(data.cartCreate, "cartCreate");
905
923
  },
906
924
  /** Runs a cart mutation; creates a fresh cart if ours expired. */
907
925
  async _mutate(name, args, inner, variables, createInput) {
@@ -909,7 +927,7 @@ ${cartFragment()}`, { id });
909
927
  if (!cartId) return createInput ? this._create(createInput) : null;
910
928
  try {
911
929
  const data = await gql(mutation(name, `$cartId: ID!, ${args}`, `cartId: $cartId, ${inner}`), { ...variables, cartId });
912
- return unwrap(data[name], name).cart;
930
+ return this._take(data[name], name);
913
931
  } catch (err) {
914
932
  if (createInput && isMissingCart(err)) {
915
933
  log("cart expired, creating a new one");
@@ -966,14 +984,13 @@ ${cartFragment()}`, { id });
966
984
  },
967
985
  setNote(note) {
968
986
  return this._queue(async () => this._commit(
969
- await this._mutate("cartNoteUpdate", "$note: String!", "note: $note", { note: note != null ? note : "" }, { note: note != null ? note : "" }),
987
+ await this._mutate("cartNoteUpdate", "$note: String!", "note: $note", { note: note ?? "" }, { note: note ?? "" }),
970
988
  "note"
971
989
  ));
972
990
  },
973
991
  /** Cart-level attributes, merged with existing ones. Value '' removes a key. */
974
992
  setAttributes(obj) {
975
- var _a;
976
- const merged = new Map((((_a = this.state) == null ? void 0 : _a.attributes) || []).map((a) => [a.key, a.value]));
993
+ const merged = new Map((this.state?.attributes || []).map((a) => [a.key, a.value]));
977
994
  toAttributes(obj).forEach((a) => merged.set(a.key, a.value));
978
995
  const attributes = [...merged].filter(([, v]) => v !== "").map(([key, value]) => ({ key, value }));
979
996
  return this._queue(async () => this._commit(
@@ -990,13 +1007,12 @@ ${cartFragment()}`, { id });
990
1007
  ));
991
1008
  },
992
1009
  applyDiscount(code) {
993
- var _a;
994
- const current = (((_a = this.state) == null ? void 0 : _a.discountCodes) || []).map((d) => d.code);
1010
+ const current = (this.state?.discountCodes || []).map((d) => d.code);
995
1011
  return this.setDiscountCodes([.../* @__PURE__ */ new Set([...current, code])]);
996
1012
  },
997
1013
  removeDiscount(code) {
998
- var _a;
999
- return this.setDiscountCodes((((_a = this.state) == null ? void 0 : _a.discountCodes) || []).map((d) => d.code).filter((c) => c !== code));
1014
+ const k = String(code).toLowerCase();
1015
+ return this.setDiscountCodes((this.state?.discountCodes || []).map((d) => d.code).filter((c) => c.toLowerCase() !== k));
1000
1016
  },
1001
1017
  /** { countryCode, email, phone } — changes currency/prices for that market. */
1002
1018
  setBuyerIdentity(buyerIdentity, { create = true } = {}) {
@@ -1015,9 +1031,8 @@ ${cartFragment()}`, { id });
1015
1031
  * URL once we have it.
1016
1032
  */
1017
1033
  async checkout(target = cfg.checkoutTarget) {
1018
- var _a;
1019
1034
  const id = this.id();
1020
- if (!id) return null;
1035
+ if (!id || this.state && !this.count) return null;
1021
1036
  const tab = target === "_blank" ? window.open("", "_blank") : null;
1022
1037
  if (tab) {
1023
1038
  try {
@@ -1027,7 +1042,7 @@ ${cartFragment()}`, { id });
1027
1042
  }
1028
1043
  try {
1029
1044
  const data = await gql("query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }", { id });
1030
- const url = (_a = data.cart) == null ? void 0 : _a.checkoutUrl;
1045
+ const url = data.cart?.checkoutUrl;
1031
1046
  if (!url) throw new TetherError("No checkoutUrl (cart empty or expired)");
1032
1047
  emit("checkout", { url, cart: this.state });
1033
1048
  if (tab && !tab.closed) tab.location.href = url;
@@ -1053,16 +1068,29 @@ ${cartFragment()}`, { id });
1053
1068
  d.classList.add(cfg.cartDrawerOpenClass);
1054
1069
  d.setAttribute("aria-hidden", "false");
1055
1070
  });
1071
+ ariaState(true);
1072
+ if (cfg.cartFocus !== false && ds[0]) {
1073
+ returnFocus = document.activeElement;
1074
+ setTimeout(() => {
1075
+ const target = focusables(ds[0])[0] || ds[0];
1076
+ if (target === ds[0] && !ds[0].hasAttribute("tabindex")) ds[0].tabIndex = -1;
1077
+ target.focus?.({ preventScroll: true });
1078
+ }, 60);
1079
+ }
1056
1080
  if (typeof cfg.onCartOpen === "function") cfg.onCartOpen(ds);
1057
1081
  emit("cart-toggled", { open: true });
1058
1082
  },
1059
1083
  close() {
1060
1084
  if (!this.isOpen()) return;
1061
1085
  const ds = this.drawers();
1086
+ const hadFocus = ds.some((d) => d.contains(document.activeElement));
1062
1087
  ds.forEach((d) => {
1063
1088
  d.classList.remove(cfg.cartDrawerOpenClass);
1064
1089
  d.setAttribute("aria-hidden", "true");
1065
1090
  });
1091
+ ariaState(false);
1092
+ if (hadFocus && returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
1093
+ returnFocus = null;
1066
1094
  if (typeof cfg.onCartClose === "function") cfg.onCartClose(ds);
1067
1095
  emit("cart-toggled", { open: false });
1068
1096
  },
@@ -1070,14 +1098,23 @@ ${cartFragment()}`, { id });
1070
1098
  if (this.isOpen()) this.close();
1071
1099
  else this.open();
1072
1100
  },
1101
+ /**
1102
+ * For framework islands (React, Svelte, Vue): fn(cart, { open }) now and on every change.
1103
+ * Returns a function that stops listening.
1104
+ */
1105
+ subscribe(fn) {
1106
+ const call = () => fn(this.state, { open: this.isOpen(), count: this.count, loaded: this.loaded, warnings: this.warnings });
1107
+ const h = () => call();
1108
+ ["cart-updated", "cart-toggled"].forEach((e) => document.addEventListener(`${cfg.prefix}:${e}`, h));
1109
+ call();
1110
+ return () => ["cart-updated", "cart-toggled"].forEach((e) => document.removeEventListener(`${cfg.prefix}:${e}`, h));
1111
+ },
1073
1112
  // -------------------------------------------------------------- render
1074
1113
  render() {
1075
1114
  const cart = this.state;
1076
1115
  const count = cart ? cart.totalQuantity : 0;
1077
- const money = (k) => {
1078
- var _a;
1079
- return cart ? formatMoney((_a = cart.cost) == null ? void 0 : _a[k]) : "";
1080
- };
1116
+ const money = (k) => cart ? formatMoney(cart.cost?.[k]) : "";
1117
+ ariaState(this.isOpen());
1081
1118
  qsa(document, sel("cart-count")).forEach((el) => setTextTracked(el, count));
1082
1119
  qsa(document, sel("cart-subtotal")).forEach((el) => setTextTracked(el, money("subtotalAmount")));
1083
1120
  qsa(document, sel("cart-total")).forEach((el) => setTextTracked(el, money("totalAmount")));
@@ -1088,18 +1125,24 @@ ${cartFragment()}`, { id });
1088
1125
  "cart-filled": count > 0,
1089
1126
  "cart-has-subscription": hasSub,
1090
1127
  "cart-no-subscription": !hasSub,
1091
- "cart-has-discount": ((cart == null ? void 0 : cart.discountCodes) || []).some((d) => d.applicable),
1092
- "cart-error": Boolean(this.lastError)
1128
+ "cart-has-discount": (cart?.discountCodes || []).some((d) => d.applicable),
1129
+ "cart-error": Boolean(this.lastError),
1130
+ "cart-warning": this.warnings.length > 0
1093
1131
  };
1132
+ qsa(document, sel("cart-warning")).forEach((el) => setTextTracked(el, [...new Set(this.warnings.map((w) => w.message))].join(" ")));
1094
1133
  qsa(document, sel("show")).forEach((el) => {
1095
1134
  const k = get(el, "show");
1096
1135
  if (k in conds && !el.closest(sel("product"))) show(el, conds[k]);
1097
1136
  });
1137
+ qsa(document, sel("checkout")).forEach((el) => {
1138
+ el.classList.toggle("is-disabled", !count);
1139
+ el.setAttribute("aria-disabled", String(!count));
1140
+ });
1098
1141
  qsa(document, `textarea${sel("cart-note")},input${sel("cart-note")}`).forEach((el) => {
1099
- if (document.activeElement !== el) el.value = (cart == null ? void 0 : cart.note) || "";
1142
+ if (document.activeElement !== el) el.value = cart?.note || "";
1100
1143
  });
1101
1144
  qsa(document, sel("cart-discounts")).forEach((container) => {
1102
- repeat(container, (cart == null ? void 0 : cart.discountCodes) || [], (node, d) => {
1145
+ repeat(container, cart?.discountCodes || [], (node, d) => {
1103
1146
  node.setAttribute(attr("discount-code"), d.code);
1104
1147
  const t = node.querySelector(sel("discount", "code")) || (node.firstElementChild ? null : node);
1105
1148
  if (t) setText(t, d.code);
@@ -1116,60 +1159,62 @@ ${cartFragment()}`, { id });
1116
1159
  }
1117
1160
  };
1118
1161
  function lineValue(line, key) {
1119
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1120
1162
  const m = line.merchandise || {};
1121
- const plan = (_a = line.sellingPlanAllocation) == null ? void 0 : _a.sellingPlan;
1163
+ const plan = line.sellingPlanAllocation?.sellingPlan;
1122
1164
  switch (key) {
1123
1165
  case "title":
1124
- return (_b = m.product) == null ? void 0 : _b.title;
1166
+ return m.product?.title;
1125
1167
  case "product-title":
1126
- return (_c = m.product) == null ? void 0 : _c.title;
1168
+ return m.product?.title;
1127
1169
  case "variant-title":
1128
1170
  return m.title === "Default Title" ? "" : m.title;
1129
1171
  case "options":
1130
1172
  return (m.selectedOptions || []).filter((o) => o.value !== "Default Title").map((o) => `${o.name}: ${o.value}`).join(", ");
1131
1173
  case "vendor":
1132
- return (_d = m.product) == null ? void 0 : _d.vendor;
1174
+ return m.product?.vendor;
1133
1175
  case "sku":
1134
1176
  return m.sku;
1135
1177
  case "price":
1136
- return formatMoney(((_e = line.cost) == null ? void 0 : _e.amountPerQuantity) || m.price);
1178
+ return formatMoney(line.cost?.amountPerQuantity || m.price);
1137
1179
  case "compare-at-price":
1138
- return formatMoney(((_f = line.cost) == null ? void 0 : _f.compareAtAmountPerQuantity) || m.compareAtPrice);
1180
+ return formatMoney(line.cost?.compareAtAmountPerQuantity || m.compareAtPrice);
1139
1181
  case "line-total":
1140
- return formatMoney((_g = line.cost) == null ? void 0 : _g.totalAmount);
1182
+ return formatMoney(line.cost?.totalAmount);
1141
1183
  case "line-subtotal":
1142
- return formatMoney((_h = line.cost) == null ? void 0 : _h.subtotalAmount);
1184
+ return formatMoney(line.cost?.subtotalAmount);
1143
1185
  case "quantity":
1144
1186
  return line.quantity;
1145
1187
  case "plan":
1146
- return (plan == null ? void 0 : plan.name) || "";
1188
+ return plan?.name || "";
1147
1189
  case "plan-description":
1148
- return (plan == null ? void 0 : plan.description) || "";
1190
+ return plan?.description || "";
1149
1191
  case "components":
1150
- return (line.lineComponents || []).map((c) => {
1151
- var _a2, _b2;
1152
- return `${c.quantity} \xD7 ${((_b2 = (_a2 = c.merchandise) == null ? void 0 : _a2.product) == null ? void 0 : _b2.title) || ""}`;
1153
- }).join(", ");
1192
+ return (line.lineComponents || []).map((c) => `${c.quantity} \xD7 ${c.merchandise?.product?.title || ""}`).join(", ");
1154
1193
  default:
1155
1194
  if (key.startsWith("attribute:")) {
1156
1195
  const k = key.slice("attribute:".length);
1157
- return ((_i = (line.attributes || []).find((a) => a.key === k)) == null ? void 0 : _i.value) || "";
1196
+ return (line.attributes || []).find((a) => a.key === k)?.value || "";
1158
1197
  }
1159
1198
  return void 0;
1160
1199
  }
1161
1200
  }
1162
1201
  function renderLine(node, line) {
1163
- var _a, _b, _c, _d;
1164
1202
  const m = line.merchandise || {};
1165
1203
  const isSub = Boolean(line.sellingPlanAllocation);
1166
1204
  node.setAttribute(attr("line-id"), line.id);
1167
1205
  node.classList.toggle("is-subscription", isSub);
1168
1206
  qsa(node, sel("line")).forEach((el) => {
1169
- var _a2, _b2, _c2, _d2, _e, _f;
1170
1207
  const key = get(el, "line");
1171
- if (key === "image") return setImage(el, ((_a2 = m.image) == null ? void 0 : _a2.url) || ((_c2 = (_b2 = m.product) == null ? void 0 : _b2.featuredImage) == null ? void 0 : _c2.url), (_d2 = m.product) == null ? void 0 : _d2.title);
1172
- if (key === "url") return el.setAttribute("href", productUrl((_e = m.product) == null ? void 0 : _e.handle, (_f = m.product) == null ? void 0 : _f.id));
1208
+ if (key === "image") return setImage(el, m.image?.url || m.product?.featuredImage?.url, m.product?.title);
1209
+ if (key === "url") {
1210
+ const u = productUrl(m.product?.handle, m.product?.id);
1211
+ const q = new URLSearchParams();
1212
+ if (m.id) q.set("variant", String(m.id).split("/").pop());
1213
+ const plan = line.sellingPlanAllocation?.sellingPlan?.id;
1214
+ if (plan) q.set("selling_plan", plan.split("/").pop());
1215
+ const qs = q.toString();
1216
+ return el.setAttribute("href", qs ? `${u}${u.includes("?") ? "&" : "?"}${qs}` : u);
1217
+ }
1173
1218
  const v = lineValue(line, key);
1174
1219
  if (v !== void 0) setTextTracked(el, v);
1175
1220
  return void 0;
@@ -1177,7 +1222,7 @@ ${cartFragment()}`, { id });
1177
1222
  const lineConds = {
1178
1223
  subscription: isSub,
1179
1224
  "one-time": !isSub,
1180
- "on-sale": parseFloat((_b = (_a = line.cost) == null ? void 0 : _a.compareAtAmountPerQuantity) == null ? void 0 : _b.amount) > parseFloat((_d = (_c = line.cost) == null ? void 0 : _c.amountPerQuantity) == null ? void 0 : _d.amount),
1225
+ "on-sale": parseFloat(line.cost?.compareAtAmountPerQuantity?.amount) > parseFloat(line.cost?.amountPerQuantity?.amount),
1181
1226
  "has-components": (line.lineComponents || []).length > 0
1182
1227
  };
1183
1228
  qsa(node, sel("line-show")).forEach((el) => {
@@ -1185,11 +1230,9 @@ ${cartFragment()}`, { id });
1185
1230
  if (k in lineConds) show(el, lineConds[k]);
1186
1231
  });
1187
1232
  qsa(node, `select${sel("line-plan")}`).forEach((select) => {
1188
- var _a2;
1189
- const handle = (_a2 = m.product) == null ? void 0 : _a2.handle;
1233
+ const handle = m.product?.handle;
1190
1234
  if (!handle) return;
1191
1235
  getProduct(handle).then((product) => {
1192
- var _a3, _b2;
1193
1236
  const plans = allPlans(product);
1194
1237
  const allowOneTime = !product.requiresSellingPlan && get(select, "line-plan") !== "subscription-only";
1195
1238
  const want = [
@@ -1207,21 +1250,38 @@ ${cartFragment()}`, { id });
1207
1250
  });
1208
1251
  select.dataset.tetherSig = sig;
1209
1252
  }
1210
- select.value = ((_b2 = (_a3 = line.sellingPlanAllocation) == null ? void 0 : _a3.sellingPlan) == null ? void 0 : _b2.id) || "";
1253
+ select.value = line.sellingPlanAllocation?.sellingPlan?.id || "";
1211
1254
  show(select, plans.length > 0);
1212
1255
  }).catch((err) => reportError(err, "line plan options"));
1213
1256
  });
1214
1257
  }
1215
1258
 
1259
+ // src/product.js
1260
+ var product_exports = {};
1261
+ __export(product_exports, {
1262
+ ProductScope: () => ProductScope,
1263
+ rememberOptionNames: () => rememberOptionNames,
1264
+ scopes: () => scopes
1265
+ });
1266
+
1216
1267
  // src/status.js
1268
+ var status_exports = {};
1269
+ __export(status_exports, {
1270
+ STATUS_KEYS: () => STATUS_KEYS,
1271
+ boundary: () => boundary,
1272
+ isNotFound: () => isNotFound,
1273
+ ownedBy: () => ownedBy,
1274
+ resolveProductKey: () => resolveProductKey,
1275
+ setScopeStatus: () => setScopeStatus
1276
+ });
1217
1277
  function resolveProductKey(el, location = window.location) {
1218
1278
  const raw = (get(el, "product") || "").trim();
1219
1279
  if (!raw.toLowerCase().startsWith("url:")) return { key: raw || null, fromUrl: false, raw };
1220
1280
  const what = raw.slice(4).trim();
1221
1281
  let key = null;
1222
1282
  if (what.toLowerCase() === "path") {
1223
- const parts = location.pathname.split("/").filter(Boolean);
1224
- key = parts.length ? safeDecode(parts[parts.length - 1]) : null;
1283
+ const parts2 = location.pathname.split("/").filter(Boolean);
1284
+ key = parts2.length ? safeDecode(parts2[parts2.length - 1]) : null;
1225
1285
  } else {
1226
1286
  key = new URLSearchParams(location.search).get(what || "handle");
1227
1287
  }
@@ -1236,29 +1296,23 @@ ${cartFragment()}`, { id });
1236
1296
  }
1237
1297
  }
1238
1298
  var STATUS_KEYS = ["loading", "ready", "error", "not-found"];
1239
- var isNotFound = (err) => (err == null ? void 0 : err.code) === "NOT_FOUND";
1299
+ var isNotFound = (err) => err?.code === "NOT_FOUND";
1240
1300
  var boundary = () => ["product", "collection", "products", "recommendations"].map((n) => sel(n)).join(",");
1241
- var ownedBy = (el, root) => {
1242
- var _a;
1243
- return el === root || ((_a = el.parentElement) == null ? void 0 : _a.closest(boundary())) === root;
1244
- };
1301
+ var ownedBy = (el, root) => el === root || el.parentElement?.closest(boundary()) === root;
1245
1302
  function setScopeStatus(root, status, err) {
1246
1303
  const notFound = status === "not-found";
1247
1304
  root.setAttribute(attr("state"), notFound ? "error" : status);
1248
- if (status === "error" || notFound) root.setAttribute(attr("error"), notFound ? "not-found" : (err == null ? void 0 : err.code) || "error");
1305
+ if (status === "error" || notFound) root.setAttribute(attr("error"), notFound ? "not-found" : err?.code || "error");
1249
1306
  else root.removeAttribute(attr("error"));
1250
1307
  scoped(root, sel("show"), boundary()).forEach((el) => {
1251
1308
  const k = get(el, "show");
1252
1309
  if (!STATUS_KEYS.includes(k)) return;
1253
- const visible = k === status;
1254
- show(el, visible);
1255
- if (visible && typeof getComputedStyle === "function" && getComputedStyle(el).display === "none") {
1256
- el.style.display = el.getAttribute(attr("display")) || "block";
1257
- }
1310
+ show(el, k === status);
1258
1311
  });
1259
1312
  if (status === "error" || notFound) {
1260
1313
  scoped(root, sel("add-to-cart"), boundary()).forEach((btn) => {
1261
1314
  btn.classList.add("is-disabled");
1315
+ btn.setAttribute("aria-disabled", "true");
1262
1316
  if ("disabled" in btn) btn.disabled = true;
1263
1317
  });
1264
1318
  }
@@ -1274,14 +1328,19 @@ ${cartFragment()}`, { id });
1274
1328
  const mf = (product.metafields || []).find((m) => m && m.namespace === ns && m.key === key);
1275
1329
  return mf || null;
1276
1330
  }
1331
+ var knownOptionNames = /* @__PURE__ */ new Map();
1332
+ function rememberOptionNames(products) {
1333
+ (products || []).forEach((p) => {
1334
+ if (p?.handle && !knownOptionNames.has(p.handle)) knownOptionNames.set(p.handle, (p.options || []).map((o) => o.name));
1335
+ });
1336
+ }
1277
1337
  var ProductScope = class {
1278
1338
  constructor(root, product) {
1279
- var _a;
1280
1339
  this.root = root;
1281
1340
  this.product = product;
1282
1341
  this.selected = {};
1283
1342
  this.variant = null;
1284
- this.quantity = Math.max(0, parseInt((_a = get(root, "quantity")) != null ? _a : "1", 10) || (has(root, "quantity") ? 0 : 1));
1343
+ this.quantity = Math.max(0, parseInt(get(root, "quantity") ?? "1", 10) || (has(root, "quantity") ? 0 : 1));
1285
1344
  this.plans = allPlans(product);
1286
1345
  this.purchase = "one-time";
1287
1346
  this.planId = null;
@@ -1299,11 +1358,10 @@ ${cartFragment()}`, { id });
1299
1358
  }
1300
1359
  /** New data for the same product (e.g. after a country switch). Keeps the selection. */
1301
1360
  update(product) {
1302
- var _a, _b;
1303
- const variantId = (_a = this.variant) == null ? void 0 : _a.id;
1361
+ const variantId = this.variant?.id;
1304
1362
  this.product = product;
1305
1363
  this.plans = allPlans(product);
1306
- if (this.planId && !this.plans.some((p) => p.id === this.planId)) this.planId = ((_b = this.plans[0]) == null ? void 0 : _b.id) || null;
1364
+ if (this.planId && !this.plans.some((p) => p.id === this.planId)) this.planId = this.plans[0]?.id || null;
1307
1365
  if (!this.plans.length && this.purchase === "subscription") this.purchase = "one-time";
1308
1366
  const v = product.variants.nodes.find((x) => x.id === variantId);
1309
1367
  if (v) {
@@ -1325,6 +1383,13 @@ ${cartFragment()}`, { id });
1325
1383
  return this.root.matches(s) ? [this.root, ...inner] : inner;
1326
1384
  }
1327
1385
  // ------------------------------------------------------------ state
1386
+ /** The product's option name for a name used in the markup (same name, or same position). */
1387
+ optName(authored) {
1388
+ const opts = this.product?.options || [];
1389
+ if (!authored || opts.some((o) => o.name === authored)) return authored;
1390
+ const i = (knownOptionNames.get(this.product.handle) || []).indexOf(authored);
1391
+ return i >= 0 && opts[i] ? opts[i].name : authored;
1392
+ }
1328
1393
  initSelection() {
1329
1394
  const vs = this.product.variants.nodes;
1330
1395
  const first = vs.find((v) => v.availableForSale) || vs[0];
@@ -1425,8 +1490,7 @@ ${cartFragment()}`, { id });
1425
1490
  this.renderVariant();
1426
1491
  }
1427
1492
  async addToCart() {
1428
- var _a;
1429
- if (!((_a = this.variant) == null ? void 0 : _a.availableForSale)) return null;
1493
+ if (!this.variant?.availableForSale) return null;
1430
1494
  const line = this.getLine();
1431
1495
  if (!line || line.quantity < 1) return null;
1432
1496
  const cart = await Cart.add(line);
@@ -1438,11 +1502,11 @@ ${cartFragment()}`, { id });
1438
1502
  const own = (el) => el && el.closest(productSel()) === this.root;
1439
1503
  this.root.addEventListener("click", (e) => {
1440
1504
  const t = e.target;
1441
- if (!(t == null ? void 0 : t.closest)) return;
1505
+ if (!t?.closest) return;
1442
1506
  const opt = t.closest(sel("option-value"));
1443
1507
  if (own(opt) && opt.closest(sel("option-list"))) {
1444
1508
  e.preventDefault();
1445
- this.select(get(opt.closest(sel("option-list")), "option-list"), get(opt, "option-value"));
1509
+ this.select(this.optName(get(opt.closest(sel("option-list")), "option-list")), get(opt, "option-value"));
1446
1510
  return;
1447
1511
  }
1448
1512
  const plan = t.closest(sel("plan-value"));
@@ -1478,7 +1542,7 @@ ${cartFragment()}`, { id });
1478
1542
  this.root.addEventListener("change", (e) => {
1479
1543
  const t = e.target;
1480
1544
  if (!own(t)) return;
1481
- if (t.matches(`select${sel("option")}`)) this.select(get(t, "option"), t.value);
1545
+ if (t.matches(`select${sel("option")}`)) this.select(this.optName(get(t, "option")), t.value);
1482
1546
  else if (t.matches(`select${sel("variant-select")}`)) this.selectVariant(t.value);
1483
1547
  else if (t.matches(`select${sel("plan")}`)) {
1484
1548
  if (t.value) this.selectPlan(t.value);
@@ -1508,24 +1572,26 @@ ${cartFragment()}`, { id });
1508
1572
  }
1509
1573
  // ------------------------------------------------------------ render
1510
1574
  renderProduct() {
1575
+ const p0 = this.product;
1576
+ if (!knownOptionNames.has(p0.handle)) {
1577
+ const authored = this.$(`${sel("option-list")},select${sel("option")}`).map((e) => get(e, "option-list") || get(e, "option"));
1578
+ if (authored.some((n) => p0.options.some((o) => o.name === n))) rememberOptionNames([p0]);
1579
+ }
1511
1580
  const p = this.product;
1512
1581
  this.$(sel("bind")).forEach((el) => {
1513
- var _a, _b, _c;
1514
- const key = get(el, "bind");
1582
+ const key = get(el, "bind") || "";
1583
+ const txt = {
1584
+ title: p.title,
1585
+ "description-text": p.description,
1586
+ vendor: p.vendor,
1587
+ type: p.productType,
1588
+ handle: p.handle
1589
+ }[key];
1590
+ if (txt !== void 0) return setText(el, txt);
1515
1591
  switch (key) {
1516
- case "title":
1517
- return setText(el, p.title);
1518
1592
  case "description":
1519
1593
  el.innerHTML = p.descriptionHtml || "";
1520
1594
  return void 0;
1521
- case "description-text":
1522
- return setText(el, p.description);
1523
- case "vendor":
1524
- return setText(el, p.vendor);
1525
- case "type":
1526
- return setText(el, p.productType);
1527
- case "handle":
1528
- return setText(el, p.handle);
1529
1595
  case "published-at":
1530
1596
  return setText(el, formatDate(p.publishedAt));
1531
1597
  case "min-price":
@@ -1533,13 +1599,17 @@ ${cartFragment()}`, { id });
1533
1599
  case "max-price":
1534
1600
  return setText(el, formatMoney(p.priceRange.maxVariantPrice));
1535
1601
  case "featured-image":
1536
- return setImage(el, (_a = p.featuredImage) == null ? void 0 : _a.url, ((_b = p.featuredImage) == null ? void 0 : _b.altText) || p.title);
1602
+ return setImage(el, p.featuredImage?.url, p.featuredImage?.altText || p.title);
1537
1603
  case "url":
1538
1604
  case "link":
1539
1605
  el.setAttribute("href", productUrl(p.handle, p.id));
1540
1606
  if (key === "url" && !el.firstElementChild && !el.textContent.trim()) setText(el, p.title);
1541
1607
  return void 0;
1542
1608
  case "images":
1609
+ {
1610
+ const imgs = [...el.children].map((n) => n.tagName === "IMG" ? n : n.querySelector("img"));
1611
+ if (!hasTemplate(el) && imgs.length === p.images.nodes.length && imgs.every((im, i) => sameImage(im?.getAttribute("src"), p.images.nodes[i].url))) return void 0;
1612
+ }
1543
1613
  return repeat(el, p.images.nodes, (node, img) => {
1544
1614
  const target = node.matches(`img,${sel("bind", "image")}`) ? node : node.querySelector("img") || node;
1545
1615
  setImage(target, img.url, img.altText || p.title);
@@ -1549,41 +1619,36 @@ ${cartFragment()}`, { id });
1549
1619
  return repeat(el, p.tags.filter((t) => !exclude.includes(t)), (node, tag) => setText(node, tag));
1550
1620
  }
1551
1621
  default:
1552
- if (key && /^image:\d+$/.test(key)) {
1553
- const img = p.images.nodes[parseInt(key.slice(6), 10) - 1] || p.featuredImage;
1554
- return setImage(el, img == null ? void 0 : img.url, (img == null ? void 0 : img.altText) || p.title);
1622
+ if (/^image:\d+$/.test(key)) {
1623
+ const img = p.images.nodes[key.slice(6) - 1] || p.featuredImage;
1624
+ return setImage(el, img?.url, img?.altText || p.title);
1555
1625
  }
1556
- if (key && key.startsWith("metafield:")) {
1557
- const mf = metafieldValue(p, key.slice("metafield:".length));
1626
+ if (key.startsWith("metafield:")) {
1627
+ const mf = metafieldValue(p, key.slice(10));
1558
1628
  if (!mf) return setText(el, "");
1559
- if ((_c = mf.reference) == null ? void 0 : _c.image) return setImage(el, mf.reference.image.url, mf.reference.image.altText);
1560
- if (mf.type === "rich_text_field" || mf.type === "multi_line_text_field") {
1561
- el.textContent = mf.value;
1562
- el.style.whiteSpace = "pre-line";
1563
- return void 0;
1564
- }
1629
+ if (mf.reference?.image) return setImage(el, mf.reference.image.url, mf.reference.image.altText);
1630
+ if (/^(rich_text|multi_line_text)_field$/.test(mf.type)) el.style.whiteSpace = "pre-line";
1565
1631
  return setText(el, mf.value);
1566
1632
  }
1567
1633
  return void 0;
1568
1634
  }
1569
1635
  });
1570
1636
  this.$(sel("option-list")).forEach((container) => {
1571
- const name = get(container, "option-list");
1637
+ const name = this.optName(get(container, "option-list"));
1572
1638
  const opt = p.options.find((o) => o.name === name);
1573
1639
  if (!opt || opt.optionValues.length <= 1 && has(container, "hide-single")) return show(container, false);
1574
1640
  return repeat(container, opt.optionValues, (node, v) => {
1575
- var _a, _b, _c;
1576
1641
  node.setAttribute(attr("option-value"), v.name);
1577
1642
  const t = node.querySelector(sel("bind", "option-value"));
1578
1643
  if (t) setText(t, v.name);
1579
1644
  else if (!node.firstElementChild) setText(node, v.name);
1580
1645
  const sw = node.querySelector(sel("bind", "swatch"));
1581
- if (sw && ((_a = v.swatch) == null ? void 0 : _a.color)) sw.style.backgroundColor = v.swatch.color;
1582
- if (sw && ((_c = (_b = v.swatch) == null ? void 0 : _b.image) == null ? void 0 : _c.previewImage)) setImage(sw, v.swatch.image.previewImage.url, v.name);
1646
+ if (sw && v.swatch?.color) sw.style.backgroundColor = v.swatch.color;
1647
+ if (sw && v.swatch?.image?.previewImage) setImage(sw, v.swatch.image.previewImage.url, v.name);
1583
1648
  });
1584
1649
  });
1585
1650
  this.$(`select${sel("option")}`).forEach((s) => {
1586
- const opt = p.options.find((o) => o.name === get(s, "option"));
1651
+ const opt = p.options.find((o) => o.name === this.optName(get(s, "option")));
1587
1652
  if (!opt) return show(s, false);
1588
1653
  s.innerHTML = "";
1589
1654
  opt.optionValues.forEach((v) => s.appendChild(new Option(v.name, v.name)));
@@ -1611,7 +1676,6 @@ ${cartFragment()}`, { id });
1611
1676
  });
1612
1677
  }
1613
1678
  renderVariant() {
1614
- var _a, _b, _c;
1615
1679
  const p = this.product;
1616
1680
  let v = this.findVariant();
1617
1681
  if (!v) {
@@ -1633,67 +1697,44 @@ ${cartFragment()}`, { id });
1633
1697
  const oneTimePrice = v.price;
1634
1698
  const subPrice = this.planId ? planPrice(v, this.planId) : null;
1635
1699
  const price = sub ? subPrice : oneTimePrice;
1636
- const compareAt = sub ? ((_c = (_b = (_a = allocationFor(v, this.planId)) == null ? void 0 : _a.priceAdjustments) == null ? void 0 : _b[0]) == null ? void 0 : _c.compareAtPrice) || v.compareAtPrice : v.compareAtPrice;
1700
+ const compareAt = sub ? allocationFor(v, this.planId)?.priceAdjustments?.[0]?.compareAtPrice || v.compareAtPrice : v.compareAtPrice;
1637
1701
  const onSale = compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount);
1638
1702
  const discountPct = onSale ? Math.round((1 - parseFloat(price.amount) / parseFloat(compareAt.amount)) * 100) : 0;
1639
1703
  const savings = this.planId ? planSavings(v, this.planId) : { amount: null, percentage: 0 };
1640
1704
  const plan = this.plan;
1641
1705
  const total = { amount: (parseFloat(price.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: price.currencyCode };
1706
+ const fm = formatMoney;
1707
+ const pct = (n) => n ? `${n}%` : "";
1708
+ const text = {
1709
+ price: () => fm(price),
1710
+ "total-price": () => fm(total),
1711
+ "one-time-price": () => fm(oneTimePrice),
1712
+ "subscription-price": () => fm(subPrice),
1713
+ "subscription-savings": () => pct(savings.percentage),
1714
+ "subscription-savings-amount": () => fm(savings.amount),
1715
+ "checkout-charge": () => fm(sub ? planCheckoutCharge(v, this.planId) : price),
1716
+ "plan-name": () => plan?.name,
1717
+ "plan-description": () => plan?.description,
1718
+ "plan-group": () => plan?.group?.name,
1719
+ "compare-at-price": () => onSale ? fm(compareAt) : "",
1720
+ "unit-price": () => fm(v.unitPrice),
1721
+ "discount-percentage": () => `${discountPct}%`,
1722
+ "discount-amount": () => onSale ? fm({ amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)), currencyCode: price.currencyCode }) : "",
1723
+ "variant-title": () => v.title,
1724
+ "variant-id": () => v.id,
1725
+ sku: () => v.sku,
1726
+ barcode: () => v.barcode,
1727
+ stock: () => v.quantityAvailable,
1728
+ quantity: () => this.quantity
1729
+ };
1642
1730
  this.$(sel("bind")).forEach((el) => {
1643
- var _a2, _b2, _c2, _d, _e;
1644
- switch (get(el, "bind")) {
1645
- case "price":
1646
- return setText(el, formatMoney(price));
1647
- case "total-price":
1648
- return setText(el, formatMoney(total));
1649
- case "one-time-price":
1650
- return setText(el, formatMoney(oneTimePrice));
1651
- case "subscription-price":
1652
- return setText(el, formatMoney(subPrice));
1653
- case "subscription-savings":
1654
- return setText(el, savings.percentage ? `${savings.percentage}%` : "");
1655
- case "subscription-savings-amount":
1656
- return setText(el, formatMoney(savings.amount));
1657
- case "checkout-charge":
1658
- return setText(el, formatMoney(sub ? planCheckoutCharge(v, this.planId) : price));
1659
- case "plan-name":
1660
- return setText(el, (plan == null ? void 0 : plan.name) || "");
1661
- case "plan-description":
1662
- return setText(el, (plan == null ? void 0 : plan.description) || "");
1663
- case "plan-group":
1664
- return setText(el, ((_a2 = plan == null ? void 0 : plan.group) == null ? void 0 : _a2.name) || "");
1665
- case "compare-at-price":
1666
- return setText(el, onSale ? formatMoney(compareAt) : "");
1667
- case "unit-price":
1668
- return setText(el, formatMoney(v.unitPrice));
1669
- case "discount-percentage":
1670
- return setText(el, `${discountPct}%`);
1671
- case "discount-amount":
1672
- return setText(el, onSale ? formatMoney({
1673
- amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)),
1674
- currencyCode: price.currencyCode
1675
- }) : "");
1676
- case "variant-title":
1677
- return setText(el, v.title);
1678
- case "variant-id":
1679
- return setText(el, v.id);
1680
- case "sku":
1681
- return setText(el, v.sku);
1682
- case "barcode":
1683
- return setText(el, v.barcode);
1684
- case "stock":
1685
- return setText(el, (_b2 = v.quantityAvailable) != null ? _b2 : "");
1686
- case "quantity":
1687
- return setText(el, this.quantity);
1688
- case "variant-image":
1689
- return setImage(el, ((_c2 = v.image) == null ? void 0 : _c2.url) || ((_d = p.featuredImage) == null ? void 0 : _d.url), ((_e = v.image) == null ? void 0 : _e.altText) || p.title);
1690
- default:
1691
- return void 0;
1692
- }
1731
+ const k = get(el, "bind");
1732
+ if (k === "variant-image") setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
1733
+ else if (text[k]) setText(el, text[k]());
1693
1734
  });
1694
- this.$(sel("option-selected")).forEach((el) => setText(el, this.selected[get(el, "option-selected")] || ""));
1735
+ this.$(sel("option-selected")).forEach((el) => setText(el, this.selected[this.optName(get(el, "option-selected"))] || ""));
1695
1736
  this.$(sel("option-list")).forEach((container) => {
1696
- const name = get(container, "option-list");
1737
+ const name = this.optName(get(container, "option-list"));
1697
1738
  qsa(container, sel("option-value")).forEach((node) => {
1698
1739
  const value = get(node, "option-value");
1699
1740
  setSelected(node, this.selected[name] === value);
@@ -1703,7 +1744,7 @@ ${cartFragment()}`, { id });
1703
1744
  });
1704
1745
  });
1705
1746
  this.$(`select${sel("option")}`).forEach((s) => {
1706
- s.value = this.selected[get(s, "option")] || "";
1747
+ s.value = this.selected[this.optName(get(s, "option"))] || "";
1707
1748
  });
1708
1749
  this.$(`select${sel("variant-select")}`).forEach((s) => {
1709
1750
  s.value = v.id;
@@ -1727,23 +1768,16 @@ ${cartFragment()}`, { id });
1727
1768
  qsa(node, sel("plan-bind")).forEach((el) => {
1728
1769
  const adj = planAdjustment(planObj);
1729
1770
  const s = planSavings(v, id);
1730
- switch (get(el, "plan-bind")) {
1731
- case "name":
1732
- return setText(el, planObj == null ? void 0 : planObj.name);
1733
- case "description":
1734
- return setText(el, (planObj == null ? void 0 : planObj.description) || "");
1735
- case "price":
1736
- return setText(el, formatMoney(planPrice(v, id)));
1737
- case "savings":
1738
- return setText(el, s.percentage ? `${s.percentage}%` : "");
1739
- case "savings-amount":
1740
- return setText(el, formatMoney(s.amount));
1741
- case "adjustment":
1742
- if (!adj) return setText(el, "");
1743
- return setText(el, adj.type === "percentage" ? `${adj.value}%` : formatMoney(adj.value));
1744
- default:
1745
- return void 0;
1746
- }
1771
+ const k = get(el, "plan-bind");
1772
+ const val = {
1773
+ name: () => planObj?.name,
1774
+ description: () => planObj?.description,
1775
+ price: () => fm(planPrice(v, id)),
1776
+ savings: () => pct(s.percentage),
1777
+ "savings-amount": () => fm(s.amount),
1778
+ adjustment: () => !adj ? "" : adj.type === "percentage" ? `${adj.value}%` : fm(adj.value)
1779
+ }[k];
1780
+ if (val) setText(el, val());
1747
1781
  });
1748
1782
  });
1749
1783
  this.$(`select${sel("plan")}`).forEach((s) => {
@@ -1782,16 +1816,17 @@ ${cartFragment()}`, { id });
1782
1816
  this.$(sel("add-to-cart")).forEach((btn) => {
1783
1817
  const disabled = !v.availableForSale;
1784
1818
  btn.classList.toggle("is-disabled", disabled);
1819
+ btn.setAttribute("aria-disabled", String(disabled));
1785
1820
  if ("disabled" in btn) btn.disabled = disabled;
1786
1821
  const soldOut = get(btn, "soldout-text");
1787
1822
  const subText = get(btn, "subscribe-text");
1788
1823
  if (soldOut || subText) {
1789
1824
  const label = btn.querySelector(sel("add-label")) || btn;
1790
1825
  if (label.dataset.tetherLabel === void 0) label.dataset.tetherLabel = label.tagName === "INPUT" ? label.value : label.textContent;
1791
- let text = label.dataset.tetherLabel;
1792
- if (sub && subText) text = subText;
1793
- if (disabled && soldOut) text = soldOut;
1794
- if (btn.getAttribute("aria-busy") !== "true") setText(label, text);
1826
+ let text2 = label.dataset.tetherLabel;
1827
+ if (sub && subText) text2 = subText;
1828
+ if (disabled && soldOut) text2 = soldOut;
1829
+ if (btn.getAttribute("aria-busy") !== "true") setText(label, text2);
1795
1830
  }
1796
1831
  });
1797
1832
  emit("variant-changed", {
@@ -1805,6 +1840,11 @@ ${cartFragment()}`, { id });
1805
1840
  };
1806
1841
 
1807
1842
  // src/collection.js
1843
+ var collection_exports = {};
1844
+ __export(collection_exports, {
1845
+ ListScope: () => ListScope,
1846
+ listSelector: () => listSelector
1847
+ });
1808
1848
  var listSelector = () => `${sel("collection")},${sel("products")},${sel("recommendations")}`;
1809
1849
  var ListScope = class {
1810
1850
  constructor(root) {
@@ -1814,8 +1854,7 @@ ${cartFragment()}`, { id });
1814
1854
  this.collection = null;
1815
1855
  setScopeStatus(root, "loading");
1816
1856
  root.addEventListener("click", (e) => {
1817
- var _a, _b;
1818
- const more = (_b = (_a = e.target).closest) == null ? void 0 : _b.call(_a, sel("load-more"));
1857
+ const more = e.target.closest?.(sel("load-more"));
1819
1858
  if (more && more.closest(listSelector()) === root) {
1820
1859
  e.preventDefault();
1821
1860
  busy(more, () => this.load(true));
@@ -1837,20 +1876,19 @@ ${cartFragment()}`, { id });
1837
1876
  return opts;
1838
1877
  }
1839
1878
  async load(more = false) {
1840
- var _a;
1841
1879
  const token = this.loadToken = (this.loadToken || 0) + 1;
1842
- const stale = () => token !== this.loadToken;
1880
+ const stale2 = () => token !== this.loadToken;
1843
1881
  try {
1844
1882
  if (has(this.root, "recommendations")) {
1845
1883
  const key = get(this.root, "recommendations");
1846
1884
  const base = await getProduct(key);
1847
1885
  const recs = await getRecommendations(base.id, get(this.root, "intent") || "RELATED");
1848
- if (stale()) return;
1886
+ if (stale2()) return;
1849
1887
  const limit = parseInt(get(this.root, "limit") || "", 10);
1850
1888
  this.products = limit ? recs.slice(0, limit) : recs;
1851
1889
  } else {
1852
- const res = await getProducts({ ...this.options(), after: more ? (_a = this.pageInfo) == null ? void 0 : _a.endCursor : null });
1853
- if (stale()) return;
1890
+ const res = await getProducts({ ...this.options(), after: more ? this.pageInfo?.endCursor : null });
1891
+ if (stale2()) return;
1854
1892
  this.collection = res.collection;
1855
1893
  this.pageInfo = res.pageInfo;
1856
1894
  this.products = more ? [...this.products, ...res.products] : res.products;
@@ -1859,7 +1897,7 @@ ${cartFragment()}`, { id });
1859
1897
  setScopeStatus(this.root, "ready");
1860
1898
  emit("list-ready", { products: this.products, collection: this.collection }, this.root);
1861
1899
  } catch (err) {
1862
- if (stale()) return;
1900
+ if (stale2()) return;
1863
1901
  if (!more) setScopeStatus(this.root, isNotFound(err) ? "not-found" : "error", err);
1864
1902
  reportError(err, "list load failed");
1865
1903
  emit("error", { error: err, context: "list" }, this.root);
@@ -1868,15 +1906,14 @@ ${cartFragment()}`, { id });
1868
1906
  render() {
1869
1907
  const c = this.collection;
1870
1908
  this.$(sel("collection-bind")).forEach((el) => {
1871
- var _a, _b;
1872
1909
  switch (get(el, "collection-bind")) {
1873
1910
  case "title":
1874
- return setText(el, (c == null ? void 0 : c.title) || "");
1911
+ return setText(el, c?.title || "");
1875
1912
  case "description":
1876
- el.innerHTML = (c == null ? void 0 : c.descriptionHtml) || "";
1913
+ el.innerHTML = c?.descriptionHtml || "";
1877
1914
  return void 0;
1878
1915
  case "image":
1879
- return setImage(el, (_a = c == null ? void 0 : c.image) == null ? void 0 : _a.url, ((_b = c == null ? void 0 : c.image) == null ? void 0 : _b.altText) || (c == null ? void 0 : c.title));
1916
+ return setImage(el, c?.image?.url, c?.image?.altText || c?.title);
1880
1917
  case "count":
1881
1918
  return setText(el, this.products.length);
1882
1919
  default:
@@ -1897,330 +1934,25 @@ ${cartFragment()}`, { id });
1897
1934
  if (k === "list-filled") el.style.display = empty ? "none" : "";
1898
1935
  });
1899
1936
  this.$(sel("load-more")).forEach((el) => {
1900
- var _a;
1901
- el.style.display = ((_a = this.pageInfo) == null ? void 0 : _a.hasNextPage) ? "" : "none";
1937
+ el.style.display = this.pageInfo?.hasNextPage ? "" : "none";
1902
1938
  });
1903
1939
  notifyLayoutChanged("list");
1904
1940
  }
1905
1941
  };
1906
1942
 
1907
- // src/api-version.js
1908
- var parse = (v) => {
1909
- const m = /^(\d{4})-(01|04|07|10)$/.exec(String(v || ""));
1910
- return m ? { y: +m[1], m: +m[2] } : null;
1911
- };
1912
- var fmt = (y, m) => `${y}-${String(m).padStart(2, "0")}`;
1913
- function nextApiVersion(v, steps = 1) {
1914
- const p = parse(v);
1915
- if (!p) throw new Error(`not a quarterly Shopify API version: ${v}`);
1916
- const q = p.y * 4 + (p.m - 1) / 3 + steps;
1917
- return fmt(Math.floor(q / 4), q % 4 * 3 + 1);
1918
- }
1919
- function apiVersionStatus(version, now = /* @__PURE__ */ new Date()) {
1920
- if (version === "unstable") return { version, ok: false, note: '"unstable" changes without notice: pin a dated version such as 2026-07' };
1921
- const p = parse(version);
1922
- if (!p) return { version, ok: false, note: "not a quarterly version (YYYY-01, -04, -07 or -10)" };
1923
- const released = new Date(Date.UTC(p.y, p.m - 1, 1));
1924
- const supportedUntil = new Date(Date.UTC(p.y + 1, p.m - 1, 1));
1925
- const monthsLeft = (supportedUntil - now) / (30.44 * 24 * 3600 * 1e3);
1926
- let note = `supported until about ${supportedUntil.toISOString().slice(0, 7)}`;
1927
- if (released > now) note = "not released yet (release candidate at best)";
1928
- else if (monthsLeft <= 0) note = "no longer supported: Shopify falls back to the oldest supported version. Upgrade (see README)";
1929
- else if (monthsLeft < 3) note = `${note}: plan the upgrade to ${nextApiVersion(version)} or later (see README)`;
1930
- return { version, ok: released <= now && monthsLeft > 0, monthsLeft: Math.round(monthsLeft), next: nextApiVersion(version), note };
1931
- }
1932
-
1933
- // src/markets.js
1934
- var COUNTRY = /^[A-Z]{2}$/;
1935
- var REGIONAL_LANGUAGES = /* @__PURE__ */ new Set(["PT_BR", "PT_PT", "ZH_CN", "ZH_TW"]);
1936
- var storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
1937
- var defaultEnv = () => ({
1938
- document: typeof document !== "undefined" ? document : null,
1939
- location: typeof window !== "undefined" ? window.location : null,
1940
- storage: (() => {
1941
- try {
1942
- return window.localStorage;
1943
- } catch {
1944
- return null;
1945
- }
1946
- })(),
1947
- global: typeof window !== "undefined" ? window : {}
1943
+ // src/mounting.js
1944
+ var mounting_exports = {};
1945
+ __export(mounting_exports, {
1946
+ hydrateFromPage: () => hydrateFromPage,
1947
+ liveLists: () => liveLists,
1948
+ liveProducts: () => liveProducts,
1949
+ mountProduct: () => mountProduct,
1950
+ mountedLists: () => mountedLists,
1951
+ mountedProducts: () => mountedProducts,
1952
+ pruneLive: () => pruneLive,
1953
+ retryFailed: () => retryFailed,
1954
+ within: () => within
1948
1955
  });
1949
- function toCountry(v) {
1950
- const c = String(v || "").trim().toUpperCase();
1951
- return COUNTRY.test(c) && c !== "XX" && c !== "T1" ? c : null;
1952
- }
1953
- function toLanguage(v) {
1954
- const raw = String(v || "").trim().replace("-", "_").toUpperCase();
1955
- if (!raw) return null;
1956
- if (REGIONAL_LANGUAGES.has(raw)) return raw;
1957
- const primary = raw.split("_")[0];
1958
- return /^[A-Z]{2,3}$/.test(primary) ? primary : null;
1959
- }
1960
- function readCookie(doc, name) {
1961
- if (!doc || !name) return null;
1962
- const hit = String(doc.cookie || "").split(";").map((c) => c.trim()).find((c) => c.startsWith(`${name}=`));
1963
- return hit ? decodeURIComponent(hit.slice(name.length + 1)) : null;
1964
- }
1965
- var lookup = (locales, key) => {
1966
- if (!locales || !key) return null;
1967
- const k = String(key).toLowerCase();
1968
- const found = Object.keys(locales).find((x) => x.toLowerCase() === k);
1969
- return found ? locales[found] : null;
1970
- };
1971
- var readers = {
1972
- saved(m, env) {
1973
- if (m.remember === false || !env.storage) return null;
1974
- try {
1975
- return JSON.parse(env.storage.getItem(storageKey()) || "null");
1976
- } catch {
1977
- return null;
1978
- }
1979
- },
1980
- server(m, env) {
1981
- const meta = (name) => {
1982
- var _a, _b, _c;
1983
- return (_c = (_b = (_a = env.document) == null ? void 0 : _a.querySelector) == null ? void 0 : _b.call(_a, `meta[name="${name}"]`)) == null ? void 0 : _c.getAttribute("content");
1984
- };
1985
- const country = env.global.TETHER_COUNTRY || meta("tether-country") || readCookie(env.document, m.cookie || "tether_country");
1986
- const language = env.global.TETHER_LANGUAGE || meta("tether-language");
1987
- return { country, language };
1988
- },
1989
- webflow(m, env) {
1990
- var _a, _b, _c;
1991
- const segment = (((_a = env.location) == null ? void 0 : _a.pathname) || "/").split("/").filter(Boolean)[0];
1992
- const fromPath = lookup(m.locales, segment);
1993
- if (fromPath) return fromPath;
1994
- const lang = (_c = (_b = env.document) == null ? void 0 : _b.documentElement) == null ? void 0 : _c.getAttribute("lang");
1995
- if (!lang) return null;
1996
- const mapped = lookup(m.locales, lang) || lookup(m.locales, lang.split("-")[0]);
1997
- if (mapped) return { locale: lang, ...mapped };
1998
- if (m.locales && m.strict) return null;
1999
- const region = lang.split("-")[1];
2000
- return { language: lang, country: region && region.length === 2 ? region : null, locale: lang };
2001
- }
2002
- };
2003
- function detectMarket(markets = cfg.markets, env = defaultEnv()) {
2004
- const m = markets || {};
2005
- if (m.detect === false) return {};
2006
- const list = !m.detect ? ["saved"] : m.detect === true ? ["saved", "server", "webflow"] : [].concat(m.detect);
2007
- markets = m;
2008
- const out = { sources: {} };
2009
- list.forEach((name) => {
2010
- var _a;
2011
- const r = (_a = readers[name]) == null ? void 0 : _a.call(readers, markets, env);
2012
- if (!r) return;
2013
- const vals = { country: toCountry(r.country), language: toLanguage(r.language), locale: r.locale || null };
2014
- Object.entries(vals).forEach(([k, v]) => {
2015
- if (v && !out[k]) {
2016
- out[k] = v;
2017
- out.sources[k] = name;
2018
- }
2019
- });
2020
- });
2021
- return out;
2022
- }
2023
- function rememberMarket(market) {
2024
- var _a;
2025
- if (((_a = cfg.markets) == null ? void 0 : _a.remember) === false) return;
2026
- try {
2027
- localStorage.setItem(storageKey(), JSON.stringify({ country: market.country, language: market.language || void 0 }));
2028
- } catch {
2029
- }
2030
- }
2031
- var localizationPromise = null;
2032
- function clearLocalization() {
2033
- localizationPromise = null;
2034
- }
2035
- function getLocalization() {
2036
- if (!localizationPromise) {
2037
- localizationPromise = gql(`query TetherLocalization {
2038
- localization {
2039
- country { isoCode name currency { isoCode symbol } }
2040
- language { isoCode name }
2041
- availableCountries { isoCode name currency { isoCode symbol } availableLanguages { isoCode name } }
2042
- }
2043
- }`).then((d) => d.localization).catch((err) => {
2044
- localizationPromise = null;
2045
- throw err;
2046
- });
2047
- }
2048
- return localizationPromise;
2049
- }
2050
-
2051
- // src/diagnose.js
2052
- var mask = (t) => t ? `${String(t).slice(0, 4)}\u2026${String(t).slice(-4)} (${String(t).length} chars)` : "(missing)";
2053
- function describe(el) {
2054
- const id = el.id ? `#${el.id}` : "";
2055
- const cls = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
2056
- return `<${el.tagName.toLowerCase()}${id}${cls}>`;
2057
- }
2058
- function checkMarkup(root = document) {
2059
- const problems = [];
2060
- const add = (level, message, el) => problems.push({ level, message, element: el ? describe(el) : void 0, el });
2061
- const inProduct = (el) => Boolean(el.closest(sel("product")));
2062
- const listSel = `${sel("collection")},${sel("products")},${sel("recommendations")}`;
2063
- qsa(root, sel("product")).forEach((el) => {
2064
- if (el.closest(listSel) && el.closest(sel("list"))) return;
2065
- const { key, fromUrl, raw } = resolveProductKey(el);
2066
- if (!raw) add("error", `${attr("product")} has no value. Bind it to the Shopify handle (CMS field), or use "url:handle".`, el);
2067
- else if (fromUrl && !key) add("warn", `${attr("product")}="${raw}" but this URL has no value for it (e.g. ?handle=my-product).`, el);
2068
- });
2069
- const productOnly = ["bind", "option-list", "option", "variant-select", "add-to-cart", "purchase", "plan-list", "plan", "qty", "line-attribute"];
2070
- productOnly.forEach((name) => {
2071
- qsa(root, sel(name)).forEach((el) => {
2072
- if (inProduct(el)) return;
2073
- if (name === "line-attribute" && el.closest(sel("group"))) return;
2074
- add("warn", `${attr(name)} outside any ${attr("product")}: it does nothing here.`, el);
2075
- });
2076
- });
2077
- qsa(root, sel("group-add")).forEach((el) => {
2078
- if (!el.closest(sel("group"))) add("warn", `${attr("group-add")} outside a ${attr("group")}.`, el);
2079
- });
2080
- [...qsa(root, sel("option-list")), ...qsa(root, sel("plan-list")), ...qsa(root, sel("cart-lines")), ...qsa(root, sel("list"))].forEach((el) => {
2081
- if (!el.firstElementChild && !hasTemplate(el)) {
2082
- add("warn", "Repeater without a template child: put one example element inside it.", el);
2083
- }
2084
- });
2085
- qsa(root, listSel).forEach((el) => {
2086
- if (!qsa(el, sel("list")).length) add("error", `List without a ${attr("list")} child: products have nowhere to render.`, el);
2087
- });
2088
- const known = /* @__PURE__ */ new Set([
2089
- "in-stock",
2090
- "out-of-stock",
2091
- "backorder",
2092
- "low-stock",
2093
- "on-sale",
2094
- "not-on-sale",
2095
- "has-variants",
2096
- "single-variant",
2097
- "subscription-available",
2098
- "subscription-unavailable",
2099
- "subscription-only",
2100
- "one-time-available",
2101
- "subscription-selected",
2102
- "one-time-selected",
2103
- "has-savings",
2104
- "loading",
2105
- "ready",
2106
- "error",
2107
- "not-found",
2108
- "cart-empty",
2109
- "cart-filled",
2110
- "cart-has-subscription",
2111
- "cart-no-subscription",
2112
- "cart-has-discount",
2113
- "cart-error",
2114
- "list-empty",
2115
- "list-filled"
2116
- ]);
2117
- qsa(root, sel("show")).forEach((el) => {
2118
- const k = get(el, "show");
2119
- if (!known.has(k)) add("warn", `Unknown ${attr("show")}="${k}".`, el);
2120
- });
2121
- qsa(root, sel("bind")).forEach((el) => {
2122
- const k = get(el, "bind") || "";
2123
- if (!k.startsWith("metafield:")) return;
2124
- const ref = k.slice(10);
2125
- const listed = (cfg.metafields || []).some((m) => (typeof m === "string" ? m : `${m.namespace}.${m.key}`) === ref);
2126
- if (!listed) add("error", `${attr("bind")}="${k}" but "${ref}" is not in config.metafields, so it stays empty.`, el);
2127
- });
2128
- if (qsa(root, sel("cart-open")).length && !qsa(root, sel("cart-drawer")).length) {
2129
- add("warn", `${attr("cart-open")} exists but there is no ${attr("cart-drawer")} on this page.`);
2130
- }
2131
- if (qsa(root, sel("cart-drawer")).length > 1) add("warn", `More than one ${attr("cart-drawer")}.`);
2132
- qsa(root, `[${attr("state")}="error"]`).forEach((el) => {
2133
- if (el.matches(boundary())) add("error", `Scope failed to load (${get(el, "error") || "error"}).`, el);
2134
- });
2135
- return problems;
2136
- }
2137
- async function diagnose({ root = document, silent = false, version, now = /* @__PURE__ */ new Date() } = {}) {
2138
- var _a, _b, _c, _d, _e, _f, _g;
2139
- const report = {
2140
- version,
2141
- config: {
2142
- domain: cfg.domain || "(missing)",
2143
- token: mask(cfg.token),
2144
- apiVersion: cfg.apiVersion,
2145
- endpoint: cfg.domain ? endpoint() : null,
2146
- country: cfg.country,
2147
- language: cfg.language,
2148
- locale: cfg.locale,
2149
- markets: cfg.markets || null,
2150
- prefix: cfg.prefix,
2151
- productPath: cfg.productPath,
2152
- metafields: cfg.metafields,
2153
- autoMount: cfg.autoMount,
2154
- observe: cfg.observe,
2155
- onError: typeof cfg.onError === "function"
2156
- },
2157
- apiVersion: apiVersionStatus(cfg.apiVersion, now),
2158
- market: ((_a = cfg.markets) == null ? void 0 : _a.detect) ? detectMarket(cfg.markets) : null,
2159
- reachable: false,
2160
- shop: null,
2161
- latencyMs: null,
2162
- products: [],
2163
- sellingPlans: [],
2164
- problems: []
2165
- };
2166
- if (!isConfigured()) {
2167
- report.problems.push({ level: "error", message: "TETHER_CONFIG needs domain and token." });
2168
- } else {
2169
- const t0 = Date.now();
2170
- try {
2171
- const data = await gql("query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }", {}, { context: false });
2172
- report.reachable = true;
2173
- report.shop = { name: (_b = data.shop) == null ? void 0 : _b.name, url: (_d = (_c = data.shop) == null ? void 0 : _c.primaryDomain) == null ? void 0 : _d.url, currency: (_f = (_e = data.shop) == null ? void 0 : _e.paymentSettings) == null ? void 0 : _f.currencyCode };
2174
- } catch (err) {
2175
- const hint = err.status === 401 || err.status === 403 ? "Token rejected: use the public Storefront token from the Headless channel, not an Admin token." : err.status === 404 ? "Store or API version not found: check domain (x.myshopify.com) and apiVersion." : "";
2176
- report.problems.push({ level: "error", message: `Storefront API not reachable: ${err.message}${hint ? `. ${hint}` : ""}` });
2177
- }
2178
- report.latencyMs = Date.now() - t0;
2179
- if (report.reachable) {
2180
- const keys = [...new Set(qsa(root, sel("product")).map((el) => resolveProductKey(el).key).filter(Boolean))].slice(0, 20);
2181
- const results = await Promise.allSettled(keys.map((k) => getProduct(k)));
2182
- results.forEach((r, i) => {
2183
- var _a2, _b2;
2184
- if (r.status === "fulfilled") {
2185
- const p = r.value;
2186
- const plans = allPlans(p);
2187
- report.products.push({ key: keys[i], handle: p.handle, title: p.title, plans: plans.length, requiresSellingPlan: p.requiresSellingPlan });
2188
- plans.forEach((pl) => {
2189
- var _a3;
2190
- return report.sellingPlans.push({ product: p.handle, group: (_a3 = pl.group) == null ? void 0 : _a3.name, plan: pl.name, id: pl.id });
2191
- });
2192
- } else {
2193
- report.products.push({ key: keys[i], error: (_a2 = r.reason) == null ? void 0 : _a2.message });
2194
- report.problems.push({ level: "error", message: `Product "${keys[i]}": ${(_b2 = r.reason) == null ? void 0 : _b2.message}. Is it published to the Headless channel?` });
2195
- }
2196
- });
2197
- }
2198
- }
2199
- if (!report.apiVersion.ok || ((_g = report.apiVersion.monthsLeft) != null ? _g : 99) < 3) {
2200
- report.problems.push({ level: report.apiVersion.ok ? "warn" : "error", message: `apiVersion ${cfg.apiVersion}: ${report.apiVersion.note}` });
2201
- }
2202
- if (typeof document !== "undefined") report.problems.push(...checkMarkup(root));
2203
- if (!silent) print(report);
2204
- return report;
2205
- }
2206
- function print(r) {
2207
- var _a, _b, _c;
2208
- const c = console;
2209
- const errors = r.problems.filter((p) => p.level === "error").length;
2210
- (_a = c.group) == null ? void 0 : _a.call(c, `[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : "no errors"}, ${r.problems.length - errors} warning(s)`);
2211
- c.log("version", r.version, "| API", r.apiVersion.version, `(${r.apiVersion.note})`);
2212
- c.log("config", r.config);
2213
- if (r.market) c.log("market detected", r.market, "(in use:", r.config.country, r.config.language, ")");
2214
- c.log(r.reachable ? `Storefront API reachable: ${(_b = r.shop) == null ? void 0 : _b.name} (${r.latencyMs} ms)` : "Storefront API NOT reachable");
2215
- if (r.products.length) (c.table || c.log)(r.products);
2216
- if (r.sellingPlans.length) (c.table || c.log)(r.sellingPlans);
2217
- else if (r.reachable) c.log("No selling plans on the products of this page.");
2218
- r.problems.forEach((p) => (p.level === "error" ? c.error : c.warn)(p.message, p.el || ""));
2219
- (_c = c.groupEnd) == null ? void 0 : _c.call(c);
2220
- }
2221
-
2222
- // src/index.js
2223
- var VERSION = "0.3.6";
2224
1956
  var mountedProducts = /* @__PURE__ */ new WeakSet();
2225
1957
  var mountedLists = /* @__PURE__ */ new WeakSet();
2226
1958
  var liveProducts = /* @__PURE__ */ new Set();
@@ -2234,35 +1966,8 @@ ${cartFragment()}`, { id });
2234
1966
  if (!l.root.isConnected) liveLists.delete(l);
2235
1967
  });
2236
1968
  }
2237
- var initPromise = null;
2238
- var observer = null;
2239
1969
  function within(root, selector) {
2240
- var _a;
2241
- return [...((_a = root.matches) == null ? void 0 : _a.call(root, selector)) ? [root] : [], ...qsa(root, selector)];
2242
- }
2243
- function mount(root = document) {
2244
- if (!isConfigured()) return 0;
2245
- let count = 0;
2246
- pruneLive();
2247
- within(root, listSelector()).forEach((el) => {
2248
- if (mountedLists.has(el)) return;
2249
- mountedLists.add(el);
2250
- count++;
2251
- const list = new ListScope(el);
2252
- liveLists.add(list);
2253
- el.addEventListener(`${cfg.prefix}:list-rendered`, (e) => mount(e.detail.container));
2254
- list.load();
2255
- });
2256
- within(root, sel("product")).forEach((el) => {
2257
- if (mountProduct(el)) count++;
2258
- });
2259
- renderCountrySelects(root);
2260
- qsa(root, sel("account-link")).forEach((a) => {
2261
- a.setAttribute("href", `https://${cfg.domain}/account`);
2262
- });
2263
- Cart.render();
2264
- log(`mounted ${count} scope(s)`, root);
2265
- return count;
1970
+ return [...root.matches?.(selector) ? [root] : [], ...qsa(root, selector)];
2266
1971
  }
2267
1972
  function mountProduct(el) {
2268
1973
  if (mountedProducts.has(el)) return false;
@@ -2290,6 +1995,7 @@ ${cartFragment()}`, { id });
2290
1995
  if (existing) existing.update(p);
2291
1996
  else new ProductScope(el, p);
2292
1997
  applyPageTitle(el, p);
1998
+ if (cfg.revalidate !== false && takeStale(key)) queueRefresh(key);
2293
1999
  }).catch((err) => {
2294
2000
  if (!current()) return;
2295
2001
  const notFound = isNotFound(err);
@@ -2305,74 +2011,126 @@ ${cartFragment()}`, { id });
2305
2011
  });
2306
2012
  return true;
2307
2013
  }
2308
- function renderCountrySelects(root = document) {
2309
- const selects = within(root, `select${sel("country-select")}`);
2310
- const binds = within(root, sel("country-bind"));
2311
- if (!selects.length && !binds.length) return Promise.resolve();
2312
- return getLocalization().then((loc) => {
2313
- var _a;
2314
- const current = (cfg.country || ((_a = loc.country) == null ? void 0 : _a.isoCode) || "").toUpperCase();
2315
- const countries = [...loc.availableCountries || []].sort((a, b) => a.name.localeCompare(b.name, cfg.locale || void 0));
2316
- const info = countries.find((c) => c.isoCode === current) || loc.country;
2317
- const fill = (tpl, c) => {
2318
- var _a2, _b;
2319
- return tpl.replace(/\{name\}/g, c.name).replace(/\{code\}/g, c.isoCode).replace(/\{currency\}/g, ((_a2 = c.currency) == null ? void 0 : _a2.isoCode) || "").replace(/\{symbol\}/g, ((_b = c.currency) == null ? void 0 : _b.symbol) || "");
2320
- };
2321
- selects.forEach((s) => {
2322
- const tpl = s.getAttribute(attr("country-label")) || "{name} ({currency})";
2323
- s.innerHTML = "";
2324
- countries.forEach((c) => s.appendChild(new Option(fill(tpl, c), c.isoCode)));
2325
- s.value = current;
2326
- if (s.value !== current) s.selectedIndex = -1;
2327
- s.disabled = countries.length < 2 && !s.hasAttribute(attr("always-enabled"));
2328
- });
2329
- binds.forEach((el) => {
2330
- var _a2, _b;
2331
- const k = get(el, "country-bind") || "name";
2332
- const v = { name: info == null ? void 0 : info.name, code: info == null ? void 0 : info.isoCode, currency: (_a2 = info == null ? void 0 : info.currency) == null ? void 0 : _a2.isoCode, symbol: (_b = info == null ? void 0 : info.currency) == null ? void 0 : _b.symbol }[k];
2333
- el.textContent = v || "";
2334
- });
2335
- }).catch((err) => reportError(err, "country select"));
2336
- }
2337
- async function setCountry(code, { language, remember = true } = {}) {
2338
- const country = toCountry(code);
2339
- if (!country) throw new TetherError(`Not a country code: ${code}`, { code: "INVALID_COUNTRY" });
2340
- const lang = language === void 0 ? cfg.language : toLanguage(language);
2341
- const changed = country !== String(cfg.country || "").toUpperCase() || lang !== cfg.language;
2342
- cfg.country = country;
2343
- cfg.language = lang || void 0;
2344
- if (remember) rememberMarket({ country, language: lang });
2345
- if (!changed) {
2346
- renderCountrySelects();
2347
- return Cart.state;
2348
- }
2349
- clearProductCache();
2350
- const jobs = [];
2351
- if (language !== void 0) clearLocalization();
2352
- if (Cart.id()) jobs.push(Cart.setBuyerIdentity({ countryCode: country }, { create: false }).catch((err) => reportError(err, "cart country")));
2014
+ function applyPageTitle(el, p) {
2015
+ const tpl = get(el, "page-title");
2016
+ if (!tpl || typeof document === "undefined") return;
2017
+ document.title = tpl.replace(/\{title\}/g, p.title).replace(/\{vendor\}/g, p.vendor || "").replace(/\{type\}/g, p.productType || "");
2018
+ }
2019
+ function retryFailed() {
2353
2020
  pruneLive();
2354
- liveLists.forEach((list) => jobs.push(list.load()));
2355
2021
  liveProducts.forEach((el) => {
2356
- if (el.closest(listSelector())) return;
2022
+ if (el.getAttribute(attr("state")) !== "error" || el.getAttribute(attr("error")) === "not-found") return;
2357
2023
  mountedProducts.delete(el);
2358
2024
  mountProduct(el);
2359
2025
  });
2360
- renderCountrySelects();
2361
- emit("country-changed", { country, language: cfg.language });
2362
- await Promise.all(jobs);
2363
- return Cart.state;
2364
2026
  }
2365
- function applyPageTitle(el, p) {
2366
- const tpl = get(el, "page-title");
2367
- if (!tpl || typeof document === "undefined") return;
2368
- document.title = tpl.replace(/\{title\}/g, p.title).replace(/\{vendor\}/g, p.vendor || "").replace(/\{type\}/g, p.productType || "");
2027
+ var refreshQueue = /* @__PURE__ */ new Set();
2028
+ function queueRefresh(key) {
2029
+ if (!refreshQueue.size) setTimeout(flushRefresh, 0);
2030
+ refreshQueue.add(key);
2031
+ }
2032
+ function flushRefresh() {
2033
+ const keys = [...refreshQueue];
2034
+ refreshQueue = /* @__PURE__ */ new Set();
2035
+ refreshProducts(keys).forEach((promise, i) => promise.then((fresh) => {
2036
+ liveProducts.forEach((el) => {
2037
+ const scope = scopes.get(el);
2038
+ if (!scope || !el.isConnected) return;
2039
+ const k = resolveProductKey(el).key;
2040
+ if (k !== keys[i] && k !== fresh.handle && k !== fresh.id) return;
2041
+ if (JSON.stringify(scope.product) !== JSON.stringify(fresh)) scope.update(fresh);
2042
+ });
2043
+ }).catch((err) => reportError(err, `refresh ${keys[i]}`)));
2044
+ }
2045
+ var readScripts = /* @__PURE__ */ new WeakSet();
2046
+ function hydrateFromPage(root = document) {
2047
+ qsa(root.ownerDocument || root, 'script[type="application/json"][data-tether-hydrate]').forEach((s) => {
2048
+ if (readScripts.has(s)) return;
2049
+ readScripts.add(s);
2050
+ try {
2051
+ const data = JSON.parse(s.textContent);
2052
+ rememberOptionNames(data.products);
2053
+ const same = (a, b) => String(a || "").toUpperCase() === String(b || "").toUpperCase();
2054
+ if (same(data.country, cfg.country) && same(data.language, cfg.language)) hydrateProducts(data.products);
2055
+ else log("hydration skipped: other market");
2056
+ } catch (err) {
2057
+ warn("bad data-tether-hydrate", err);
2058
+ }
2059
+ });
2060
+ }
2061
+
2062
+ // src/lazy-iife.js
2063
+ var { cfg: cfg2 } = config_exports;
2064
+ var coreModules = {
2065
+ "config.js": config_exports,
2066
+ "client.js": client_exports,
2067
+ "dom.js": dom_exports,
2068
+ "fragments.js": fragments_exports,
2069
+ "loader.js": loader_exports,
2070
+ "selling-plans.js": selling_plans_exports,
2071
+ "status.js": status_exports,
2072
+ "cart.js": cart_exports,
2073
+ "product.js": product_exports,
2074
+ "collection.js": collection_exports,
2075
+ "mounting.js": mounting_exports
2076
+ };
2077
+ var self = typeof document !== "undefined" ? document.currentScript?.src || "" : "";
2078
+ var parts = {};
2079
+ function loadPart(name) {
2080
+ if (parts[name]) return parts[name];
2081
+ const base = cfg2.partsUrl || self.replace(/[^/]*$/, "");
2082
+ const min = /\.min\.js(\?|$)/.test(self) || !self ? ".min" : "";
2083
+ const url = `${base.replace(/\/?$/, "/")}tether.${name}${min}.js`;
2084
+ parts[name] = new Promise((resolve, reject) => {
2085
+ const s = document.createElement("script");
2086
+ s.src = url;
2087
+ s.async = true;
2088
+ s.onload = () => window.Tether?.__parts?.[name] ? resolve(window.Tether.__parts[name]) : reject(new Error(`${url} did not register`));
2089
+ s.onerror = () => {
2090
+ delete parts[name];
2091
+ reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`));
2092
+ };
2093
+ document.head.appendChild(s);
2094
+ });
2095
+ return parts[name];
2096
+ }
2097
+
2098
+ // src/index.js
2099
+ var VERSION = "0.5.0";
2100
+ var initPromise = null;
2101
+ var observer = null;
2102
+ function mount(root = document) {
2103
+ if (!isConfigured()) return 0;
2104
+ let count = 0;
2105
+ pruneLive();
2106
+ hydrateFromPage(root);
2107
+ within(root, listSelector()).forEach((el) => {
2108
+ if (mountedLists.has(el)) return;
2109
+ mountedLists.add(el);
2110
+ count++;
2111
+ const list = new ListScope(el);
2112
+ liveLists.add(list);
2113
+ el.addEventListener(`${cfg.prefix}:list-rendered`, (e) => mount(e.detail.container));
2114
+ list.load();
2115
+ });
2116
+ within(root, sel("product")).forEach((el) => {
2117
+ if (mountProduct(el)) count++;
2118
+ });
2119
+ if (within(root, `${sel("country-select")},${sel("country-bind")}`).length) {
2120
+ loadPart("markets").then((m) => m.renderCountrySelects(root)).catch((err) => reportError(err, "markets"));
2121
+ }
2122
+ qsa(root, sel("account-link")).forEach((a) => {
2123
+ a.setAttribute("href", `https://${cfg.domain}/account`);
2124
+ });
2125
+ Cart.render();
2126
+ log(`mounted ${count} scope(s)`, root);
2127
+ return count;
2369
2128
  }
2370
2129
  function unmount() {
2371
2130
  Cart.close();
2372
2131
  }
2373
2132
  function getScope(el) {
2374
- var _a;
2375
- const root = (_a = el == null ? void 0 : el.closest) == null ? void 0 : _a.call(el, sel("product"));
2133
+ const root = el?.closest?.(sel("product"));
2376
2134
  return root ? scopes.get(root) || null : null;
2377
2135
  }
2378
2136
  var controlsBound = false;
@@ -2380,9 +2138,8 @@ ${cartFragment()}`, { id });
2380
2138
  if (controlsBound) return;
2381
2139
  controlsBound = true;
2382
2140
  document.addEventListener("click", (e) => {
2383
- var _a;
2384
2141
  const t = e.target;
2385
- if (!(t == null ? void 0 : t.closest)) return;
2142
+ if (!t?.closest) return;
2386
2143
  if (t.closest(sel("cart-open"))) {
2387
2144
  e.preventDefault();
2388
2145
  Cart.toggle();
@@ -2420,7 +2177,7 @@ ${cartFragment()}`, { id });
2420
2177
  const removeCode = t.closest(sel("discount-remove"));
2421
2178
  if (removeCode) {
2422
2179
  e.preventDefault();
2423
- const code = (_a = removeCode.closest(sel("discount-code"))) == null ? void 0 : _a.getAttribute(attr("discount-code"));
2180
+ const code = removeCode.closest(sel("discount-code"))?.getAttribute(attr("discount-code"));
2424
2181
  if (code) Cart.removeDiscount(code).catch((err) => reportError(err, "discount"));
2425
2182
  return;
2426
2183
  }
@@ -2432,9 +2189,9 @@ ${cartFragment()}`, { id });
2432
2189
  });
2433
2190
  document.addEventListener("change", (e) => {
2434
2191
  const t = e.target;
2435
- if (!(t == null ? void 0 : t.closest)) return;
2192
+ if (!t?.closest) return;
2436
2193
  const lineEl = t.closest(dataKeySel());
2437
- const id = lineEl == null ? void 0 : lineEl.getAttribute(`data-${cfg.prefix}-key`);
2194
+ const id = lineEl?.getAttribute(`data-${cfg.prefix}-key`);
2438
2195
  if (id && t.matches(`input${sel("line", "quantity")}`)) {
2439
2196
  Cart.updateLine(id, parseInt(t.value, 10) || 0).catch((err) => reportError(err, "cart line"));
2440
2197
  } else if (id && t.matches(`select${sel("line-plan")}`)) {
@@ -2446,18 +2203,18 @@ ${cartFragment()}`, { id });
2446
2203
  }
2447
2204
  });
2448
2205
  document.addEventListener("submit", (e) => {
2449
- var _a, _b;
2450
- const form = (_b = (_a = e.target).closest) == null ? void 0 : _b.call(_a, sel("discount-form"));
2206
+ const form = e.target.closest?.(sel("discount-form"));
2451
2207
  if (!form) return;
2452
2208
  e.preventDefault();
2453
2209
  const input = form.querySelector(sel("discount-input")) || form.querySelector("input");
2454
- const code = input == null ? void 0 : input.value.trim();
2210
+ const code = input?.value.trim();
2455
2211
  if (!code) return;
2456
2212
  busy(form, async () => {
2457
- var _a2;
2458
2213
  try {
2459
2214
  const cart = await Cart.applyDiscount(code);
2460
- const ok = (_a2 = ((cart == null ? void 0 : cart.discountCodes) || []).find((d) => d.code.toLowerCase() === code.toLowerCase())) == null ? void 0 : _a2.applicable;
2215
+ const ok = (cart?.discountCodes || []).find((d) => d.code.toLowerCase() === code.toLowerCase())?.applicable;
2216
+ if (!ok) await Cart.removeDiscount(code).catch(() => {
2217
+ });
2461
2218
  form.classList.toggle("is-invalid", !ok);
2462
2219
  emit(ok ? "discount-applied" : "discount-invalid", { code }, form);
2463
2220
  if (ok && input) input.value = "";
@@ -2466,9 +2223,36 @@ ${cartFragment()}`, { id });
2466
2223
  reportError(err, "discount");
2467
2224
  }
2468
2225
  });
2226
+ }, true);
2227
+ document.addEventListener("input", (e) => {
2228
+ e.target.closest?.(sel("discount-form"))?.classList.remove("is-invalid");
2229
+ });
2230
+ if (cfg.astro !== false) {
2231
+ document.addEventListener("astro:page-load", () => mount(document));
2232
+ document.addEventListener("astro:before-swap", () => unmount());
2233
+ }
2234
+ window.addEventListener("online", () => {
2235
+ retryFailed();
2236
+ liveLists.forEach((l) => {
2237
+ if (l.root.getAttribute(attr("state")) === "error") l.load();
2238
+ });
2469
2239
  });
2470
2240
  document.addEventListener("keydown", (e) => {
2471
- if (e.key === "Escape" && Cart.isOpen()) Cart.close();
2241
+ if (!Cart.isOpen()) return;
2242
+ if (e.key === "Escape") {
2243
+ Cart.close();
2244
+ return;
2245
+ }
2246
+ if (e.key !== "Tab" || cfg.cartFocus === false) return;
2247
+ const d = Cart.drawers().find((x) => x.classList.contains(cfg.cartDrawerOpenClass));
2248
+ const f = d ? focusables(d) : [];
2249
+ if (!f.length) return;
2250
+ const first = f[0];
2251
+ const last = f[f.length - 1];
2252
+ if (!d.contains(document.activeElement) || e.shiftKey && document.activeElement === first || !e.shiftKey && document.activeElement === last) {
2253
+ e.preventDefault();
2254
+ (e.shiftKey ? last : first).focus();
2255
+ }
2472
2256
  });
2473
2257
  }
2474
2258
  function dataKeySel() {
@@ -2480,10 +2264,7 @@ ${cartFragment()}`, { id });
2480
2264
  const name = get(group, "group");
2481
2265
  const extra = toAttributes(parseJSONAttr(group, "line-attributes"));
2482
2266
  const groupAttrs = name ? [{ key: "_group", value: name }, ...extra] : extra;
2483
- const lines = qsa(group, sel("product")).map((el) => scopes.get(el)).filter((s) => {
2484
- var _a;
2485
- return s && ((_a = s.variant) == null ? void 0 : _a.availableForSale) && s.quantity > 0;
2486
- }).map((s) => {
2267
+ const lines = qsa(group, sel("product")).map((el) => scopes.get(el)).filter((s) => s && s.variant?.availableForSale && s.quantity > 0).map((s) => {
2487
2268
  const l = s.getLine();
2488
2269
  l.attributes = [...l.attributes || [], ...groupAttrs];
2489
2270
  return l;
@@ -2502,7 +2283,6 @@ ${cartFragment()}`, { id });
2502
2283
  }
2503
2284
  }
2504
2285
  function init(overrides = {}) {
2505
- var _a;
2506
2286
  if (initPromise && !Object.keys(overrides).length) return initPromise;
2507
2287
  configure({ ...window.TETHER_CONFIG || {}, ...overrides });
2508
2288
  if (!isConfigured()) {
@@ -2510,17 +2290,11 @@ ${cartFragment()}`, { id });
2510
2290
  initPromise = Promise.resolve(null);
2511
2291
  return initPromise;
2512
2292
  }
2513
- const market = detectMarket(cfg.markets);
2514
- if (market.country) cfg.country = market.country;
2515
- if (market.language) cfg.language = market.language;
2516
- if (market.locale && !overrides.locale && !((_a = window.TETHER_CONFIG) == null ? void 0 : _a.locale)) cfg.locale = market.locale;
2517
- if (market.sources && Object.keys(market.sources).length) log("market", market);
2518
2293
  bindControls();
2519
2294
  if (cfg.observe) startObserver();
2520
- initPromise = Cart.init().then(async (cart) => {
2521
- var _a2;
2295
+ initPromise = applyMarket(overrides).then(() => Cart.init()).then(async (cart) => {
2522
2296
  const want = cfg.country ? String(cfg.country).toUpperCase() : null;
2523
- if (cart && want && ((_a2 = cart.buyerIdentity) == null ? void 0 : _a2.countryCode) && cart.buyerIdentity.countryCode !== want) {
2297
+ if (cart && want && cart.buyerIdentity?.countryCode && cart.buyerIdentity.countryCode !== want) {
2524
2298
  try {
2525
2299
  cart = await Cart.setBuyerIdentity({ countryCode: want }, { create: false });
2526
2300
  } catch (err) {
@@ -2532,6 +2306,33 @@ ${cartFragment()}`, { id });
2532
2306
  });
2533
2307
  return initPromise;
2534
2308
  }
2309
+ async function applyMarket(overrides) {
2310
+ const m = cfg.markets || {};
2311
+ if (m.detect === false) return;
2312
+ let market = null;
2313
+ if (m.detect) {
2314
+ try {
2315
+ market = (await loadPart("markets")).detectMarket(m);
2316
+ } catch (err) {
2317
+ reportError(err, "markets");
2318
+ return;
2319
+ }
2320
+ } else if (m.remember !== false) {
2321
+ try {
2322
+ market = JSON.parse(localStorage.getItem(`${cfg.prefix}:market:${cfg.domain}`) || "null");
2323
+ } catch {
2324
+ }
2325
+ if (market && !/^[A-Z]{2}$/.test(market.country || "")) market = null;
2326
+ }
2327
+ if (!market) return;
2328
+ if (market.country) cfg.country = market.country;
2329
+ if (market.language) cfg.language = market.language;
2330
+ if (market.locale && !overrides.locale) cfg.locale = market.locale;
2331
+ log("market", market);
2332
+ }
2333
+ function setCountry(code, opts) {
2334
+ return loadPart("markets").then((m) => m.setCountry(code, opts));
2335
+ }
2535
2336
  function startObserver() {
2536
2337
  if (observer || typeof MutationObserver === "undefined") return;
2537
2338
  let t = null;
@@ -2573,13 +2374,16 @@ ${cartFragment()}`, { id });
2573
2374
  normalizeProductKey,
2574
2375
  clearProductCache,
2575
2376
  formatMoney,
2377
+ imageUrl,
2576
2378
  plans: selling_plans_exports,
2577
2379
  TetherError,
2578
2380
  resolveProductKey,
2579
2381
  setCountry,
2580
- detectMarket: (markets) => detectMarket(markets || cfg.markets),
2581
- getLocalization,
2582
- diagnose: (opts) => diagnose({ ...opts, version: VERSION }),
2382
+ detectMarket: (markets) => loadPart("markets").then((m) => m.detectMarket(markets || cfg.markets)),
2383
+ getLocalization: () => loadPart("markets").then((m) => m.getLocalization()),
2384
+ diagnose: (opts) => loadPart("diagnose").then((m) => m.diagnose({ ...opts, version: VERSION })),
2385
+ __core: coreModules,
2386
+ __parts: {},
2583
2387
  on(name, fn) {
2584
2388
  const h = (e) => fn(e.detail, e);
2585
2389
  document.addEventListener(`${cfg.prefix}:${name}`, h);
@@ -2589,7 +2393,7 @@ ${cartFragment()}`, { id });
2589
2393
  var index_default = api;
2590
2394
  if (typeof window !== "undefined" && typeof document !== "undefined") {
2591
2395
  window.Tether = api;
2592
- const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false;
2396
+ const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false && !window.TETHER_INTEGRATION;
2593
2397
  if (auto) {
2594
2398
  const start = () => init().then(() => mount(document));
2595
2399
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });