@occluze/tether 0.9.2 → 0.9.4

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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-2467DHLU.js +1 -0
  4. package/dist/esm/chunks/{account-pages-FIXSWJH7.js → account-pages-X7U3E7D7.js} +1 -1
  5. package/dist/esm/chunks/{analytics-MO2ZWBOD.js → analytics-TF7M4KI2.js} +1 -1
  6. package/dist/esm/chunks/{chunk-7CRTJV25.js → chunk-4QINHHE4.js} +1 -1
  7. package/dist/esm/chunks/chunk-5TS6IBBB.js +1 -0
  8. package/dist/esm/chunks/{chunk-BVO7KK2M.js → chunk-646NGAEM.js} +1 -1
  9. package/dist/esm/chunks/chunk-67MCLHFS.js +1 -0
  10. package/dist/esm/chunks/{chunk-FWMPL6PR.js → chunk-6FWNC7QH.js} +1 -1
  11. package/dist/esm/chunks/{chunk-IJAMRI33.js → chunk-AHYDU7Z6.js} +1 -1
  12. package/dist/esm/chunks/{chunk-SZRDO62E.js → chunk-B2X6ZOKA.js} +1 -1
  13. package/dist/esm/chunks/chunk-C2GKO34C.js +1 -0
  14. package/dist/esm/chunks/chunk-EYVBEFKK.js +1 -0
  15. package/dist/esm/chunks/{chunk-DM5LEAWM.js → chunk-F3OXGENC.js} +1 -1
  16. package/dist/esm/chunks/{chunk-OENVX4NY.js → chunk-HEK66LYK.js} +1 -1
  17. package/dist/esm/chunks/chunk-I7XE7GQR.js +1 -0
  18. package/dist/esm/chunks/chunk-KIZBET7N.js +1 -0
  19. package/dist/esm/chunks/chunk-UJYL5G7T.js +1 -0
  20. package/dist/esm/chunks/{chunk-CRKJ2VEX.js → chunk-UM37ZVSP.js} +1 -1
  21. package/dist/esm/chunks/chunk-YW3ZIFWB.js +1 -0
  22. package/dist/esm/chunks/{diagnose-XSZPWEG7.js → diagnose-AOSBLXOJ.js} +1 -1
  23. package/dist/esm/chunks/{extras-DZIOTZ3H.js → extras-THTXX76O.js} +1 -1
  24. package/dist/esm/chunks/lists-UJMD7BCQ.js +1 -0
  25. package/dist/esm/chunks/{markets-ui-DEEYV4KT.js → markets-ui-PTAY3FHU.js} +1 -1
  26. package/dist/esm/chunks/search-G2SRQXDG.js +1 -0
  27. package/dist/esm/index.js +1 -1
  28. package/dist/esm/server.js +2 -2
  29. package/dist/tether.account-pages.js +3 -3
  30. package/dist/tether.account-pages.min.js +1 -1
  31. package/dist/tether.account.js +20 -20
  32. package/dist/tether.account.min.js +1 -1
  33. package/dist/tether.analytics.js +2 -2
  34. package/dist/tether.analytics.min.js +1 -1
  35. package/dist/tether.diagnose.js +2 -2
  36. package/dist/tether.diagnose.min.js +1 -1
  37. package/dist/tether.extras.js +1 -1
  38. package/dist/tether.extras.min.js +1 -1
  39. package/dist/tether.js +248 -129
  40. package/dist/tether.lists.js +12 -8
  41. package/dist/tether.lists.min.js +1 -1
  42. package/dist/tether.markets.js +2 -2
  43. package/dist/tether.markets.min.js +1 -1
  44. package/dist/tether.min.js +1 -1
  45. package/dist/tether.min.js.map +4 -4
  46. package/dist/tether.search.js +60 -23
  47. package/dist/tether.search.min.js +1 -1
  48. package/dist/types/server/src/shared/metafield-ref.d.ts +15 -0
  49. package/dist/types/src/account/server-client.d.ts +2 -1
  50. package/dist/types/src/analytics-queue.d.ts +9 -0
  51. package/dist/types/src/cart/checkout.d.ts +2 -2
  52. package/dist/types/src/cart/drawer.d.ts +9 -4
  53. package/dist/types/src/cart/store.d.ts +2 -0
  54. package/dist/types/src/cart.d.ts +1 -1
  55. package/dist/types/src/client.d.ts +6 -0
  56. package/dist/types/src/dom.d.ts +25 -1
  57. package/dist/types/src/index.d.ts +1 -1
  58. package/dist/types/src/search.d.ts +1 -1
  59. package/dist/types/src/shopify-values.d.ts +2 -0
  60. package/dist/types/src/version.d.ts +1 -1
  61. package/package.json +1 -1
  62. package/dist/esm/chunks/account-XBGNTM7V.js +0 -1
  63. package/dist/esm/chunks/chunk-2J3MTS3E.js +0 -1
  64. package/dist/esm/chunks/chunk-4FR3CUO2.js +0 -1
  65. package/dist/esm/chunks/chunk-7ZXEVDOS.js +0 -1
  66. package/dist/esm/chunks/chunk-FI4NIWAD.js +0 -1
  67. package/dist/esm/chunks/chunk-H6IORH7O.js +0 -1
  68. package/dist/esm/chunks/chunk-OATTAXPO.js +0 -1
  69. package/dist/esm/chunks/chunk-OJAV2I3C.js +0 -1
  70. package/dist/esm/chunks/chunk-Y3NKKN2O.js +0 -1
  71. package/dist/esm/chunks/lists-OJGSVQMM.js +0 -1
  72. package/dist/esm/chunks/search-A6HF5PMJ.js +0 -1
package/dist/tether.js CHANGED
@@ -28,9 +28,19 @@
28
28
  return isRecord(value) ? value[key] : void 0;
29
29
  }
30
30
 
31
- // src/config-sections.ts
32
- var METAFIELD_PART = /^[A-Za-z0-9_.-]+$/;
31
+ // server/src/shared/metafield-ref.ts
32
+ var METAFIELD_KEY = /^[A-Za-z0-9_.-]+$/;
33
33
  var METAFIELD_NAMESPACE = /^(\$app(:[A-Za-z0-9_-]+)?|[A-Za-z0-9_.-]+)$/;
34
+ var isMetafieldIdentifier = (namespace, key) => METAFIELD_NAMESPACE.test(namespace) && METAFIELD_KEY.test(key);
35
+ function metafieldIdentifierOf(ref) {
36
+ const dot = ref.indexOf(".");
37
+ if (dot <= 0) return null;
38
+ const namespace = ref.slice(0, dot);
39
+ const key = ref.slice(dot + 1);
40
+ return isMetafieldIdentifier(namespace, key) ? { namespace, key } : null;
41
+ }
42
+
43
+ // src/config-sections.ts
34
44
  var HOSTNAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
35
45
  var COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
36
46
  var GLOBAL_NAME = /^[A-Za-z_$][\w$]*$/;
@@ -99,18 +109,12 @@
99
109
  return origins;
100
110
  }
101
111
  function toMetafieldIdentifier(ref) {
102
- let namespace;
103
- let key;
104
- if (typeof ref === "string") {
105
- const dot = ref.indexOf(".");
106
- namespace = dot > 0 ? ref.slice(0, dot) : "";
107
- key = ref.slice(dot + 1);
108
- } else if (typeof ref === "object" && ref !== null) {
109
- namespace = Reflect.get(ref, "namespace");
110
- key = Reflect.get(ref, "key");
111
- }
112
+ if (typeof ref === "string") return metafieldIdentifierOf(ref);
113
+ if (typeof ref !== "object" || ref === null) return null;
114
+ const namespace = Reflect.get(ref, "namespace");
115
+ const key = Reflect.get(ref, "key");
112
116
  if (typeof namespace !== "string" || typeof key !== "string") return null;
113
- return METAFIELD_NAMESPACE.test(namespace) && METAFIELD_PART.test(key) ? { namespace, key } : null;
117
+ return isMetafieldIdentifier(namespace, key) ? { namespace, key } : null;
114
118
  }
115
119
  function metafieldsOption(value, invalid) {
116
120
  if (!Array.isArray(value)) {
@@ -220,6 +224,45 @@
220
224
  return LANGUAGE.test(primary) ? primary : null;
221
225
  }
222
226
 
227
+ // src/shopify-values.ts
228
+ var MAX_CONNECTION_SIZE = 250;
229
+ function isHttpsUrl(url2) {
230
+ if (typeof url2 !== "string") return false;
231
+ try {
232
+ return new URL(url2).protocol === "https:";
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+ function formatMoney(money, locale) {
238
+ if (!money) return "";
239
+ const amount = parseFloat(money.amount);
240
+ if (Number.isNaN(amount)) return "";
241
+ try {
242
+ return new Intl.NumberFormat(locale || void 0, { style: "currency", currency: money.currencyCode }).format(
243
+ amount
244
+ );
245
+ } catch {
246
+ return `${money.currencyCode} ${amount.toFixed(2)}`;
247
+ }
248
+ }
249
+ function productUrl(productPath, handle, id) {
250
+ if (!/\{(handle|id)\}/.test(productPath)) {
251
+ return handle ? `${productPath.replace(/\/$/, "")}/${encodeURIComponent(handle)}` : null;
252
+ }
253
+ const numericId2 = id ? id.split("/").at(-1) ?? "" : "";
254
+ if (productPath.includes("{handle}") && !handle) return null;
255
+ if (productPath.includes("{id}") && !numericId2) return null;
256
+ return productPath.replace(/\{handle\}/g, encodeURIComponent(handle ?? "")).replace(/\{id\}/g, encodeURIComponent(numericId2));
257
+ }
258
+ function productKey(raw) {
259
+ const value = String(raw ?? "").trim();
260
+ if (!value) return null;
261
+ if (value.startsWith("gid://")) return value;
262
+ if (/^\d+$/.test(value)) return `gid://shopify/Product/${value}`;
263
+ return /\/products\/([^/?#]+)/.exec(value)?.[1] ?? value;
264
+ }
265
+
223
266
  // src/config.ts
224
267
  var DEFAULTS = {
225
268
  domain: "",
@@ -270,22 +313,29 @@
270
313
  debug: false
271
314
  };
272
315
  var cfg = { ...DEFAULTS };
316
+ function refuseChange(key) {
317
+ warn(`Tether.config is read-only (${String(key)}): pass options to Tether.init() instead`);
318
+ return false;
319
+ }
320
+ var readOnlyViews = /* @__PURE__ */ new WeakMap();
321
+ var READ_ONLY = {
322
+ get(object, key) {
323
+ const value = Reflect.get(object, key);
324
+ return typeof value === "object" && value !== null ? readOnlyView(value) : value;
325
+ },
326
+ set: (_object, key) => refuseChange(key),
327
+ deleteProperty: (_object, key) => refuseChange(key),
328
+ defineProperty: (_object, key) => refuseChange(key)
329
+ };
273
330
  function readOnlyView(target) {
274
- const refuse = (key) => {
275
- warn(`Tether.config is read-only (${String(key)}): pass options to Tether.init() instead`);
276
- return false;
277
- };
278
- return new Proxy(target, {
279
- get(object, key) {
280
- const value = Reflect.get(object, key);
281
- return typeof value === "object" && value !== null ? readOnlyView(value) : value;
282
- },
283
- set: (_object, key) => refuse(key),
284
- deleteProperty: (_object, key) => refuse(key),
285
- defineProperty: (_object, key) => refuse(key)
286
- });
331
+ let view = readOnlyViews.get(target);
332
+ if (!view) {
333
+ view = new Proxy(target, READ_ONLY);
334
+ readOnlyViews.set(target, view);
335
+ }
336
+ return view;
287
337
  }
288
- var configView = readOnlyView(cfg);
338
+ var configView = new Proxy(cfg, READ_ONLY);
289
339
  var PREFIX = /^[a-z][a-z0-9-]*$/;
290
340
  var SWITCH_OPTIONS = [
291
341
  "revalidate",
@@ -297,7 +347,7 @@
297
347
  "astro",
298
348
  "debug"
299
349
  ];
300
- var TEXT_OPTIONS = ["token", "productPath", "collectionPath", "cartDrawerOpenClass"];
350
+ var TEXT_OPTIONS = ["token", "productPath", "collectionPath"];
301
351
  var COUNT_OPTIONS = [
302
352
  "imagesFirst",
303
353
  "variantsFirst",
@@ -330,7 +380,7 @@
330
380
  }
331
381
  function warnUnknownKeys(options) {
332
382
  for (const key of Object.keys(options)) {
333
- if (!(key in DEFAULTS)) warn(`unknown option ${JSON.stringify(key)}: ignored`);
383
+ if (!Object.hasOwn(DEFAULTS, key)) warn(`unknown option ${JSON.stringify(key)}: ignored`);
334
384
  }
335
385
  }
336
386
  function setHook(config, name, value) {
@@ -342,16 +392,22 @@
342
392
  }
343
393
  var MAX_TIMER_MS = 2147483647;
344
394
  var MAX_RETRIES = 10;
345
- var COUNT_MAXIMUMS = {
346
- requestTimeout: MAX_TIMER_MS,
347
- searchDelay: MAX_TIMER_MS,
348
- retries: MAX_RETRIES
395
+ var CONNECTION_RANGE = { min: 1, max: MAX_CONNECTION_SIZE };
396
+ var COUNT_RANGES = {
397
+ imagesFirst: CONNECTION_RANGE,
398
+ variantsFirst: CONNECTION_RANGE,
399
+ sellingPlanGroupsFirst: CONNECTION_RANGE,
400
+ sellingPlansFirst: CONNECTION_RANGE,
401
+ collectionFirst: CONNECTION_RANGE,
402
+ requestTimeout: { max: MAX_TIMER_MS },
403
+ searchDelay: { max: MAX_TIMER_MS },
404
+ retries: { max: MAX_RETRIES }
349
405
  };
350
406
  function countOption(name, value) {
351
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
407
+ const { min = 0, max: maximum } = COUNT_RANGES[name] ?? {};
408
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
352
409
  return invalidOption(name, value, DEFAULTS[name]);
353
410
  }
354
- const maximum = COUNT_MAXIMUMS[name];
355
411
  if (maximum === void 0 || value <= maximum) return value;
356
412
  warn(`${name} ${String(value)} is more than its maximum: using ${String(maximum)}`);
357
413
  return maximum;
@@ -362,6 +418,10 @@
362
418
  function textOption(name, value) {
363
419
  return typeof value === "string" ? value : invalidOption(name, value, DEFAULTS[name]);
364
420
  }
421
+ var CLASS_TOKEN = /^\S+$/;
422
+ function classOption(name, value) {
423
+ return typeof value === "string" && CLASS_TOKEN.test(value) ? value : invalidOption(name, value, DEFAULTS[name]);
424
+ }
365
425
  function localeOption(value) {
366
426
  if (value === void 0 || value === null || value === "") return void 0;
367
427
  try {
@@ -408,7 +468,8 @@
408
468
  metafields: metafieldsOption(options.metafields ?? [], dropped),
409
469
  embedOrigins: embedOriginsOption(options.embedOrigins ?? [], dropped),
410
470
  checkoutTarget: choiceOption("checkoutTarget", options.checkoutTarget, ["_self", "_blank"]),
411
- defaultPurchase: choiceOption("defaultPurchase", options.defaultPurchase, ["one-time", "subscription"])
471
+ defaultPurchase: choiceOption("defaultPurchase", options.defaultPurchase, ["one-time", "subscription"]),
472
+ cartDrawerOpenClass: classOption("cartDrawerOpenClass", options.cartDrawerOpenClass)
412
473
  };
413
474
  for (const name of COUNT_OPTIONS) resolved[name] = countOption(name, options[name]);
414
475
  for (const name of SWITCH_OPTIONS) resolved[name] = switchOption(name, options[name]);
@@ -881,44 +942,6 @@
881
942
  return template.content;
882
943
  }
883
944
 
884
- // src/shopify-values.ts
885
- function isHttpsUrl(url2) {
886
- if (typeof url2 !== "string") return false;
887
- try {
888
- return new URL(url2).protocol === "https:";
889
- } catch {
890
- return false;
891
- }
892
- }
893
- function formatMoney(money, locale) {
894
- if (!money) return "";
895
- const amount = parseFloat(money.amount);
896
- if (Number.isNaN(amount)) return "";
897
- try {
898
- return new Intl.NumberFormat(locale || void 0, { style: "currency", currency: money.currencyCode }).format(
899
- amount
900
- );
901
- } catch {
902
- return `${money.currencyCode} ${amount.toFixed(2)}`;
903
- }
904
- }
905
- function productUrl(productPath, handle, id) {
906
- if (!/\{(handle|id)\}/.test(productPath)) {
907
- return handle ? `${productPath.replace(/\/$/, "")}/${encodeURIComponent(handle)}` : null;
908
- }
909
- const numericId2 = id ? id.split("/").at(-1) ?? "" : "";
910
- if (productPath.includes("{handle}") && !handle) return null;
911
- if (productPath.includes("{id}") && !numericId2) return null;
912
- return productPath.replace(/\{handle\}/g, encodeURIComponent(handle ?? "")).replace(/\{id\}/g, encodeURIComponent(numericId2));
913
- }
914
- function productKey(raw) {
915
- const value = String(raw ?? "").trim();
916
- if (!value) return null;
917
- if (value.startsWith("gid://")) return value;
918
- if (/^\d+$/.test(value)) return `gid://shopify/Product/${value}`;
919
- return /\/products\/([^/?#]+)/.exec(value)?.[1] ?? value;
920
- }
921
-
922
945
  // src/images.ts
923
946
  function imageUrl(url2, { width, height, crop } = {}) {
924
947
  if (!url2) return url2;
@@ -1152,13 +1175,33 @@
1152
1175
  }
1153
1176
  el.style.display = "none";
1154
1177
  }
1178
+ var CHECKABLE = 'input[type="radio"],input[type="checkbox"]';
1179
+ var CHECKED_ROLES = ["radio", "checkbox", "switch", "menuitemradio", "menuitemcheckbox"];
1180
+ var BUTTON_INPUTS = ["button", "submit", "reset"];
1181
+ function chosenState(el) {
1182
+ const role = el.getAttribute("role");
1183
+ if (role === "button") return "aria-pressed";
1184
+ if (role && CHECKED_ROLES.includes(role)) return "aria-checked";
1185
+ if (role === "option" || role === "tab") return "aria-selected";
1186
+ if (role) return null;
1187
+ if (isInput(el)) return BUTTON_INPUTS.includes(el.type) ? "aria-pressed" : null;
1188
+ return el.querySelector(CHECKABLE) ? null : "aria-pressed";
1189
+ }
1155
1190
  function setSelected(el, selected) {
1156
1191
  el.classList.toggle("is-selected", selected);
1157
- el.setAttribute("aria-pressed", selected ? "true" : "false");
1158
- if (isInput(el) && (el.type === "radio" || el.type === "checkbox")) el.checked = selected;
1159
- const input = el.querySelector('input[type="radio"],input[type="checkbox"]');
1192
+ const state = chosenState(el);
1193
+ if (state === "aria-pressed" && el.tagName !== "BUTTON" && !isInput(el)) el.setAttribute("role", "button");
1194
+ if (state) el.setAttribute(state, String(selected));
1195
+ if (isInput(el) && el.matches(CHECKABLE)) el.checked = selected;
1196
+ const input = el.querySelector(CHECKABLE);
1160
1197
  if (input && isInput(input)) input.checked = selected;
1161
1198
  }
1199
+ function setUnavailable(el, unavailable) {
1200
+ el.classList.toggle("is-unavailable", unavailable);
1201
+ const isControl = chosenState(el) !== null || isInput(el);
1202
+ const control = isControl ? el : el.querySelector(CHECKABLE);
1203
+ control?.setAttribute("aria-disabled", String(unavailable));
1204
+ }
1162
1205
  var layoutTimer;
1163
1206
  var pageGlobal = (name) => Reflect.get(window, name);
1164
1207
  function refreshScrollTrigger() {
@@ -1209,6 +1252,31 @@
1209
1252
  return null;
1210
1253
  }
1211
1254
  }
1255
+ var INTEGER_TEXT = /^-?\d+$/;
1256
+ var invalidIntegerAttributes = /* @__PURE__ */ new WeakMap();
1257
+ function warnInvalidAttribute(el, name, message) {
1258
+ const warned = invalidIntegerAttributes.get(el) ?? /* @__PURE__ */ new Set();
1259
+ invalidIntegerAttributes.set(el, warned);
1260
+ if (warned.has(name)) return;
1261
+ warned.add(name);
1262
+ warn(`${attr(name)} ${message}`, el);
1263
+ }
1264
+ function integerAttribute(el, name, { min, max, fallback }) {
1265
+ const raw = get(el, name)?.trim();
1266
+ if (!raw) return fallback;
1267
+ const value = INTEGER_TEXT.test(raw) ? Number(raw) : Number.NaN;
1268
+ if (Number.isNaN(value) || value < min) {
1269
+ warnInvalidAttribute(
1270
+ el,
1271
+ name,
1272
+ `${JSON.stringify(raw)} is not a whole number from ${String(min)}: using ${String(fallback)}`
1273
+ );
1274
+ return fallback;
1275
+ }
1276
+ if (max === void 0 || value <= max) return value;
1277
+ warnInvalidAttribute(el, name, `${raw} is more than its maximum: using ${String(max)}`);
1278
+ return max;
1279
+ }
1212
1280
  function attributeText(value) {
1213
1281
  if (value === null || value === void 0) return "";
1214
1282
  if (typeof value === "string") return value;
@@ -1434,28 +1502,43 @@
1434
1502
  var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
1435
1503
  var FOCUS_AFTER_OPEN_MS = 60;
1436
1504
  var openWithoutDrawer = false;
1505
+ var announcedOpen = false;
1437
1506
  var returnFocus = null;
1507
+ var classWatcher = null;
1438
1508
  var isFocusable = (el) => el !== null && "focus" in el;
1439
1509
  var focusables = (drawer) => qsa(drawer, FOCUSABLE).filter((el) => el.offsetParent !== null || el === document.activeElement);
1440
1510
  var drawers = () => qsa(document, sel("cart-drawer"));
1441
1511
  var isOpenDrawer = (drawer) => drawer.classList.contains(cfg.cartDrawerOpenClass);
1512
+ var hasFocusInside = (all) => all.some((drawer) => drawer.contains(document.activeElement));
1513
+ var isManual = (drawer) => get(drawer, "cart-drawer") === "manual";
1442
1514
  function isOpen() {
1443
1515
  const all = drawers();
1444
1516
  return all.length ? all.some(isOpenDrawer) : openWithoutDrawer;
1445
1517
  }
1446
- function syncDrawerAria(open) {
1518
+ function watchOpenClass(drawer) {
1519
+ classWatcher ?? (classWatcher = new MutationObserver(() => {
1520
+ const open = isOpen();
1521
+ if (open === announcedOpen) return;
1522
+ const all = drawers();
1523
+ if (open) announceOpen(all.filter(isOpenDrawer));
1524
+ else announceClosed(all, hasFocusInside(all));
1525
+ }));
1526
+ classWatcher.observe(drawer, { attributes: true, attributeFilter: ["class"] });
1527
+ }
1528
+ function syncDrawerAria() {
1447
1529
  qsa(document, sel("cart-open")).forEach((button) => {
1448
- button.setAttribute("aria-expanded", String(open));
1530
+ button.setAttribute("aria-expanded", String(isOpen()));
1449
1531
  });
1450
1532
  drawers().forEach((drawer) => {
1451
1533
  if (!drawer.hasAttribute("role")) drawer.setAttribute("role", "dialog");
1452
1534
  if (!drawer.hasAttribute("aria-label") && !drawer.hasAttribute("aria-labelledby")) {
1453
1535
  drawer.setAttribute("aria-label", get(drawer, "cart-label") || "Cart");
1454
1536
  }
1537
+ const open = isOpenDrawer(drawer);
1455
1538
  drawer.setAttribute("aria-modal", String(open));
1456
- if (isOpenDrawer(drawer)) return;
1457
- drawer.setAttribute("aria-hidden", "true");
1458
- if (cfg.cartFocus) drawer.inert = true;
1539
+ drawer.setAttribute("aria-hidden", String(!open));
1540
+ drawer.inert = !open;
1541
+ watchOpenClass(drawer);
1459
1542
  });
1460
1543
  }
1461
1544
  function focusInto(drawer) {
@@ -1463,16 +1546,9 @@
1463
1546
  if (target === drawer && !drawer.hasAttribute("tabindex")) drawer.tabIndex = -1;
1464
1547
  target.focus({ preventScroll: true });
1465
1548
  }
1466
- function openCart() {
1467
- if (isOpen()) return;
1468
- openWithoutDrawer = true;
1469
- const opened = drawers().filter((drawer) => get(drawer, "cart-drawer") !== "manual");
1470
- opened.forEach((drawer) => {
1471
- drawer.inert = false;
1472
- drawer.classList.add(cfg.cartDrawerOpenClass);
1473
- drawer.setAttribute("aria-hidden", "false");
1474
- });
1475
- syncDrawerAria(true);
1549
+ function announceOpen(opened) {
1550
+ announcedOpen = true;
1551
+ syncDrawerAria();
1476
1552
  const [first] = opened;
1477
1553
  if (cfg.cartFocus && first) {
1478
1554
  returnFocus = isFocusable(document.activeElement) ? document.activeElement : null;
@@ -1483,21 +1559,34 @@
1483
1559
  cfg.onCartOpen?.(opened);
1484
1560
  emit("cart-toggled", { open: true });
1485
1561
  }
1562
+ function announceClosed(all, hadFocus) {
1563
+ announcedOpen = false;
1564
+ syncDrawerAria();
1565
+ if (hadFocus && returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
1566
+ returnFocus = null;
1567
+ cfg.onCartClose?.(all);
1568
+ emit("cart-toggled", { open: false });
1569
+ }
1570
+ function openCart() {
1571
+ if (isOpen()) return;
1572
+ const all = drawers();
1573
+ const opened = all.filter((drawer) => !isManual(drawer));
1574
+ if (all.length && !opened.length) return;
1575
+ openWithoutDrawer = true;
1576
+ opened.forEach((drawer) => {
1577
+ drawer.classList.add(cfg.cartDrawerOpenClass);
1578
+ });
1579
+ announceOpen(opened);
1580
+ }
1486
1581
  function closeCart() {
1487
1582
  if (!isOpen()) return;
1488
1583
  openWithoutDrawer = false;
1489
1584
  const all = drawers();
1490
- const hadFocus = all.some((drawer) => drawer.contains(document.activeElement));
1585
+ const hadFocus = hasFocusInside(all);
1491
1586
  all.forEach((drawer) => {
1492
1587
  drawer.classList.remove(cfg.cartDrawerOpenClass);
1493
- drawer.setAttribute("aria-hidden", "true");
1494
- if (cfg.cartFocus) drawer.inert = true;
1495
1588
  });
1496
- syncDrawerAria(false);
1497
- if (hadFocus && returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
1498
- returnFocus = null;
1499
- cfg.onCartClose?.(all);
1500
- emit("cart-toggled", { open: false });
1589
+ announceClosed(all, hadFocus);
1501
1590
  }
1502
1591
  function toggleCart() {
1503
1592
  if (isOpen()) closeCart();
@@ -1969,7 +2058,7 @@
1969
2058
  function renderCart(view) {
1970
2059
  const { cart } = view;
1971
2060
  const money = (key) => cart ? formatMoney2(cart.cost[key]) : "";
1972
- syncDrawerAria(isOpen());
2061
+ syncDrawerAria();
1973
2062
  qsa(document, sel("cart-count")).forEach((el) => {
1974
2063
  setTextTracked(el, cart?.totalQuantity ?? 0);
1975
2064
  });
@@ -2282,14 +2371,23 @@
2282
2371
  } catch {
2283
2372
  }
2284
2373
  }
2285
- async function checkout(target = cfg.checkoutTarget) {
2286
- const cartId = savedCartId();
2374
+ function cartToCheckOut() {
2287
2375
  const cart = currentCart();
2288
- if (!cartId || cart && !cart.totalQuantity) return null;
2376
+ return cart && !cart.totalQuantity ? null : savedCartId();
2377
+ }
2378
+ async function checkout(target = cfg.checkoutTarget) {
2379
+ if (!cartToCheckOut()) return null;
2289
2380
  const tab = target === "_blank" ? window.open("", "_blank") : null;
2290
2381
  if (tab) detachOpenerIfAllowed(tab);
2291
2382
  try {
2292
- const url2 = await checkoutUrl(cartId);
2383
+ const url2 = await runInOrder(async () => {
2384
+ const cartId = cartToCheckOut();
2385
+ return cartId && checkoutUrl(cartId);
2386
+ });
2387
+ if (!url2) {
2388
+ tab?.close();
2389
+ return null;
2390
+ }
2293
2391
  if (!isHttpsUrl(url2)) throw new TetherError("The checkout URL is not https: not opened");
2294
2392
  emit("checkout", { url: url2, cart: currentCart() });
2295
2393
  if (tab && !tab.closed) tab.location.href = url2;
@@ -2532,7 +2630,8 @@
2532
2630
  *
2533
2631
  * @throws {TetherError} `INVALID_DISCOUNT` (rejected) when Shopify doesn't accept the code.
2534
2632
  */
2535
- async applyDiscount(code) {
2633
+ async applyDiscount(typed) {
2634
+ const code = typed.trim();
2536
2635
  const cart = await Cart.setDiscountCodes((current) => [.../* @__PURE__ */ new Set([...current, code])]);
2537
2636
  const wanted = code.toLowerCase();
2538
2637
  const applied = (cart?.discountCodes ?? []).find((discount) => discount.code.toLowerCase() === wanted);
@@ -2545,7 +2644,7 @@
2545
2644
  throw new TetherError(`Discount code not valid: ${code}`, { code: "INVALID_DISCOUNT" });
2546
2645
  },
2547
2646
  removeDiscount(code) {
2548
- const unwanted = code.toLowerCase();
2647
+ const unwanted = code.trim().toLowerCase();
2549
2648
  return Cart.setDiscountCodes((current) => current.filter((existing) => existing.toLowerCase() !== unwanted));
2550
2649
  },
2551
2650
  /**
@@ -2803,7 +2902,7 @@
2803
2902
  const value = get(node, "option-value") ?? "";
2804
2903
  setSelected(node, view.selected[name] === value);
2805
2904
  const unavailable = !isValueAvailable(product, view.selected, name, value);
2806
- node.classList.toggle("is-unavailable", unavailable);
2905
+ setUnavailable(node, unavailable);
2807
2906
  node.setAttribute("data-unavailable", unavailable ? "true" : "false");
2808
2907
  });
2809
2908
  });
@@ -2823,13 +2922,12 @@
2823
2922
  }
2824
2923
  const mode = get(el, "purchase");
2825
2924
  setSelected(el, mode === purchase.kind);
2826
- const unavailable = mode === "subscription" ? !view.subscriptionAvailable : scope.product.requiresSellingPlan;
2827
- el.classList.toggle("is-unavailable", unavailable);
2925
+ setUnavailable(el, mode === "subscription" ? !view.subscriptionAvailable : scope.product.requiresSellingPlan);
2828
2926
  });
2829
2927
  scope.$(sel("plan-value")).forEach((node) => {
2830
2928
  const planId = get(node, "plan-value") ?? "";
2831
2929
  setSelected(node, line.kind === "subscription" && planId === line.planId);
2832
- node.classList.toggle("is-unavailable", !allocationFor(variant, planId));
2930
+ setUnavailable(node, !allocationFor(variant, planId));
2833
2931
  const plan = scope.plans.find((candidate) => candidate.id === planId);
2834
2932
  const texts = planBindTexts(variant, planId, plan, FORMAT);
2835
2933
  qsa(node, sel("plan-bind")).forEach((el) => {
@@ -3223,7 +3321,12 @@
3223
3321
  function applyPageTitle(el, product) {
3224
3322
  const template = get(el, "page-title");
3225
3323
  if (!template || typeof document === "undefined") return;
3226
- document.title = template.replace(/\{title\}/g, product.title).replace(/\{vendor\}/g, product.vendor || "").replace(/\{type\}/g, product.productType || "");
3324
+ const fields = /* @__PURE__ */ new Map([
3325
+ ["title", product.title],
3326
+ ["vendor", product.vendor || ""],
3327
+ ["type", product.productType || ""]
3328
+ ]);
3329
+ document.title = template.replace(/\{(\w+)\}/g, (placeholder, name) => fields.get(name) ?? placeholder);
3227
3330
  }
3228
3331
  function mountWithoutKey(el, { fromUrl, raw }) {
3229
3332
  if (fromUrl) {
@@ -3440,7 +3543,7 @@
3440
3543
  }
3441
3544
 
3442
3545
  // src/part-integrity.ts
3443
- var PART_INTEGRITY = { "tether.diagnose.js": "sha384-6b98ZqhqrOFydwTyklda6rjvyWrgz1OnmffApLQ3+gg6VPSU1Yy3LhR1OT4lprfa", "tether.markets.js": "sha384-f9ZeUfc/tMICwVFPrC44Sa1oxm9c6jTQASJdNhHWDk55thsEVScodDBKSYUiuDgr", "tether.search.js": "sha384-kNQI7aVmvUyNgV2e6+PRy53xmbKiZi2n1hhYmHUDFCGU3BR6SoGpGKVKVbPE0ZtG", "tether.lists.js": "sha384-MfQgl9RjiTn6vr80fQymnFikrsDXh3wcmT8cu858s7z9p/oByQaJUKZo92qhjX1P", "tether.extras.js": "sha384-H4CWsEibHw/99UiBAi366ctyaNZrVMkK6Y1YJYYcGDbNthy5eQdS+RRcrlm1clmQ", "tether.account.js": "sha384-UeOp/wddJlVjmlYcVqqO9hU7ANLYEtY/TtK/y5cFkQ6J+SPBmqI/VReaAh+reCMC", "tether.account-pages.js": "sha384-jBeU+vF/U13v+9LZxg5R8lQb/PDHTXPVvu1EmsdDUrlh7BmIntbZJh2+iT6YykTy", "tether.analytics.js": "sha384-4gJ/lcu+S+sfpEHu64+0m2zbXpvNsyOxKDQchPEIb1SFg3ky1JlXODNSdRoodST9" };
3546
+ var PART_INTEGRITY = { "tether.diagnose.js": "sha384-CqtXzvWbLD8aIgyTM43+KMXEFA7r7lhG+0XyQAw3Sp7B5qrzPsxlA9QfzBjoPqZz", "tether.markets.js": "sha384-WjowbRhM7OzzZOrCPZWSZqN7EjIiiR7u9cGmdI+9kgFVgmEQS8lVGWjgx+8jILOf", "tether.search.js": "sha384-fLQ7eKc5XOv0Cg2FfrZyeFAkRR4wDMjUWFpwvKrfPRnT2NvTuhKg80+KVu8Qce/G", "tether.lists.js": "sha384-l2uyr96zTetjo2WWhGsBamd1ZYkC3OB3G3+OScLEvxdok0c8k9GXLAh1WRALUr4S", "tether.extras.js": "sha384-/SMjz6qCFAOV0wwvEVNhQxtT/1pyx4hRxDJHOxHB6M3uclAvItSULKZqJvaehH+i", "tether.account.js": "sha384-XUPSX/7R9chq2KlQ1RzLiV7jYcWGe7o5tj4rLZMnf8MEa2aDOOCW2269Bj/ZkbmX", "tether.account-pages.js": "sha384-ND3kIaRypgnHG3QMDb8CqDxt7fuukDCdHsIAHoOUUsz3+FjvBqXgRDV4Mjp6x8jr", "tether.analytics.js": "sha384-jnLesn/+edWBSPuXLj7VX1kSAGwEqtb0VbHjuU//ofqgxkndHlDFsS8Bc8htlGt8" };
3444
3547
 
3445
3548
  // src/part-registry.ts
3446
3549
  function isTetherInternals(value) {
@@ -3448,7 +3551,7 @@
3448
3551
  }
3449
3552
 
3450
3553
  // src/version.ts
3451
- var VERSION = "0.9.2";
3554
+ var VERSION = "0.9.4";
3452
3555
 
3453
3556
  // src/lazy-iife.ts
3454
3557
  var coreModules = {
@@ -3456,7 +3559,7 @@
3456
3559
  return [cfg, isConfigured, log, MARKET_SOURCES, parseMarkets, reportError, warn];
3457
3560
  },
3458
3561
  get "client.ts"() {
3459
- return [endpoint, gql, matches, orNull, shaped, TetherError];
3562
+ return [endpoint, gql, matches, orNull, shaped, TetherError, withTimeout];
3460
3563
  },
3461
3564
  get "dom.ts"() {
3462
3565
  return [
@@ -3470,6 +3573,7 @@
3470
3573
  get,
3471
3574
  has,
3472
3575
  hasTemplate,
3576
+ integerAttribute,
3473
3577
  isOwnEvent,
3474
3578
  notifyLayoutChanged,
3475
3579
  parseJSONAttr,
@@ -3515,7 +3619,7 @@
3515
3619
  return [setCountry];
3516
3620
  },
3517
3621
  get "shopify-values.ts"() {
3518
- return [isHttpsUrl];
3622
+ return [isHttpsUrl, MAX_CONNECTION_SIZE];
3519
3623
  },
3520
3624
  // Parts that load other parts (account → account-pages) use this same loader.
3521
3625
  get "lazy.ts"() {
@@ -3605,6 +3709,23 @@
3605
3709
  };
3606
3710
  }
3607
3711
 
3712
+ // src/analytics-queue.ts
3713
+ function queueAnalyticsEvents(load) {
3714
+ const queue = [];
3715
+ const listening = new window.AbortController();
3716
+ const options = { signal: listening.signal };
3717
+ ANALYTICS_EVENTS.forEach((name) => {
3718
+ document.addEventListener(`${cfg.prefix}:${name}`, (event) => queue.push(event), options);
3719
+ });
3720
+ document.addEventListener("astro:page-load", (event) => queue.push(event), options);
3721
+ load().then((part) => part.startAnalytics(queue)).catch((error) => {
3722
+ listening.abort();
3723
+ queue.length = 0;
3724
+ reportError(error, "analytics");
3725
+ });
3726
+ return queue;
3727
+ }
3728
+
3608
3729
  // src/account-marks.ts
3609
3730
  var hintKey = () => `${cfg.prefix}:account-hint:${cfg.domain}`;
3610
3731
  function removeFromBothAreas(key) {
@@ -3735,12 +3856,21 @@
3735
3856
  line.setAttribute("aria-busy", "true");
3736
3857
  reportFailure(change, context);
3737
3858
  }
3859
+ function onQuantityInput(input, { line, id }) {
3860
+ const typed = input.value.trim();
3861
+ if (INTEGER_TEXT.test(typed)) {
3862
+ showLineBusy(line, Cart.updateLine(id, Number(typed)), "cart line");
3863
+ return;
3864
+ }
3865
+ const current = Cart.lines.find((candidate) => candidate.id === id);
3866
+ if (current) input.value = String(Cart.quantityOf(current));
3867
+ }
3738
3868
  function onChange2(event) {
3739
3869
  const { target } = event;
3740
3870
  if (!isElement2(target)) return;
3741
3871
  const found = lineOf(target);
3742
3872
  if (found && target instanceof window.HTMLInputElement && target.matches(sel("line", "quantity"))) {
3743
- showLineBusy(found.line, Cart.updateLine(found.id, parseInt(target.value, 10) || 0), "cart line");
3873
+ onQuantityInput(target, found);
3744
3874
  } else if (found && target instanceof window.HTMLSelectElement && target.matches(sel("line-plan"))) {
3745
3875
  showLineBusy(found.line, Cart.updateLine(found.id, { sellingPlanId: target.value || null }), "cart line plan");
3746
3876
  } else if (hasValue(target) && target.matches(sel("cart-note"))) {
@@ -3991,17 +4121,6 @@
3991
4121
  bindExtrasControls();
3992
4122
  bindNavigation({ ready: () => initPromise ?? Promise.resolve(null), mount, unmount });
3993
4123
  }
3994
- function startAnalyticsQueue() {
3995
- const queue = [];
3996
- analyticsQueue = queue;
3997
- ANALYTICS_EVENTS.forEach((name) => {
3998
- document.addEventListener(`${cfg.prefix}:${name}`, (event) => queue.push(event));
3999
- });
4000
- document.addEventListener("astro:page-load", (event) => queue.push(event));
4001
- loadPart("analytics").then((part) => part.startAnalytics(queue)).catch((error) => {
4002
- reportError(error, "analytics");
4003
- });
4004
- }
4005
4124
  async function cartInPageMarket(cart) {
4006
4125
  const country = cfg.country;
4007
4126
  const cartCountry = cart?.buyerIdentity.countryCode;
@@ -4024,7 +4143,7 @@
4024
4143
  forgetBrowserModeTokens();
4025
4144
  bindControls();
4026
4145
  if (cfg.observe) startObserver();
4027
- if (cfg.analytics && !analyticsQueue) startAnalyticsQueue();
4146
+ if (cfg.analytics) analyticsQueue ?? (analyticsQueue = queueAnalyticsEvents(() => loadPart("analytics")));
4028
4147
  initPromise = applyStartMarket(overrides).then(() => Cart.init()).then(cartInPageMarket).then((cart) => {
4029
4148
  emit("ready", { cart });
4030
4149
  return api;