@occluze/tether 0.6.8 → 0.7.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 (259) hide show
  1. package/README.md +11 -11
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
  4. package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
  5. package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
  6. package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
  7. package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
  8. package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
  9. package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
  10. package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
  11. package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
  12. package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
  13. package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
  14. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  15. package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
  16. package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
  17. package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
  18. package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
  19. package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
  20. package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
  21. package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -2
  24. package/dist/presets/barba.esm.js +19 -23
  25. package/dist/tether.account-pages.js +320 -365
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +242 -491
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +66 -88
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +36 -33
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +28 -36
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +1270 -894
  36. package/dist/tether.markets.js +73 -90
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +44 -68
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  43. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  44. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  45. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  46. package/dist/types/server/src/shared/json.d.ts +25 -0
  47. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  48. package/dist/types/src/account/addresses-view.d.ts +5 -0
  49. package/dist/types/src/account/answers.d.ts +40 -0
  50. package/dist/types/src/account/markup.d.ts +19 -0
  51. package/dist/types/src/account/operations.d.ts +41 -0
  52. package/dist/types/src/account/order-lines.d.ts +13 -0
  53. package/dist/types/src/account/order-list.d.ts +12 -0
  54. package/dist/types/src/account/order-page.d.ts +6 -0
  55. package/dist/types/src/account/page-answers.d.ts +182 -0
  56. package/dist/types/src/account/page-events.d.ts +2 -0
  57. package/dist/types/src/account/server-client.d.ts +60 -0
  58. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  59. package/dist/types/src/account-api.d.ts +18 -0
  60. package/dist/types/src/account-marks.d.ts +15 -0
  61. package/dist/types/src/account-pages.d.ts +8 -0
  62. package/dist/types/src/account.d.ts +64 -0
  63. package/dist/types/src/amounts.d.ts +12 -0
  64. package/dist/types/src/analytics-shopify.d.ts +41 -0
  65. package/dist/types/src/analytics.d.ts +2 -0
  66. package/dist/types/src/api-version.d.ts +28 -0
  67. package/dist/types/src/astro.d.ts +42 -0
  68. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  69. package/dist/types/src/cart/checkout.d.ts +16 -0
  70. package/dist/types/src/cart/drawer.d.ts +18 -0
  71. package/dist/types/src/cart/mutations.d.ts +79 -0
  72. package/dist/types/src/cart/render.d.ts +17 -0
  73. package/dist/types/src/cart/store.d.ts +182 -0
  74. package/dist/types/src/cart.d.ts +106 -0
  75. package/dist/types/src/client.d.ts +100 -0
  76. package/dist/types/src/collection.d.ts +25 -0
  77. package/dist/types/src/config-sections.d.ts +40 -0
  78. package/dist/types/src/config.d.ts +185 -0
  79. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  80. package/dist/types/src/controls/discount.d.ts +2 -0
  81. package/dist/types/src/controls/extras.d.ts +5 -0
  82. package/dist/types/src/controls/navigation.d.ts +12 -0
  83. package/dist/types/src/diagnose-markup.d.ts +19 -0
  84. package/dist/types/src/diagnose.d.ts +60 -0
  85. package/dist/types/src/dom.d.ts +75 -0
  86. package/dist/types/src/extras.d.ts +21 -0
  87. package/dist/types/src/fragments.d.ts +158 -0
  88. package/dist/types/src/globals.d.ts +15 -0
  89. package/dist/types/src/images.d.ts +20 -0
  90. package/dist/types/src/index.d.ts +167 -0
  91. package/dist/types/src/initial.d.ts +56 -0
  92. package/dist/types/src/lazy.d.ts +28 -0
  93. package/dist/types/src/loader.d.ts +84 -0
  94. package/dist/types/src/market-codes.d.ts +6 -0
  95. package/dist/types/src/markets-ui.d.ts +32 -0
  96. package/dist/types/src/markets.d.ts +72 -0
  97. package/dist/types/src/markup.d.ts +131 -0
  98. package/dist/types/src/mounting.d.ts +46 -0
  99. package/dist/types/src/page-market.d.ts +27 -0
  100. package/dist/types/src/presets/barba.d.ts +56 -0
  101. package/dist/types/src/product-events.d.ts +2 -0
  102. package/dist/types/src/product-render.d.ts +17 -0
  103. package/dist/types/src/product-shape.d.ts +88 -0
  104. package/dist/types/src/product-state.d.ts +110 -0
  105. package/dist/types/src/product.d.ts +66 -0
  106. package/dist/types/src/repeaters.d.ts +16 -0
  107. package/dist/types/src/search.d.ts +33 -0
  108. package/dist/types/src/selling-plans.d.ts +64 -0
  109. package/dist/types/src/server.d.ts +88 -0
  110. package/dist/types/src/shopify-values.d.ts +46 -0
  111. package/dist/types/src/status.d.ts +33 -0
  112. package/dist/types/src/storage.d.ts +12 -0
  113. package/dist/types/src/store-html.d.ts +39 -0
  114. package/dist/types/src/storefront/catalog.d.ts +38 -0
  115. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  116. package/dist/types/src/variant-render.d.ts +14 -0
  117. package/package.json +24 -23
  118. package/src/account/addresses-view.ts +58 -0
  119. package/src/account/answers.ts +79 -0
  120. package/src/account/markup.ts +36 -0
  121. package/src/account/operations.ts +145 -0
  122. package/src/account/{order-lines.js → order-lines.ts} +20 -41
  123. package/src/account/order-list.ts +107 -0
  124. package/src/account/order-page.ts +137 -0
  125. package/src/account/page-answers.ts +317 -0
  126. package/src/account/{page-events.js → page-events.ts} +55 -90
  127. package/src/account/server-client.ts +183 -0
  128. package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
  129. package/src/account-api.ts +98 -0
  130. package/src/account-marks.ts +35 -0
  131. package/src/{account-pages.js → account-pages.ts} +29 -37
  132. package/src/account.ts +284 -0
  133. package/src/amounts.ts +64 -0
  134. package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
  135. package/src/analytics.ts +315 -0
  136. package/src/{api-version.js → api-version.ts} +25 -31
  137. package/src/{astro.js → astro.ts} +110 -104
  138. package/src/cart/cart-shape.ts +25 -0
  139. package/src/cart/checkout.ts +101 -0
  140. package/src/cart/{drawer.js → drawer.ts} +18 -31
  141. package/src/cart/mutations.ts +237 -0
  142. package/src/cart/{render.js → render.ts} +68 -96
  143. package/src/cart/store.ts +279 -0
  144. package/src/{cart.js → cart.ts} +82 -128
  145. package/src/client.ts +391 -0
  146. package/src/{collection.js → collection.ts} +30 -38
  147. package/src/config-sections.ts +284 -0
  148. package/src/config.ts +480 -0
  149. package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
  150. package/src/controls/{discount.js → discount.ts} +13 -34
  151. package/src/controls/{extras.js → extras.ts} +15 -24
  152. package/src/controls/{navigation.js → navigation.ts} +24 -30
  153. package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
  154. package/src/{diagnose.js → diagnose.ts} +95 -96
  155. package/src/dom.ts +268 -0
  156. package/src/{extras.js → extras.ts} +43 -52
  157. package/src/fragments.ts +261 -0
  158. package/src/globals.d.ts +15 -0
  159. package/src/{images.js → images.ts} +30 -43
  160. package/src/index.ts +415 -0
  161. package/src/initial.ts +101 -0
  162. package/src/lazy-iife.ts +197 -0
  163. package/src/lazy.ts +42 -0
  164. package/src/{loader.js → loader.ts} +149 -123
  165. package/src/market-codes.ts +6 -0
  166. package/src/{markets-ui.js → markets-ui.ts} +91 -68
  167. package/src/markets.ts +246 -0
  168. package/src/markup.ts +127 -0
  169. package/src/{mounting.js → mounting.ts} +88 -101
  170. package/src/{page-market.js → page-market.ts} +32 -38
  171. package/src/part-integrity.ts +7 -0
  172. package/src/parts/account-pages.ts +7 -0
  173. package/src/parts/account.ts +7 -0
  174. package/src/parts/analytics.ts +7 -0
  175. package/src/parts/diagnose.ts +7 -0
  176. package/src/parts/extras.ts +7 -0
  177. package/src/parts/markets.ts +7 -0
  178. package/src/parts/search.ts +7 -0
  179. package/src/presets/barba.ts +194 -0
  180. package/src/{product-events.js → product-events.ts} +33 -48
  181. package/src/{product-render.js → product-render.ts} +38 -77
  182. package/src/product-shape.ts +49 -0
  183. package/src/product-state.ts +318 -0
  184. package/src/{product.js → product.ts} +90 -126
  185. package/src/{repeaters.js → repeaters.ts} +40 -52
  186. package/src/{search.js → search.ts} +85 -103
  187. package/src/selling-plans.ts +145 -0
  188. package/src/server.ts +211 -0
  189. package/src/{shopify-values.js → shopify-values.ts} +24 -22
  190. package/src/{status.js → status.ts} +34 -54
  191. package/src/storage.ts +34 -0
  192. package/src/store-html.ts +359 -0
  193. package/src/storefront/catalog.ts +196 -0
  194. package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
  195. package/src/{variant-render.js → variant-render.ts} +24 -50
  196. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  197. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  198. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  199. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  200. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  201. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  202. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  203. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  204. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  205. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  206. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  207. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  208. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  209. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  210. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  211. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  212. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  213. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  214. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  215. package/src/account/addresses-view.js +0 -92
  216. package/src/account/browser-session.js +0 -359
  217. package/src/account/markup.js +0 -48
  218. package/src/account/operations.js +0 -180
  219. package/src/account/order-list.js +0 -91
  220. package/src/account/order-page.js +0 -192
  221. package/src/account/server-client.js +0 -172
  222. package/src/account/shared.js +0 -111
  223. package/src/account/shopify-auth.js +0 -150
  224. package/src/account-api.js +0 -118
  225. package/src/account-links.js +0 -35
  226. package/src/account.js +0 -342
  227. package/src/amounts.js +0 -36
  228. package/src/analytics.js +0 -352
  229. package/src/cart/checkout.js +0 -97
  230. package/src/cart/mutations.js +0 -234
  231. package/src/cart/store.js +0 -326
  232. package/src/client.js +0 -267
  233. package/src/config.js +0 -349
  234. package/src/dom.js +0 -287
  235. package/src/fragments.js +0 -237
  236. package/src/index.js +0 -353
  237. package/src/initial.js +0 -87
  238. package/src/lazy-iife.js +0 -116
  239. package/src/lazy.js +0 -39
  240. package/src/market-codes.js +0 -35
  241. package/src/markets.js +0 -334
  242. package/src/markup.js +0 -64
  243. package/src/parts/account-pages.js +0 -6
  244. package/src/parts/account.js +0 -6
  245. package/src/parts/analytics.js +0 -6
  246. package/src/parts/diagnose.js +0 -6
  247. package/src/parts/extras.js +0 -6
  248. package/src/parts/markets.js +0 -6
  249. package/src/parts/search.js +0 -6
  250. package/src/presets/barba.js +0 -190
  251. package/src/product-state.js +0 -318
  252. package/src/selling-plans.js +0 -154
  253. package/src/server.js +0 -215
  254. package/src/store-html.js +0 -0
  255. package/src/storefront/catalog.js +0 -199
  256. package/types/astro.d.ts +0 -28
  257. package/types/barba.d.ts +0 -16
  258. package/types/index.d.ts +0 -577
  259. package/types/server.d.ts +0 -182
package/dist/tether.js CHANGED
@@ -1,28 +1,231 @@
1
- "use strict";
2
1
  (() => {
3
2
  var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __export = (target, all) => {
5
5
  for (var name in all)
6
6
  __defProp(target, name, { get: all[name], enumerable: true });
7
7
  };
8
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
8
9
 
9
- // src/config.js
10
- var config_exports = {};
11
- __export(config_exports, {
12
- DEFAULTS: () => DEFAULTS,
13
- cfg: () => cfg,
14
- configure: () => configure,
15
- isConfigured: () => isConfigured,
16
- log: () => log,
17
- reportError: () => reportError,
18
- resolveConfig: () => resolveConfig,
19
- toMetafieldIdentifier: () => toMetafieldIdentifier,
20
- warn: () => warn
21
- });
10
+ // server/src/shared/api-versions.ts
11
+ var STOREFRONT_API_VERSION = "2026-07";
12
+
13
+ // src/api-version.ts
14
+ var QUARTERLY_VERSION = /^(\d{4})-(01|04|07|10)$/;
15
+ var AVERAGE_MONTH_MS = 30.44 * 24 * 3600 * 1e3;
16
+ function parseVersion(version) {
17
+ const match = QUARTERLY_VERSION.exec(typeof version === "string" ? version : "");
18
+ if (!match) return null;
19
+ return { year: Number(match[1]), month: Number(match[2]) };
20
+ }
21
+ var isQuarterlyVersion = (version) => parseVersion(version) !== null;
22
+
23
+ // server/src/shared/json.ts
24
+ function isRecord(value) {
25
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26
+ }
27
+ function field(value, key) {
28
+ return isRecord(value) ? value[key] : void 0;
29
+ }
30
+
31
+ // src/config-sections.ts
32
+ var METAFIELD_PART = /^[A-Za-z0-9_.-]+$/;
33
+ var METAFIELD_NAMESPACE = /^(\$app(:[A-Za-z0-9_-]+)?|[A-Za-z0-9_.-]+)$/;
34
+ var HOSTNAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
35
+ var COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
36
+ var GLOBAL_NAME = /^[A-Za-z_$][\w$]*$/;
37
+ var SITE_ORIGIN = "https://site.invalid";
38
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
39
+ var MARKET_SOURCES = ["saved", "server", "webflow"];
40
+ var CONSENT_MODES = [
41
+ "no-banner",
42
+ "default-banner",
43
+ "custom-banner"
44
+ ];
45
+ var isText = (value) => typeof value === "string" && value !== "";
46
+ var isFunction = (value) => typeof value === "function";
47
+ function domainOption(value, invalid) {
48
+ if (typeof value !== "string" || !value) return "";
49
+ const host = value.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
50
+ if (isHostname(host)) return host;
51
+ invalid("domain", value);
52
+ return "";
53
+ }
54
+ function isSiteOrHttpsUrl(value) {
55
+ if (!isText(value)) return false;
56
+ try {
57
+ const url2 = new URL(value, `${SITE_ORIGIN}/`);
58
+ if (url2.origin === SITE_ORIGIN) return true;
59
+ return url2.protocol === "https:" || url2.protocol === "http:" && LOOPBACK_HOSTS.has(url2.hostname);
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function partsUrlOption(value, invalid) {
65
+ if (value === void 0 || value === null || value === "") return null;
66
+ if (typeof value === "string" && (isSiteOrHttpsUrl(value) || isPageOrigin(value))) return value;
67
+ invalid("partsUrl", value);
68
+ return null;
69
+ }
70
+ function isPageOrigin(value) {
71
+ if (typeof location === "undefined") return false;
72
+ try {
73
+ return new URL(value).origin === location.origin;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ function toHttpsOrigin(value) {
79
+ if (typeof value !== "string") return null;
80
+ try {
81
+ const url2 = new URL(value);
82
+ return url2.protocol === "https:" && url2.href === new URL(url2.origin).href ? url2.origin : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ function embedOriginsOption(value, invalid) {
88
+ if (!Array.isArray(value)) {
89
+ invalid("embedOrigins", value);
90
+ return [];
91
+ }
92
+ const origins = [];
93
+ const entries = value;
94
+ for (const entry of entries) {
95
+ const origin = toHttpsOrigin(entry);
96
+ if (origin) origins.push(origin);
97
+ else invalid("embedOrigin", entry);
98
+ }
99
+ return origins;
100
+ }
101
+ 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 namespace !== "string" || typeof key !== "string") return null;
113
+ return METAFIELD_NAMESPACE.test(namespace) && METAFIELD_PART.test(key) ? { namespace, key } : null;
114
+ }
115
+ function metafieldsOption(value, invalid) {
116
+ if (!Array.isArray(value)) {
117
+ invalid("metafields", value);
118
+ return [];
119
+ }
120
+ const refs = [];
121
+ const entries = value;
122
+ for (const ref of entries) {
123
+ const identifier = toMetafieldIdentifier(ref);
124
+ if (identifier) refs.push(typeof ref === "string" ? ref : identifier);
125
+ else invalid("metafield", ref);
126
+ }
127
+ return refs;
128
+ }
129
+ var when = (check) => (value) => check(value) ? value : void 0;
130
+ var isHostname = (value) => isText(value) && HOSTNAME.test(value);
131
+ var isCookieName = (value) => isText(value) && COOKIE_NAME.test(value);
132
+ var isDataLayer = (value) => typeof value === "boolean" || isText(value) && GLOBAL_NAME.test(value);
133
+ var url = when(isSiteOrHttpsUrl);
134
+ var text = when(isText);
135
+ var flag = when((value) => typeof value === "boolean");
136
+ var hook = when(isFunction);
137
+ function sectionOption(name, value, schema, invalid) {
138
+ if (value === null || value === void 0) return null;
139
+ if (!isRecord(value)) {
140
+ invalid(name, value);
141
+ return null;
142
+ }
143
+ const section = {};
144
+ const readers = schema;
145
+ for (const [key, read] of Object.entries(readers)) {
146
+ const field2 = value[key];
147
+ if (field2 === void 0) continue;
148
+ const parsed = read(field2);
149
+ if (parsed === void 0) invalid(`${name}.${key}`, field2);
150
+ else section[key] = parsed;
151
+ }
152
+ return section;
153
+ }
154
+ var ACCOUNT = {
155
+ server: url,
156
+ orderUrl: url,
157
+ subscriptionsUrl: url,
158
+ paymentUrl: url,
159
+ logoutUrl: url,
160
+ locale: text,
161
+ navigate: hook
162
+ };
163
+ var ACCOUNT_NEEDS_SERVER = "customer accounts need the Worker since 0.7.0: set account.server";
164
+ var LOCALE_MARKET = { country: text, language: text, locale: text };
165
+ var MARKETS = {
166
+ // A single source name counts as a list of one.
167
+ detect: (value) => {
168
+ if (typeof value === "boolean") return value;
169
+ const names = Array.isArray(value) ? value : [value];
170
+ return typeof value === "string" || Array.isArray(value) ? MARKET_SOURCES.filter((source) => names.includes(source)) : void 0;
171
+ },
172
+ locales: (value) => {
173
+ if (!isRecord(value)) return void 0;
174
+ const locales = {};
175
+ for (const [locale, entry] of Object.entries(value)) {
176
+ const market = sectionOption(`markets.locales.${locale}`, entry, LOCALE_MARKET, () => void 0);
177
+ if (market) locales[locale] = market;
178
+ }
179
+ return locales;
180
+ },
181
+ cookie: when(isCookieName),
182
+ remember: flag,
183
+ strict: flag
184
+ };
185
+ var SHOPIFY_ANALYTICS = {
186
+ consent: (value) => CONSENT_MODES.find((mode) => mode === value),
187
+ proxy: (value) => value !== void 0 && Boolean(value)
188
+ };
189
+ var ANALYTICS = {
190
+ dataLayer: when(isDataLayer),
191
+ shopify: (value) => typeof value === "boolean" ? value : sectionOption("analytics.shopify", value, SHOPIFY_ANALYTICS, () => void 0) ?? void 0,
192
+ onEvent: hook
193
+ };
194
+ function accountOption(value, invalid, warn2) {
195
+ const account = sectionOption("account", value, ACCOUNT, invalid);
196
+ if (account === null) return null;
197
+ const { server } = account;
198
+ if (server === void 0) {
199
+ warn2(ACCOUNT_NEEDS_SERVER);
200
+ return null;
201
+ }
202
+ return { ...account, server };
203
+ }
204
+ var marketsOption = (value, invalid) => sectionOption("markets", value, MARKETS, invalid);
205
+ var analyticsOption = (value, invalid) => sectionOption("analytics", value, ANALYTICS, invalid);
206
+
207
+ // server/src/shared/market-codes.ts
208
+ var COUNTRY = /^[A-Z]{2}$/;
209
+ var NOT_A_COUNTRY = /* @__PURE__ */ new Set(["XX", "T1"]);
210
+ var REGIONAL_LANGUAGES = /* @__PURE__ */ new Set(["PT_BR", "PT_PT", "ZH_CN", "ZH_TW"]);
211
+ var LANGUAGE = /^[A-Z]{2,3}$/;
212
+ function toCountry(value) {
213
+ const code = (typeof value === "string" ? value : "").trim().toUpperCase();
214
+ if (!COUNTRY.test(code) || NOT_A_COUNTRY.has(code)) return null;
215
+ return code;
216
+ }
217
+ function toLanguage(value) {
218
+ const raw = (typeof value === "string" ? value : "").trim().replace("-", "_").toUpperCase();
219
+ if (REGIONAL_LANGUAGES.has(raw)) return raw;
220
+ const primary = raw.split("_")[0] ?? "";
221
+ return LANGUAGE.test(primary) ? primary : null;
222
+ }
223
+
224
+ // src/config.ts
22
225
  var DEFAULTS = {
23
226
  domain: "",
24
227
  token: "",
25
- apiVersion: "2026-07",
228
+ apiVersion: STOREFRONT_API_VERSION,
26
229
  country: void 0,
27
230
  language: void 0,
28
231
  locale: void 0,
@@ -31,6 +234,7 @@
31
234
  productPath: "/product",
32
235
  collectionPath: "/collection",
33
236
  metafields: [],
237
+ embedOrigins: [],
34
238
  imagesFirst: 25,
35
239
  variantsFirst: 100,
36
240
  sellingPlanGroupsFirst: 10,
@@ -66,9 +270,34 @@
66
270
  debug: false
67
271
  };
68
272
  var cfg = { ...DEFAULTS };
273
+ 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
+ });
287
+ }
288
+ var configView = readOnlyView(cfg);
69
289
  var PREFIX = /^[a-z][a-z0-9-]*$/;
70
- var METAFIELD_PART = /^[A-Za-z0-9_.-]+$/;
71
- var METAFIELD_NAMESPACE = /^(\$app(:[A-Za-z0-9_-]+)?|[A-Za-z0-9_.-]+)$/;
290
+ var SWITCH_OPTIONS = [
291
+ "revalidate",
292
+ "openCartOnAdd",
293
+ "cartFocus",
294
+ "variantUrl",
295
+ "autoMount",
296
+ "observe",
297
+ "astro",
298
+ "debug"
299
+ ];
300
+ var TEXT_OPTIONS = ["token", "productPath", "collectionPath", "cartDrawerOpenClass"];
72
301
  var COUNT_OPTIONS = [
73
302
  "imagesFirst",
74
303
  "variantsFirst",
@@ -77,7 +306,8 @@
77
306
  "collectionFirst",
78
307
  "retries",
79
308
  "maxAge",
80
- "lowStock"
309
+ "lowStock",
310
+ "searchDelay"
81
311
  ];
82
312
  var HOOK_OPTIONS = [
83
313
  "warningText",
@@ -94,65 +324,86 @@
94
324
  if (value !== void 0) warn(`invalid ${name} ${JSON.stringify(value)}: using ${JSON.stringify(fallback)}`);
95
325
  return fallback;
96
326
  }
327
+ function dropped(name, value) {
328
+ warn(`invalid ${name} ${JSON.stringify(value)}: ignored`);
329
+ }
330
+ function warnUnknownKeys(options) {
331
+ for (const key of Object.keys(options)) {
332
+ if (!(key in DEFAULTS)) warn(`unknown option ${JSON.stringify(key)}: ignored`);
333
+ }
334
+ }
335
+ function hookOption(value) {
336
+ return typeof value === "function" ? value : null;
337
+ }
338
+ function hookOptions(options) {
339
+ const hooks = Object.fromEntries(HOOK_OPTIONS.map((name) => [name, hookOption(options[name])]));
340
+ return hooks;
341
+ }
97
342
  function countOption(name, value) {
98
343
  if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
99
344
  return invalidOption(name, value, DEFAULTS[name]);
100
345
  }
101
- function choiceOption(name, value, choices) {
102
- return choices.find((choice) => choice === value) ?? invalidOption(name, value, choices[0]);
346
+ function switchOption(name, value) {
347
+ return typeof value === "boolean" ? value : invalidOption(name, value, DEFAULTS[name]);
103
348
  }
104
- function domainOption(value) {
105
- if (typeof value !== "string") return "";
106
- return value.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
349
+ function textOption(name, value) {
350
+ return typeof value === "string" ? value : invalidOption(name, value, DEFAULTS[name]);
107
351
  }
108
- function toMetafieldIdentifier(ref) {
109
- let namespace;
110
- let key;
111
- if (typeof ref === "string") {
112
- const dot = ref.indexOf(".");
113
- namespace = dot > 0 ? ref.slice(0, dot) : "";
114
- key = ref.slice(dot + 1);
115
- } else if (typeof ref === "object" && ref !== null) {
116
- namespace = Reflect.get(ref, "namespace");
117
- key = Reflect.get(ref, "key");
352
+ function localeOption(value) {
353
+ if (value === void 0 || value === null || value === "") return void 0;
354
+ try {
355
+ if (typeof value === "string" && Intl.getCanonicalLocales(value).length) return value;
356
+ } catch {
118
357
  }
119
- if (typeof namespace !== "string" || typeof key !== "string") return null;
120
- return METAFIELD_NAMESPACE.test(namespace) && METAFIELD_PART.test(key) ? { namespace, key } : null;
358
+ dropped("locale", value);
359
+ return void 0;
121
360
  }
122
- function metafieldsOption(value) {
123
- if (!Array.isArray(value)) return invalidOption("metafields", value, []);
124
- const refs = [];
125
- for (
126
- const ref of
127
- /** @type {unknown[]} */
128
- value
129
- ) {
130
- const identifier = toMetafieldIdentifier(ref);
131
- if (identifier) refs.push(typeof ref === "string" ? ref : identifier);
132
- else invalidOption("metafield", ref, null);
361
+ function choiceOption(name, value, choices) {
362
+ return choices.find((choice) => choice === value) ?? invalidOption(name, value, choices[0]);
363
+ }
364
+ function apiVersionOption(value) {
365
+ if (value === "unstable") {
366
+ warn('apiVersion "unstable" changes without notice. Pin a dated version (e.g. 2026-07).');
367
+ return value;
133
368
  }
134
- return refs;
369
+ return isQuarterlyVersion(value) ? value : invalidOption("apiVersion", value, DEFAULTS.apiVersion);
370
+ }
371
+ function codeOption(name, value, parse) {
372
+ if (value === void 0 || value === null || value === "") return void 0;
373
+ const code = parse(value);
374
+ if (code !== null) return code;
375
+ dropped(name, value);
376
+ return void 0;
135
377
  }
136
378
  function resolveConfig(options) {
137
- const domain = domainOption(options.domain);
379
+ warnUnknownKeys(options);
380
+ const domain = domainOption(options.domain, dropped);
138
381
  const prefix = typeof options.prefix === "string" && PREFIX.test(options.prefix) ? options.prefix : invalidOption("prefix", options.prefix, DEFAULTS.prefix);
139
382
  const resolved = {
140
383
  ...DEFAULTS,
141
- ...options,
384
+ ...hookOptions(options),
142
385
  domain,
386
+ apiVersion: apiVersionOption(options.apiVersion),
387
+ country: codeOption("country", options.country, toCountry),
388
+ locale: localeOption(options.locale),
389
+ partsUrl: partsUrlOption(options.partsUrl, dropped),
390
+ language: codeOption("language", options.language, toLanguage),
391
+ markets: marketsOption(options.markets, dropped),
392
+ analytics: analyticsOption(options.analytics, dropped),
393
+ account: accountOption(options.account, dropped, warn),
143
394
  prefix,
144
- storageKey: options.storageKey || `${prefix}:cart:${domain}`,
145
- metafields: metafieldsOption(options.metafields ?? []),
395
+ storageKey: typeof options.storageKey === "string" && options.storageKey ? options.storageKey : `${prefix}:cart:${domain}`,
396
+ metafields: metafieldsOption(options.metafields ?? [], dropped),
397
+ embedOrigins: embedOriginsOption(options.embedOrigins ?? [], dropped),
146
398
  checkoutTarget: choiceOption("checkoutTarget", options.checkoutTarget, ["_self", "_blank"]),
147
399
  defaultPurchase: choiceOption("defaultPurchase", options.defaultPurchase, ["one-time", "subscription"])
148
400
  };
149
401
  for (const name of COUNT_OPTIONS) resolved[name] = countOption(name, options[name]);
150
- for (const name of HOOK_OPTIONS) if (typeof resolved[name] !== "function") resolved[name] = null;
151
- if (resolved.apiVersion === "unstable") {
152
- warn('apiVersion "unstable" changes without notice. Pin a dated version (e.g. 2026-07).');
153
- }
402
+ for (const name of SWITCH_OPTIONS) resolved[name] = switchOption(name, options[name]);
403
+ for (const name of TEXT_OPTIONS) resolved[name] = textOption(name, options[name]);
154
404
  return resolved;
155
405
  }
406
+ var parseMarkets = (value) => marketsOption(value, dropped);
156
407
  function configure(options = {}) {
157
408
  const resolved = resolveConfig(options);
158
409
  for (const key of Object.keys(cfg)) {
@@ -177,83 +428,73 @@
177
428
  }
178
429
  }
179
430
 
180
- // src/client.js
181
- var client_exports = {};
182
- __export(client_exports, {
183
- TetherError: () => TetherError,
184
- endpoint: () => endpoint,
185
- gql: () => gql,
186
- request: () => request,
187
- unwrap: () => unwrap,
188
- withContext: () => withContext
189
- });
190
-
191
- // src/market-codes.js
192
- var market_codes_exports = {};
193
- __export(market_codes_exports, {
194
- toCountry: () => toCountry,
195
- toLanguage: () => toLanguage
196
- });
197
- var COUNTRY = /^[A-Z]{2}$/;
198
- var NOT_A_COUNTRY = /* @__PURE__ */ new Set(["XX", "T1"]);
199
- var REGIONAL_LANGUAGES = /* @__PURE__ */ new Set(["PT_BR", "PT_PT", "ZH_CN", "ZH_TW"]);
200
- var LANGUAGE = /^[A-Z]{2,3}$/;
201
- function toCountry(value) {
202
- const code = (typeof value === "string" ? value : "").trim().toUpperCase();
203
- if (!COUNTRY.test(code) || NOT_A_COUNTRY.has(code)) return null;
204
- return (
205
- /** @type {CountryCode} */
206
- code
207
- );
208
- }
209
- function toLanguage(value) {
210
- const raw = (typeof value === "string" ? value : "").trim().replace("-", "_").toUpperCase();
211
- if (REGIONAL_LANGUAGES.has(raw)) return (
212
- /** @type {LanguageCode} */
213
- raw
214
- );
215
- const primary = raw.split("_")[0] ?? "";
216
- return LANGUAGE.test(primary) ? (
217
- /** @type {LanguageCode} */
218
- primary
219
- ) : null;
431
+ // server/src/shared/error-message.ts
432
+ function messageOf(error) {
433
+ return error instanceof Error ? error.message : String(error);
220
434
  }
221
435
 
222
- // src/client.js
436
+ // src/client.ts
223
437
  var TetherError = class extends Error {
224
- /**
225
- * @param {string} message
226
- * @param {TetherErrorDetails} [details]
227
- */
228
438
  constructor(message, { status: status2, errors, userErrors, code } = {}) {
229
439
  super(message);
230
- this.name = "TetherError";
440
+ __publicField(this, "name", "TetherError");
441
+ __publicField(this, "code");
442
+ __publicField(this, "status");
443
+ __publicField(this, "errors");
444
+ __publicField(this, "userErrors");
231
445
  this.code = code ?? (status2 ? `HTTP_${status2}` : "ERROR");
232
446
  this.status = status2;
233
447
  this.errors = errors;
234
448
  this.userErrors = userErrors;
235
449
  }
236
450
  };
451
+ function matches(value, shape) {
452
+ if (typeof shape === "function") return shape(value);
453
+ if (typeof shape === "string") return shape === "object" ? isRecord(value) : typeof value === shape;
454
+ if (isShapeList(shape)) return Array.isArray(value) && value.every((item) => matches(item, shape[0]));
455
+ return isRecord(value) && Object.entries(shape).every(([key, inner]) => matches(value[key], inner));
456
+ }
457
+ var isShapeList = (shape) => Array.isArray(shape);
458
+ var orNull = (shape) => (value) => value === null || matches(value, shape);
459
+ var shaped = (shape) => (data) => matches(data, shape);
237
460
  function endpoint(conf = cfg) {
238
461
  return `https://${conf.domain}/api/${conf.apiVersion}/graphql.json`;
239
462
  }
240
- function withContext(query, ctx = cfg) {
463
+ var STRINGS_AND_COMMENTS = /"(?:\\.|[^"\\\n])*"|#[^\n]*/g;
464
+ var OPERATION_START = /^\s*(?:query|mutation)(?:\s+\w+)?\s*(?:\([^)]*\))?/;
465
+ function inContext(query2, { country, language }) {
241
466
  const args = [];
242
- const country = toCountry(ctx.country);
243
- const language = toLanguage(ctx.language);
244
467
  if (country) args.push(`country: ${country}`);
245
468
  if (language) args.push(`language: ${language}`);
246
- if (!args.length || query.includes("@inContext")) return query;
469
+ if (!args.length) return query2;
247
470
  const tag = `@inContext(${args.join(", ")})`;
248
- if (/^\s*\{/.test(query)) return `query ${tag} ${query.trimStart()}`;
249
- return query.replace(/^(\s*(?:query|mutation)(?:\s+\w+)?\s*(?:\([^)]*\))?)/, `$1 ${tag}`);
471
+ const blanked = query2.replace(STRINGS_AND_COMMENTS, (text2) => " ".repeat(text2.length));
472
+ const selectionStart = blanked.indexOf("{");
473
+ if (blanked.slice(0, selectionStart).includes("@inContext")) return query2;
474
+ if (blanked.trimStart().startsWith("{")) return `query ${tag} ${query2.slice(selectionStart)}`;
475
+ const header = OPERATION_START.exec(blanked);
476
+ if (!header) return query2;
477
+ const end = header[0].length;
478
+ return `${query2.slice(0, end).trimEnd()} ${tag} ${query2.slice(end).trimStart()}`;
479
+ }
480
+ function withContext(query2, ctx = cfg) {
481
+ const country = toCountry(ctx.country) ?? void 0;
482
+ const language = toLanguage(ctx.language) ?? void 0;
483
+ return inContext(query2, { country, language });
250
484
  }
251
485
  var RETRY_BACKOFF_MS = 300;
252
486
  var THROTTLE_BACKOFF_MS = 500;
253
487
  var SECURITY_REJECTION = 430;
488
+ var MS_PER_SECOND = 1e3;
254
489
  var INVENTORY_FIELD = "quantityAvailable";
255
490
  var storesWithoutInventory = /* @__PURE__ */ new Set();
256
491
  var storeOf = (conf) => `${conf.domain} ${conf.apiVersion} ${conf.token}`;
492
+ function queryText(conf, query2, access, context) {
493
+ const written = typeof query2 === "string" ? query2 : query2(access);
494
+ const text2 = context ? inContext(written, conf) : written;
495
+ if (conf.maxAge <= 0 || /^\s*mutation\b/.test(text2)) return text2;
496
+ return `${text2} #${Math.floor(Date.now() / (conf.maxAge * MS_PER_SECOND))}`;
497
+ }
257
498
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
258
499
  function statusText(status2, conf) {
259
500
  if (status2 === 401 || status2 === 403) {
@@ -265,23 +506,40 @@
265
506
  }
266
507
  return `HTTP ${status2} from ${conf.domain}`;
267
508
  }
509
+ function toGraphQLError(value) {
510
+ const message = field(value, "message");
511
+ const code = field(field(value, "extensions"), "code");
512
+ const text2 = typeof message === "string" ? message : "";
513
+ return typeof code === "string" ? { message: text2, extensions: { code } } : { message: text2 };
514
+ }
515
+ function toGraphQLResponse(json) {
516
+ if (!isRecord(json)) return null;
517
+ const { data, errors } = json;
518
+ if (errors === void 0 || errors === null) return { data };
519
+ if (!Array.isArray(errors)) return null;
520
+ return { data, errors: errors.map(toGraphQLError) };
521
+ }
522
+ function invalidResponse(res, conf) {
523
+ const text2 = res.ok ? `Invalid response (${res.status})` : statusText(res.status, conf);
524
+ return new TetherError(text2, { status: res.status });
525
+ }
268
526
  function graphQLFailure(res, errors, conf) {
269
527
  const message = errors.map((error) => error.message).filter(Boolean).join(" | ");
270
- let text = message || "Storefront API error";
271
- if (!res.ok) text = res.status === 401 || res.status === 403 || !message ? statusText(res.status, conf) : message;
272
- return new TetherError(text, { status: res.status, errors });
528
+ let text2 = message || "Storefront API error";
529
+ if (!res.ok) text2 = res.status === 401 || res.status === 403 || !message ? statusText(res.status, conf) : message;
530
+ return new TetherError(text2, { status: res.status, errors });
273
531
  }
274
- async function attempt(conf, query, variables, canRetry) {
532
+ async function attempt(conf, query2, variables, isData, canRetry) {
275
533
  let res;
276
534
  try {
277
535
  res = await (conf.fetch ?? fetch)(endpoint(conf), {
278
536
  method: "POST",
279
537
  headers: { "Content-Type": "application/json", "X-Shopify-Storefront-Access-Token": conf.token },
280
- body: JSON.stringify({ query, variables })
538
+ body: JSON.stringify({ query: query2, variables })
281
539
  });
282
540
  } catch (error) {
283
541
  if (canRetry) return { kind: "retry", backoffMs: RETRY_BACKOFF_MS };
284
- throw new TetherError(`Network error: ${error instanceof Error ? error.message : String(error)}`, {
542
+ throw new TetherError(`Network error: ${messageOf(error)}`, {
285
543
  code: "NETWORK"
286
544
  });
287
545
  }
@@ -295,16 +553,14 @@
295
553
  try {
296
554
  json = await res.json();
297
555
  } catch {
298
- const text = res.ok ? `Invalid response (${res.status})` : statusText(res.status, conf);
299
- throw new TetherError(text, { status: res.status });
556
+ throw invalidResponse(res, conf);
300
557
  }
301
- const { data, errors = [] } = (
302
- /** @type {GraphQLResponse | null} */
303
- json ?? {}
304
- );
558
+ const body = toGraphQLResponse(json);
559
+ if (!body) throw invalidResponse(res, conf);
560
+ const { data, errors = [] } = body;
305
561
  if (errors.length) {
306
- if (query.includes(INVENTORY_FIELD) && errors.some((error) => error.message.includes(INVENTORY_FIELD))) {
307
- return { kind: "inventory-refused" };
562
+ if (query2.includes(INVENTORY_FIELD) && errors.some((error) => error.message.includes(INVENTORY_FIELD))) {
563
+ return { kind: "inventory-refused", error: graphQLFailure(res, errors, conf) };
308
564
  }
309
565
  if (canRetry && errors.some((error) => error.extensions?.code === "THROTTLED")) {
310
566
  return { kind: "retry", backoffMs: THROTTLE_BACKOFF_MS };
@@ -312,28 +568,23 @@
312
568
  throw graphQLFailure(res, errors, conf);
313
569
  }
314
570
  if (!res.ok) throw new TetherError(statusText(res.status, conf), { status: res.status });
571
+ if (!isData(data)) throw invalidResponse(res, conf);
315
572
  return { kind: "data", data };
316
573
  }
317
- async function request(conf, query, variables = {}, { context = true } = {}) {
574
+ async function request(conf, query2, variables, isData, { context = true } = {}) {
318
575
  if (!conf.domain || !conf.token) {
319
576
  throw new TetherError("Tether is not configured: domain and token are required", { code: "NOT_CONFIGURED" });
320
577
  }
321
- let text = context ? withContext(query, conf) : query;
322
- if (conf.maxAge > 0 && !/^\s*mutation\b/.test(text)) {
323
- text += ` #${Math.floor(Date.now() / (conf.maxAge * 1e3))}`;
324
- }
325
578
  const store = storeOf(conf);
326
579
  let retries = 0;
327
580
  for (; ; ) {
328
- const sent = storesWithoutInventory.has(store) ? text.replaceAll(INVENTORY_FIELD, "") : text;
329
- const outcome = await attempt(conf, sent, variables, retries < conf.retries);
581
+ const sent = queryText(conf, query2, { inventory: !storesWithoutInventory.has(store) }, context);
582
+ const outcome = await attempt(conf, sent, variables, isData, retries < conf.retries);
330
583
  switch (outcome.kind) {
331
584
  case "data":
332
- return (
333
- /** @type {T} */
334
- outcome.data
335
- );
585
+ return outcome.data;
336
586
  case "inventory-refused":
587
+ if (typeof query2 === "string") throw outcome.error;
337
588
  log(`the token can't read inventory: ${INVENTORY_FIELD} is left out from now on`);
338
589
  storesWithoutInventory.add(store);
339
590
  break;
@@ -345,7 +596,13 @@
345
596
  }
346
597
  }
347
598
  }
348
- var gql = (query, variables, options) => request(cfg, query, variables, options);
599
+ function gql(query2, variables, isData, options) {
600
+ return request(cfg, query2, variables, isData, options);
601
+ }
602
+ var hasData = (data) => typeof data === "object" && data !== null;
603
+ function query(text2, variables = {}, options) {
604
+ return gql(text2, variables, hasData, options);
605
+ }
349
606
  function unwrap(payload, name) {
350
607
  if (!payload) throw new TetherError(`${name}: empty response`);
351
608
  const userErrors = payload.userErrors ?? [];
@@ -355,133 +612,237 @@
355
612
  return payload;
356
613
  }
357
614
 
358
- // src/cart.js
359
- var cart_exports = {};
360
- __export(cart_exports, {
361
- Cart: () => Cart,
362
- trapFocus: () => trapFocus
363
- });
364
-
365
- // src/dom.js
366
- var dom_exports = {};
367
- __export(dom_exports, {
368
- attr: () => attr,
369
- busy: () => busy,
370
- cdnUrl: () => cdnUrl,
371
- emit: () => emit,
372
- formatDate: () => formatDate,
373
- formatMoney: () => formatMoney2,
374
- formatPercent: () => formatPercent,
375
- get: () => get,
376
- has: () => has,
377
- hasTemplate: () => hasTemplate,
378
- imageUrl: () => imageUrl,
379
- notifyLayoutChanged: () => notifyLayoutChanged,
380
- parseJSONAttr: () => parseJSONAttr,
381
- productUrl: () => productUrl2,
382
- qsa: () => qsa,
383
- repeat: () => repeat,
384
- repeatKeyed: () => repeatKeyed,
385
- sameImage: () => sameImage,
386
- scoped: () => scoped,
387
- sel: () => sel,
388
- setImage: () => setImage,
389
- setSelected: () => setSelected,
390
- setStoreHtml: () => setStoreHtml,
391
- setText: () => setText,
392
- setTextTracked: () => setTextTracked,
393
- show: () => show,
394
- toAttributes: () => toAttributes,
395
- watchImages: () => watchImages
396
- });
397
-
398
- // src/markup.js
615
+ // src/markup.ts
399
616
  var attr = (name) => `${cfg.prefix}-${name}`;
400
617
  var sel = (name, value) => value === void 0 ? `[${attr(name)}]` : `[${attr(name)}="${value}"]`;
401
618
  var get = (el, name) => el.getAttribute(attr(name));
402
619
  var has = (el, name) => el.hasAttribute(attr(name));
403
620
  function qsa(root, selector) {
404
621
  if (!root) return [];
405
- return [.../** @type {NodeListOf<HTMLElement>} */
406
- root.querySelectorAll(selector)];
622
+ return [...root.querySelectorAll(selector)];
407
623
  }
408
624
  var scoped = (root, selector, scopeSelector) => qsa(root, selector).filter((el) => el.closest(scopeSelector) === root);
625
+ var ANALYTICS_EVENTS = [
626
+ "product-ready",
627
+ "list-ready",
628
+ "cart-updated",
629
+ "cart-toggled",
630
+ "checkout",
631
+ "search"
632
+ ];
633
+ var ownEvents = /* @__PURE__ */ new WeakMap();
634
+ function ownEvent(name, detail) {
635
+ const event = new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true });
636
+ ownEvents.set(event, name);
637
+ return event;
638
+ }
639
+ function isOwnEvent(event, name) {
640
+ return ownEvents.get(event) === name;
641
+ }
409
642
  function emit(name, detail, target = document) {
410
- target.dispatchEvent(new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true }));
643
+ target.dispatchEvent(ownEvent(name, detail));
411
644
  }
412
645
 
413
- // src/store-html.js
414
- var store_html_exports = {};
415
- __export(store_html_exports, {
416
- storeHtmlFragment: () => storeHtmlFragment
417
- });
418
- var BLOCKED_ELEMENTS = /* @__PURE__ */ new Set([
419
- "SCRIPT",
420
- "OBJECT",
421
- "EMBED",
422
- "BASE",
423
- "META",
424
- "LINK",
425
- "NOSCRIPT",
426
- "TEMPLATE",
427
- // SVG animation can set an attribute such as href to a script URL after the attributes were checked.
428
- "ANIMATE",
429
- "SET"
646
+ // src/store-html.ts
647
+ var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
648
+ var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
649
+ ...["h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "div", "br", "hr", "blockquote", "code", "pre"],
650
+ ...["strong", "em", "b", "i", "u", "s", "small", "sub", "sup"],
651
+ ...["ul", "ol", "li", "dl", "dt", "dd"],
652
+ ...["table", "caption", "colgroup", "col", "thead", "tbody", "tfoot", "tr", "th", "td"],
653
+ ...["a", "img", "picture", "source", "video", "figure", "figcaption", "iframe"]
430
654
  ]);
431
- var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "data", "background"]);
432
- var SCRIPT_URL = /^\s*(javascript|vbscript|data):/i;
433
- var DATA_IMAGE = /^\s*data:image\/(png|gif|jpeg|webp|avif);/i;
434
- var IGNORED_IN_SCHEME = /[- ]/g;
435
- function isUnsafeAttribute(element, attribute) {
436
- const name = attribute.name.toLowerCase();
437
- if (name.startsWith("on") || name === "srcdoc") return true;
438
- const url = attribute.value.replace(IGNORED_IN_SCHEME, "");
439
- if (!URL_ATTRIBUTES.has(name) || !SCRIPT_URL.test(url)) return false;
440
- return !(element.tagName === "IMG" && name === "src" && DATA_IMAGE.test(url));
441
- }
442
- function clean(root) {
443
- for (const element of [...root.querySelectorAll("*")]) {
444
- if (BLOCKED_ELEMENTS.has(element.tagName.toUpperCase())) {
445
- element.remove();
446
- continue;
447
- }
448
- for (const attribute of [...element.attributes]) {
449
- if (isUnsafeAttribute(element, attribute)) element.removeAttribute(attribute.name);
450
- }
655
+ var REMOVED_WITH_CONTENT = /* @__PURE__ */ new Set(["script", "style", "template", "noscript", "textarea", "select", "title"]);
656
+ var GLOBAL_ATTRIBUTES = /* @__PURE__ */ new Set(["style", "title", "lang", "dir", "role", "align", "width", "height"]);
657
+ var ELEMENT_ATTRIBUTES = /* @__PURE__ */ new Map([
658
+ ["a", /* @__PURE__ */ new Set(["href", "target", "rel", "hreflang"])],
659
+ ["img", /* @__PURE__ */ new Set(["src", "srcset", "sizes", "alt", "loading", "decoding"])],
660
+ ["source", /* @__PURE__ */ new Set(["src", "srcset", "sizes", "type", "media"])],
661
+ ["video", /* @__PURE__ */ new Set(["src", "poster", "controls", "autoplay", "muted", "loop", "playsinline", "preload"])],
662
+ ["iframe", /* @__PURE__ */ new Set(["src", "allow", "allowfullscreen", "frameborder", "loading"])],
663
+ ["blockquote", /* @__PURE__ */ new Set(["cite"])],
664
+ ["ol", /* @__PURE__ */ new Set(["start", "type", "reversed"])],
665
+ ["li", /* @__PURE__ */ new Set(["value"])],
666
+ ["table", /* @__PURE__ */ new Set(["border", "cellpadding", "cellspacing", "summary"])],
667
+ ["col", /* @__PURE__ */ new Set(["span"])],
668
+ ["colgroup", /* @__PURE__ */ new Set(["span"])],
669
+ ["th", /* @__PURE__ */ new Set(["colspan", "rowspan", "headers", "scope", "valign"])],
670
+ ["td", /* @__PURE__ */ new Set(["colspan", "rowspan", "headers", "valign"])]
671
+ ]);
672
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "poster", "cite"]);
673
+ var SCRIPT_URL = /^(javascript|vbscript|data):/i;
674
+ var DATA_IMAGE = /^data:image\/(png|gif|jpeg|webp|avif);/i;
675
+ var OUT_OF_PLACE_PROPERTIES = /* @__PURE__ */ new Set([
676
+ "position",
677
+ "top",
678
+ "right",
679
+ "bottom",
680
+ "left",
681
+ "z-index",
682
+ "-webkit-box-reflect"
683
+ ]);
684
+ var OUT_OF_PLACE_PREFIXES = ["inset", "transform", "translate", "rotate", "scale", "offset"];
685
+ var VIEWPORT_LENGTH = /\d(?:[sld]?v|cq)(?:w|h|i|b|min|max)\b/i;
686
+ var PAINTS_BEYOND = /^(?:outline|box-shadow|text-shadow|filter|text-decoration|text-underline-offset|text-stroke)|^(?:border-image|mask-border|mask-box-image)(?:-outset)?$/;
687
+ var MAX_PAINT_PX = 40;
688
+ var NEGATIVE_LIMITS = /* @__PURE__ */ new Map([
689
+ ["letter-spacing", { px: 4, em: 0.25 }],
690
+ ["word-spacing", { px: 4, em: 0.25 }],
691
+ ["text-indent", { px: MAX_PAINT_PX }]
692
+ ]);
693
+ var NUMBER = /(?<![\w#.-])[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?(%|[a-z]+)?/gi;
694
+ var NON_LENGTH_FUNCTION = /(?<![\w-])(?!drop-shadow\(|blur\()[\w-]+\([^()]*\)/g;
695
+ var COMPUTED_VALUE = /(?:var|calc|min|max|clamp|env|attr)\(/i;
696
+ var FETCHING_VALUE = /url\(|expression\(|@import/i;
697
+ var IGNORED_IN_SCHEME = /[\u0000- ]/g;
698
+ var IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-popups allow-presentation";
699
+ var IFRAME_FEATURES = /* @__PURE__ */ new Set([
700
+ ...["autoplay", "encrypted-media", "picture-in-picture", "fullscreen", "clipboard-write"],
701
+ ...["accelerometer", "gyroscope", "web-share"]
702
+ ]);
703
+ var IFRAME_REFERRER_POLICY = "strict-origin-when-cross-origin";
704
+ var EMBED_ORIGINS = [
705
+ "https://www.youtube.com",
706
+ "https://www.youtube-nocookie.com",
707
+ "https://player.vimeo.com",
708
+ "https://www.loom.com",
709
+ "https://fast.wistia.net"
710
+ ];
711
+ function isAllowedAttribute(tag, name) {
712
+ return GLOBAL_ATTRIBUTES.has(name) || name.startsWith("aria-") || (ELEMENT_ATTRIBUTES.get(tag)?.has(name) ?? false);
713
+ }
714
+ function isSafeUrl(tag, name, value) {
715
+ const url2 = value.replace(IGNORED_IN_SCHEME, "");
716
+ if (!SCRIPT_URL.test(url2)) return true;
717
+ return tag === "img" && name === "src" && DATA_IMAGE.test(url2);
718
+ }
719
+ function urlOrNull(value, base) {
720
+ try {
721
+ return new URL(value, base);
722
+ } catch {
723
+ return null;
724
+ }
725
+ }
726
+ function pageOf(document2, styleParser, embedOrigins) {
727
+ const baseUrl = document2.baseURI;
728
+ const origin = urlOrNull(baseUrl)?.origin ?? "null";
729
+ return {
730
+ origin: origin === "null" ? null : origin,
731
+ baseUrl,
732
+ styleParser,
733
+ embedOrigins: /* @__PURE__ */ new Set([...EMBED_ORIGINS, ...embedOrigins])
734
+ };
735
+ }
736
+ function isAllowedEmbed(value, page) {
737
+ const url2 = urlOrNull(value.trim(), page.baseUrl);
738
+ return url2 !== null && url2.origin !== page.origin && page.embedOrigins.has(url2.origin);
739
+ }
740
+ function allowedFeatures(value) {
741
+ const features = value.split(";").map((directive) => directive.trim().split(/\s+/)[0]?.toLowerCase() ?? "");
742
+ return [...new Set(features.filter((feature) => IFRAME_FEATURES.has(feature)))].join("; ");
743
+ }
744
+ function isOutOfPlace(property, value) {
745
+ if (OUT_OF_PLACE_PROPERTIES.has(property) || OUT_OF_PLACE_PREFIXES.some((prefix) => property.startsWith(prefix))) {
746
+ return true;
451
747
  }
748
+ if (VIEWPORT_LENGTH.test(value)) return true;
749
+ const limits = NEGATIVE_LIMITS.get(property);
750
+ if (limits) return COMPUTED_VALUE.test(value) || movesBackTooFar(value, limits);
751
+ return property.startsWith("margin") && value.includes("-");
752
+ }
753
+ function movesBackTooFar(value, limits) {
754
+ return [...value.matchAll(NUMBER)].some(([number = "", unit = ""]) => {
755
+ const amount = parseFloat(number);
756
+ if (amount >= 0) return false;
757
+ const limit = limits[unit.toLowerCase()];
758
+ return limit === void 0 || -amount > limit;
759
+ });
452
760
  }
453
- function storeHtmlFragment(html, document2 = globalThis.document) {
761
+ function paintsTooFar(property, value) {
762
+ if (!PAINTS_BEYOND.test(property.replace(/^-webkit-/, ""))) return false;
763
+ if (COMPUTED_VALUE.test(value)) return true;
764
+ return [...value.replace(NON_LENGTH_FUNCTION, "").matchAll(NUMBER)].some(([number = "", unit = ""]) => {
765
+ const amount = Math.abs(parseFloat(number));
766
+ return amount !== 0 && (unit.toLowerCase() !== "px" || amount > MAX_PAINT_PX);
767
+ });
768
+ }
769
+ function cleanStyle(value, styleParser) {
770
+ const style = styleParser.style;
771
+ style.cssText = value;
772
+ const properties = Array.from({ length: style.length }, (_, index) => style.item(index));
773
+ for (const property of properties) {
774
+ const value2 = style.getPropertyValue(property);
775
+ const remove = isOutOfPlace(property, value2) || paintsTooFar(property, value2) || FETCHING_VALUE.test(value2);
776
+ if (remove) style.removeProperty(property);
777
+ }
778
+ const cleaned = style.cssText;
779
+ styleParser.removeAttribute("style");
780
+ return cleaned;
781
+ }
782
+ function cleanAttributes(element, page) {
783
+ const tag = element.localName;
784
+ for (const attribute of [...element.attributes]) {
785
+ const name = attribute.name.toLowerCase();
786
+ const isUrl = URL_ATTRIBUTES.has(name);
787
+ const keep = isAllowedAttribute(tag, name) && (!isUrl || isSafeUrl(tag, name, attribute.value));
788
+ if (!keep) element.removeAttribute(attribute.name);
789
+ }
790
+ const style = element.getAttribute("style");
791
+ if (style === null) return;
792
+ const cleaned = cleanStyle(style, page.styleParser);
793
+ if (cleaned === "") element.removeAttribute("style");
794
+ else element.setAttribute("style", cleaned);
795
+ }
796
+ function hardenEmbedsAndLinks(element) {
797
+ if (element.localName === "iframe") {
798
+ const allow = allowedFeatures(element.getAttribute("allow") ?? "");
799
+ if (allow === "") element.removeAttribute("allow");
800
+ else element.setAttribute("allow", allow);
801
+ element.setAttribute("sandbox", IFRAME_SANDBOX);
802
+ element.setAttribute("referrerpolicy", IFRAME_REFERRER_POLICY);
803
+ return;
804
+ }
805
+ if (element.localName !== "a" || element.getAttribute("target")?.toLowerCase() !== "_blank") return;
806
+ const rel = new Set((element.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean));
807
+ rel.add("noopener");
808
+ rel.add("noreferrer");
809
+ element.setAttribute("rel", [...rel].join(" "));
810
+ }
811
+ function cleanElement(element, page) {
812
+ const tag = element.localName;
813
+ const isHtml = element.namespaceURI === HTML_NAMESPACE;
814
+ if (isHtml && REMOVED_WITH_CONTENT.has(tag)) {
815
+ element.remove();
816
+ return;
817
+ }
818
+ if (!isHtml || !ALLOWED_ELEMENTS.has(tag)) {
819
+ element.replaceWith(...element.childNodes);
820
+ return;
821
+ }
822
+ if (tag === "iframe" && !isAllowedEmbed(element.getAttribute("src") ?? "", page)) {
823
+ element.remove();
824
+ return;
825
+ }
826
+ cleanAttributes(element, page);
827
+ hardenEmbedsAndLinks(element);
828
+ }
829
+ function storeHtmlFragment(html, document2 = globalThis.document, embedOrigins = []) {
454
830
  const template = document2.createElement("template");
455
831
  template.innerHTML = html ?? "";
456
- clean(template.content);
832
+ const page = pageOf(document2, template, embedOrigins);
833
+ for (const element of [...template.content.querySelectorAll("*")]) cleanElement(element, page);
457
834
  return template.content;
458
835
  }
459
836
 
460
- // src/shopify-values.js
461
- var shopify_values_exports = {};
462
- __export(shopify_values_exports, {
463
- STORE_ENV_NAMES: () => STORE_ENV_NAMES,
464
- formatMoney: () => formatMoney,
465
- isHttpsUrl: () => isHttpsUrl,
466
- isSecretShopifyToken: () => isSecretShopifyToken,
467
- productKey: () => productKey,
468
- productUrl: () => productUrl
469
- });
470
- var STORE_ENV_NAMES = {
471
- domain: ["PUBLIC_SHOPIFY_DOMAIN", "PUBLIC_SHOPIFY_STORE_DOMAIN", "SHOPIFY_DOMAIN"],
472
- token: ["PUBLIC_SHOPIFY_STOREFRONT_TOKEN", "PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN", "SHOPIFY_STOREFRONT_TOKEN"]
473
- };
474
- function isHttpsUrl(url) {
475
- if (typeof url !== "string") return false;
837
+ // src/shopify-values.ts
838
+ function isHttpsUrl(url2) {
839
+ if (typeof url2 !== "string") return false;
476
840
  try {
477
- return new URL(url).protocol === "https:";
841
+ return new URL(url2).protocol === "https:";
478
842
  } catch {
479
843
  return false;
480
844
  }
481
845
  }
482
- function isSecretShopifyToken(token) {
483
- return /^shp[a-z]{2}_/.test(token.trim());
484
- }
485
846
  function formatMoney(money, locale) {
486
847
  if (!money) return "";
487
848
  const amount = parseFloat(money.amount);
@@ -495,8 +856,12 @@
495
856
  }
496
857
  }
497
858
  function productUrl(productPath, handle, id) {
498
- if (!/\{(handle|id)\}/.test(productPath)) return `${productPath.replace(/\/$/, "")}/${handle ?? String(handle)}`;
499
- const numericId2 = id ? id.split("/").pop() ?? "" : "";
859
+ if (!/\{(handle|id)\}/.test(productPath)) {
860
+ return handle ? `${productPath.replace(/\/$/, "")}/${encodeURIComponent(handle)}` : null;
861
+ }
862
+ const numericId2 = id ? id.split("/").at(-1) ?? "" : "";
863
+ if (productPath.includes("{handle}") && !handle) return null;
864
+ if (productPath.includes("{id}") && !numericId2) return null;
500
865
  return productPath.replace(/\{handle\}/g, encodeURIComponent(handle ?? "")).replace(/\{id\}/g, encodeURIComponent(numericId2));
501
866
  }
502
867
  function productKey(raw) {
@@ -507,43 +872,44 @@
507
872
  return /\/products\/([^/?#]+)/.exec(value)?.[1] ?? value;
508
873
  }
509
874
 
510
- // src/images.js
511
- function imageUrl(url, { width, height, crop } = {}) {
512
- if (!url) return url;
875
+ // src/images.ts
876
+ function imageUrl(url2, { width, height, crop } = {}) {
877
+ if (!url2) return url2;
513
878
  let parsed;
514
879
  try {
515
- parsed = new URL(url);
880
+ parsed = new URL(url2);
516
881
  } catch (error) {
517
- if (error instanceof TypeError) return url;
882
+ if (error instanceof TypeError) return url2;
518
883
  throw error;
519
884
  }
520
885
  if (width) parsed.searchParams.set("width", String(width));
521
886
  if (height) parsed.searchParams.set("height", String(height));
522
887
  if (crop) parsed.searchParams.set("crop", crop);
523
- return (
524
- /** @type {Url} */
525
- parsed.toString()
526
- );
888
+ return parsed.toString();
527
889
  }
528
890
  function sizeAttribute(el, name) {
529
891
  return el.getAttribute(name) ?? el.getAttribute(attr(name)) ?? el.getAttribute(`data-${name}`);
530
892
  }
531
- function cdnUrl(url, el) {
532
- if (!url || !el) return url;
893
+ function cdnUrl(url2, el) {
894
+ if (!url2 || !el) return url2;
533
895
  const width = sizeAttribute(el, "max-width");
534
896
  const height = sizeAttribute(el, "max-height");
535
- return imageUrl(url, { width, height, crop: width || height ? sizeAttribute(el, "crop") : null });
897
+ return imageUrl(url2, { width, height, crop: width || height ? sizeAttribute(el, "crop") : null });
536
898
  }
537
- var sameImage = (a, b) => Boolean(a && b) && a?.split("?")[0] === b?.split("?")[0];
538
- function cssUrl(url) {
539
- const hasScheme = /^\s*[a-z][a-z0-9+.-]*:/i.test(url);
540
- if (hasScheme && !/^\s*(https?:|data:image\/)/i.test(url)) return "none";
541
- const escaped = url.replace(/["\\\n\r\f]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
899
+ var withoutQuery = (url2) => url2.replace(/\?.*$/, "");
900
+ function sameImage(a, b) {
901
+ if (!a || !b) return false;
902
+ return withoutQuery(a) === withoutQuery(b);
903
+ }
904
+ function cssUrl(url2) {
905
+ const hasScheme = /^\s*[a-z][a-z0-9+.-]*:/i.test(url2);
906
+ if (hasScheme && !/^\s*(https?:|data:image\/)/i.test(url2)) return "none";
907
+ const escaped = url2.replace(/["\\\n\r\f]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
542
908
  return `url("${escaped}")`;
543
909
  }
544
- function setImage(el, url, alt) {
545
- if (!url) return;
546
- const src = cdnUrl(url, el);
910
+ function setImage(el, url2, alt) {
911
+ if (!url2) return;
912
+ const src = cdnUrl(url2, el);
547
913
  if (el.tagName !== "IMG") {
548
914
  el.style.backgroundImage = cssUrl(src);
549
915
  return;
@@ -559,21 +925,15 @@
559
925
  if (!el.getAttribute("loading")) el.setAttribute("loading", "lazy");
560
926
  }
561
927
 
562
- // src/repeaters.js
928
+ // src/repeaters.ts
563
929
  var templates = /* @__PURE__ */ new WeakMap();
564
930
  var hasTemplate = (container) => Boolean(templates.get(container));
565
- var copyOf = (template) => (
566
- /** @type {HTMLElement} */
567
- template.cloneNode(true)
568
- );
931
+ var copyOf = (template) => template.cloneNode(true);
569
932
  function templateOf(container) {
570
933
  const known = templates.get(container);
571
934
  if (known !== void 0) return known;
572
935
  const first = container.firstElementChild;
573
- const template = first ? copyOf(
574
- /** @type {HTMLElement} */
575
- first
576
- ) : null;
936
+ const template = first ? copyOf(first) : null;
577
937
  templates.set(container, template);
578
938
  container.replaceChildren();
579
939
  return template;
@@ -628,8 +988,7 @@
628
988
  }
629
989
  const keyAttribute = `data-${cfg.prefix}-key`;
630
990
  const leavingAttribute = `data-${cfg.prefix}-leaving`;
631
- const live = () => [.../** @type {HTMLCollectionOf<HTMLElement>} */
632
- container.children].filter(
991
+ const live = () => [...container.children].filter(
633
992
  (node) => node.hasAttribute(keyAttribute) && !node.hasAttribute(leavingAttribute)
634
993
  );
635
994
  const existing = new Map(live().map((node) => [node.getAttribute(keyAttribute), node]));
@@ -667,7 +1026,7 @@
667
1026
  });
668
1027
  }
669
1028
 
670
- // src/dom.js
1029
+ // src/dom.ts
671
1030
  var SEEN = "data-tether-seen";
672
1031
  var SHOWN = "data-tether-shown";
673
1032
  var PREVIOUS_DISPLAY = "data-tether-prev-display";
@@ -698,14 +1057,14 @@
698
1057
  return formatOr(() => new Date(iso).toLocaleDateString(cfg.locale || void 0), "");
699
1058
  }
700
1059
  function setStoreHtml(el, html) {
701
- el.replaceChildren(storeHtmlFragment(html, el.ownerDocument));
1060
+ el.replaceChildren(storeHtmlFragment(html, el.ownerDocument, cfg.embedOrigins));
702
1061
  }
703
1062
  var isTextField = (el) => el.tagName === "INPUT" || el.tagName === "TEXTAREA";
704
1063
  var isInput = (el) => el.tagName === "INPUT";
705
1064
  function setText(el, value) {
706
- const text = value == null ? "" : String(value);
707
- if (isTextField(el)) el.value = text;
708
- else el.textContent = text;
1065
+ const text2 = value == null ? "" : String(value);
1066
+ if (isTextField(el)) el.value = text2;
1067
+ else el.textContent = text2;
709
1068
  }
710
1069
  var readText = (el) => isTextField(el) ? el.value : el.textContent;
711
1070
  function setTextTracked(el, value) {
@@ -754,15 +1113,22 @@
754
1113
  if (input && isInput(input)) input.checked = selected;
755
1114
  }
756
1115
  var layoutTimer;
1116
+ var pageGlobal = (name) => Reflect.get(window, name);
1117
+ function refreshScrollTrigger() {
1118
+ const scrollTrigger = pageGlobal("ScrollTrigger");
1119
+ if (typeof scrollTrigger !== "function") return;
1120
+ const refresh = Reflect.get(scrollTrigger, "refresh");
1121
+ if (typeof refresh === "function") Reflect.apply(refresh, scrollTrigger, []);
1122
+ }
757
1123
  function notifyLayoutChanged(reason) {
758
1124
  clearTimeout(layoutTimer);
759
1125
  layoutTimer = setTimeout(() => {
760
- const { ScrollTrigger } = (
761
- /** @type {Window & { ScrollTrigger?: { refresh?: () => void } }} */
762
- window
763
- );
764
- if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
765
- else ScrollTrigger?.refresh?.();
1126
+ try {
1127
+ if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
1128
+ else refreshScrollTrigger();
1129
+ } catch (error) {
1130
+ reportError(error, "layout-changed");
1131
+ }
766
1132
  emit("layout-changed", { reason });
767
1133
  }, LAYOUT_DEBOUNCE_MS);
768
1134
  }
@@ -789,15 +1155,27 @@
789
1155
  const raw = el ? get(el, name) : null;
790
1156
  if (!raw) return null;
791
1157
  try {
792
- return (
793
- /** @type {unknown} */
794
- JSON.parse(raw)
795
- );
1158
+ const json = JSON.parse(raw);
1159
+ return json;
796
1160
  } catch {
797
1161
  warn(`invalid JSON in ${attr(name)}`, el);
798
1162
  return null;
799
1163
  }
800
1164
  }
1165
+ function attributeText(value) {
1166
+ if (value === null || value === void 0) return "";
1167
+ if (typeof value === "string") return value;
1168
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1169
+ return JSON.stringify(value);
1170
+ }
1171
+ function attributesFromJson(json) {
1172
+ if (Array.isArray(json)) {
1173
+ const entries = json;
1174
+ return entries.filter(isRecord).map((entry) => ({ key: attributeText(entry["key"]), value: attributeText(entry["value"]) }));
1175
+ }
1176
+ if (!isRecord(json)) return [];
1177
+ return Object.entries(json).map(([key, value]) => ({ key, value: attributeText(value) }));
1178
+ }
801
1179
  function toAttributes(attributes) {
802
1180
  if (!attributes) return [];
803
1181
  if (Array.isArray(attributes)) {
@@ -806,14 +1184,10 @@
806
1184
  return Object.entries(attributes).map(([key, value]) => ({ key, value: String(value ?? "") }));
807
1185
  }
808
1186
 
809
- // src/fragments.js
810
- var fragments_exports = {};
811
- __export(fragments_exports, {
812
- cartFragment: () => cartFragment,
813
- collectionFields: () => collectionFields,
814
- productFragment: () => productFragment
815
- });
1187
+ // src/fragments.ts
816
1188
  var MONEY = "amount currencyCode";
1189
+ var FULL_ACCESS = { inventory: true };
1190
+ var inventoryField = (access) => access.inventory ? "quantityAvailable" : "";
817
1191
  var IMAGE = "url altText width height";
818
1192
  function metafieldsField(metafields) {
819
1193
  const identifiers = [];
@@ -827,11 +1201,11 @@
827
1201
  }
828
1202
  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}}}}}`;
829
1203
  var ALLOCATION_FIELDS = ` sellingPlan{id}checkoutChargeAmount{${MONEY}}remainingBalanceChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}unitPrice{${MONEY}}}`;
830
- function productFragment(o = cfg) {
831
- 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.metafields)} 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}}}}}}`;
1204
+ function productFragment(o = cfg, access = FULL_ACCESS) {
1205
+ 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.metafields)} 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 ${inventoryField(access)} sku barcode selectedOptions{name value}image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}unitPrice{${MONEY}}sellingPlanAllocations(first:${o.sellingPlansFirst}){nodes{${ALLOCATION_FIELDS}}}}}}`;
832
1206
  }
833
- function cartFragment() {
834
- 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 quantityAvailable 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}}}}}}}}`;
1207
+ function cartFragment(access = FULL_ACCESS) {
1208
+ return ` fragment TetherCart on Cart{id checkoutUrl totalQuantity note createdAt updatedAt attributes{key value}buyerIdentity{countryCode email}discountCodes{code applicable}cost{subtotalAmount{${MONEY}}totalAmount{${MONEY}}checkoutChargeAmount{${MONEY}}}lines(first:100){nodes{id quantity attributes{key value}cost{totalAmount{${MONEY}}subtotalAmount{${MONEY}}amountPerQuantity{${MONEY}}compareAtAmountPerQuantity{${MONEY}}}discountAllocations{discountedAmount{${MONEY}}}sellingPlanAllocation{sellingPlan{id name description recurringDeliveries options{name value}}checkoutChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}}}merchandise{... on ProductVariant{id title sku availableForSale requiresShipping ${inventoryField(access)} image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}selectedOptions{name value}product{id title handle vendor featuredImage{${IMAGE}}}}}... on ComponentizableCartLine{lineComponents{id quantity merchandise{... on ProductVariant{id title product{title handle}}}}}}}}`;
835
1209
  }
836
1210
  function collectionFields() {
837
1211
  return `
@@ -840,7 +1214,7 @@
840
1214
  seo { title description }`;
841
1215
  }
842
1216
 
843
- // src/selling-plans.js
1217
+ // src/selling-plans.ts
844
1218
  var selling_plans_exports = {};
845
1219
  __export(selling_plans_exports, {
846
1220
  GID: () => GID,
@@ -855,19 +1229,59 @@
855
1229
  plansForVariant: () => plansForVariant,
856
1230
  toGid: () => toGid
857
1231
  });
1232
+
1233
+ // src/amounts.ts
1234
+ var DECIMAL = /^(-?)(\d+)(?:\.(\d+))?$/;
1235
+ function toScaledInteger(amount, fractionDigits) {
1236
+ const match = DECIMAL.exec(amount.trim());
1237
+ if (!match) return null;
1238
+ const [, sign = "", whole = "0", fraction = ""] = match;
1239
+ const digits = BigInt(`${whole}${fraction.padEnd(fractionDigits, "0")}`);
1240
+ return sign === "-" ? -digits : digits;
1241
+ }
1242
+ var fractionLength = (amount) => amount.split(".")[1]?.length ?? 0;
1243
+ function isGreaterAmount(amount, other) {
1244
+ if (amount == null || other == null) return false;
1245
+ const fractionDigits = Math.max(fractionLength(amount.trim()), fractionLength(other.trim()));
1246
+ const scaled = toScaledInteger(amount, fractionDigits);
1247
+ const scaledOther = toScaledInteger(other, fractionDigits);
1248
+ if (scaled === null || scaledOther === null) return false;
1249
+ return scaled > scaledOther;
1250
+ }
1251
+ var DEFAULT_FRACTION_DIGITS = 2;
1252
+ var fractionDigitsByCurrency = /* @__PURE__ */ new Map();
1253
+ function currencyFractionDigits(currencyCode) {
1254
+ const known = fractionDigitsByCurrency.get(currencyCode);
1255
+ if (known !== void 0) return known;
1256
+ let digits = DEFAULT_FRACTION_DIGITS;
1257
+ try {
1258
+ const format = new Intl.NumberFormat("en", { style: "currency", currency: currencyCode });
1259
+ digits = format.resolvedOptions().maximumFractionDigits ?? DEFAULT_FRACTION_DIGITS;
1260
+ } catch {
1261
+ }
1262
+ fractionDigitsByCurrency.set(currencyCode, digits);
1263
+ return digits;
1264
+ }
1265
+ function toMinorUnits(money) {
1266
+ return Math.round(parseFloat(money.amount) * 10 ** currencyFractionDigits(money.currencyCode));
1267
+ }
1268
+ function fromMinorUnits(minorUnits, currencyCode) {
1269
+ const digits = currencyFractionDigits(currencyCode);
1270
+ return { amount: (minorUnits / 10 ** digits).toFixed(digits), currencyCode };
1271
+ }
1272
+
1273
+ // src/selling-plans.ts
1274
+ var PERCENT = 100;
858
1275
  function toGid(value, type) {
859
- const text = typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
860
- if (!text) return null;
861
- if (text.startsWith("gid://")) return text;
862
- if (/^\d+$/.test(text)) return `gid://shopify/${type}/${text}`;
863
- return text;
1276
+ const text2 = typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
1277
+ if (!text2) return null;
1278
+ if (text2.startsWith("gid://")) return text2;
1279
+ if (/^\d+$/.test(text2)) return `gid://shopify/${type}/${text2}`;
1280
+ return text2;
864
1281
  }
865
1282
  var GID = {
866
- /** @param {unknown} value */
867
1283
  product: (value) => toGid(value, "Product"),
868
- /** @param {unknown} value */
869
1284
  variant: (value) => toGid(value, "ProductVariant"),
870
- /** @param {unknown} value */
871
1285
  sellingPlan: (value) => toGid(value, "SellingPlan")
872
1286
  };
873
1287
  function allPlans(product) {
@@ -894,14 +1308,17 @@
894
1308
  return allocation?.checkoutChargeAmount ?? allocation?.priceAdjustments[0]?.price ?? variant?.price ?? null;
895
1309
  }
896
1310
  function planSavings(variant, planId) {
897
- const oneTime = parseFloat(variant?.price.amount ?? "");
898
- const onPlan = parseFloat(planPrice(variant, planId)?.amount ?? "");
899
- if (!variant || !Number.isFinite(oneTime) || !Number.isFinite(onPlan) || oneTime <= 0 || onPlan >= oneTime) {
1311
+ const onPlanPrice = planPrice(variant, planId);
1312
+ if (!variant || !onPlanPrice) return { amount: null, percentage: 0 };
1313
+ const oneTime = parseFloat(variant.price.amount);
1314
+ const onPlan = parseFloat(onPlanPrice.amount);
1315
+ if (!Number.isFinite(oneTime) || !Number.isFinite(onPlan) || oneTime <= 0 || onPlan >= oneTime) {
900
1316
  return { amount: null, percentage: 0 };
901
1317
  }
1318
+ const saved = toMinorUnits(variant.price) - toMinorUnits(onPlanPrice);
902
1319
  return {
903
- amount: { amount: (oneTime - onPlan).toFixed(2), currencyCode: variant.price.currencyCode },
904
- percentage: Math.round((1 - onPlan / oneTime) * 100)
1320
+ amount: fromMinorUnits(saved, variant.price.currencyCode),
1321
+ percentage: Math.round((1 - onPlan / oneTime) * PERCENT)
905
1322
  };
906
1323
  }
907
1324
  function planAdjustment(plan) {
@@ -926,7 +1343,40 @@
926
1343
  return plans.find((plan) => plan.id === gid) ?? plans.find((plan) => plan.name.toLowerCase() === needle) ?? plans.find((plan) => plan.options.some((option) => option.value.toLowerCase() === needle)) ?? null;
927
1344
  }
928
1345
 
929
- // src/cart/drawer.js
1346
+ // src/product-shape.ts
1347
+ var MONEY2 = { amount: "string", currencyCode: "string" };
1348
+ var PRODUCT = {
1349
+ id: "string",
1350
+ handle: "string",
1351
+ title: "string",
1352
+ options: ["object"],
1353
+ images: { nodes: ["object"] },
1354
+ priceRange: { minVariantPrice: MONEY2, maxVariantPrice: MONEY2 },
1355
+ variants: { nodes: [{ id: "string", price: MONEY2, selectedOptions: ["object"] }] }
1356
+ };
1357
+ var PRODUCT_CONNECTION = { nodes: [PRODUCT], pageInfo: { hasNextPage: "boolean" } };
1358
+ var isProductShape = (value) => field(value, "id") !== "" && field(value, "handle") !== "" && matches(value, PRODUCT);
1359
+ var PRODUCT_NODE = (node) => isRecord(node) && (node["id"] === void 0 || matches(node, PRODUCT));
1360
+
1361
+ // src/cart/cart-shape.ts
1362
+ var LINE = {
1363
+ id: "string",
1364
+ quantity: "number",
1365
+ attributes: ["object"],
1366
+ cost: { totalAmount: MONEY2, subtotalAmount: MONEY2, amountPerQuantity: MONEY2 },
1367
+ merchandise: { id: "string", selectedOptions: ["object"], product: "object" }
1368
+ };
1369
+ var CART = {
1370
+ id: "string",
1371
+ checkoutUrl: "string",
1372
+ totalQuantity: "number",
1373
+ attributes: ["object"],
1374
+ discountCodes: ["object"],
1375
+ cost: { totalAmount: MONEY2, subtotalAmount: MONEY2 },
1376
+ lines: { nodes: [LINE] }
1377
+ };
1378
+
1379
+ // src/cart/drawer.ts
930
1380
  var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
931
1381
  var FOCUS_AFTER_OPEN_MS = 60;
932
1382
  var openWithoutDrawer = false;
@@ -1014,19 +1464,7 @@
1014
1464
  (event.shiftKey ? last : first).focus();
1015
1465
  }
1016
1466
 
1017
- // src/loader.js
1018
- var loader_exports = {};
1019
- __export(loader_exports, {
1020
- clearProductCache: () => clearProductCache,
1021
- getProduct: () => getProduct,
1022
- getProducts: () => getProducts,
1023
- getRecommendations: () => getRecommendations,
1024
- hydrateProducts: () => hydrateProducts,
1025
- normalizeProductKey: () => normalizeProductKey,
1026
- primeProduct: () => primeProduct,
1027
- refreshProducts: () => refreshProducts,
1028
- takeStale: () => takeStale
1029
- });
1467
+ // src/loader.ts
1030
1468
  var MISS_CACHE_MS = 1e4;
1031
1469
  var FRESH_MS = 6e4;
1032
1470
  var productCache = /* @__PURE__ */ new Map();
@@ -1060,6 +1498,7 @@
1060
1498
  productCache.set(key, loading);
1061
1499
  return loading;
1062
1500
  }
1501
+ var withProductFragment = (operation) => (access) => `${operation}${productFragment(cfg, access)}`;
1063
1502
  function batchQuery(keys) {
1064
1503
  const ids = keys.filter(isGid);
1065
1504
  const handles = keys.filter((key) => !isGid(key));
@@ -1076,8 +1515,8 @@
1076
1515
  variables[`h${index}`] = handle;
1077
1516
  selections.push(`h${index}: product(handle: $h${index}) { ...TetherProduct }`);
1078
1517
  });
1079
- const query = `query TetherProducts(${definitions.join(",")}){${selections.join(" ")}}${productFragment()}`;
1080
- return { query, variables, handles };
1518
+ const query2 = withProductFragment(`query TetherProducts(${definitions.join(",")}){${selections.join(" ")}}`);
1519
+ return { query: query2, variables, handles };
1081
1520
  }
1082
1521
  function settleWaiting(key, settle) {
1083
1522
  const waiter = waiting.get(key);
@@ -1091,16 +1530,21 @@
1091
1530
  if (productCache.get(key) === miss) productCache.delete(key);
1092
1531
  }, MISS_CACHE_MS);
1093
1532
  }
1533
+ function isBatchData(data) {
1534
+ if (!isRecord(data)) return false;
1535
+ const { byId = [], ...byHandle } = data;
1536
+ return matches(byId, [orNull(PRODUCT_NODE)]) && Object.values(byHandle).every(orNull(PRODUCT));
1537
+ }
1094
1538
  async function flush() {
1095
1539
  flushScheduled = false;
1096
1540
  const batchGeneration = generation;
1097
1541
  const keys = pending.splice(0);
1098
1542
  if (!keys.length) return;
1099
- const { query, variables, handles } = batchQuery(keys);
1543
+ const { query: query2, variables, handles } = batchQuery(keys);
1100
1544
  log("fetch products", keys);
1101
1545
  let data;
1102
1546
  try {
1103
- data = await gql(query, variables);
1547
+ data = await gql(query2, variables, isBatchData);
1104
1548
  } catch (error) {
1105
1549
  keys.forEach((key) => {
1106
1550
  settleWaiting(key, (waiter) => {
@@ -1111,7 +1555,7 @@
1111
1555
  return;
1112
1556
  }
1113
1557
  const found = /* @__PURE__ */ new Map();
1114
- for (const product of data.byId ?? []) if (product) found.set(product.id, product);
1558
+ for (const product of data.byId ?? []) if (product?.id) found.set(product.id, product);
1115
1559
  handles.forEach((handle, index) => {
1116
1560
  const product = data[`h${index}`];
1117
1561
  if (product) found.set(handle, product);
@@ -1157,51 +1601,57 @@
1157
1601
  void productCache.get(key)?.then((product) => {
1158
1602
  stale.delete(product.handle);
1159
1603
  stale.delete(product.id);
1160
- });
1604
+ }, failureIsReportedByTheRender);
1161
1605
  return true;
1162
1606
  }
1163
- function refreshProducts(keys) {
1164
- for (const key of keys) {
1165
- const normalized = normalizeProductKey(key);
1166
- if (normalized) productCache.delete(normalized);
1167
- }
1168
- return keys.map((key) => getProduct(key));
1607
+ function failureIsReportedByTheRender() {
1169
1608
  }
1609
+ function refreshProduct(key) {
1610
+ const normalized = normalizeProductKey(key);
1611
+ if (normalized) productCache.delete(normalized);
1612
+ return getProduct(key);
1613
+ }
1614
+ var isCollectionAnswer = shaped({
1615
+ collection: orNull({ handle: "string", products: PRODUCT_CONNECTION })
1616
+ });
1617
+ var isSearchAnswer = shaped({ products: PRODUCT_CONNECTION });
1618
+ var isRecommendationsAnswer = shaped({
1619
+ productRecommendations: orNull([PRODUCT])
1620
+ });
1170
1621
  var COLLECTION_QUERY = `query TetherCollection($handle:String!,$first:Int!,$after:String,$sortKey:ProductCollectionSortKeys,$reverse:Boolean,$filters:[ProductFilter!]){collection(handle:$handle){${collectionFields()} products(first:$first,after:$after,sortKey:$sortKey,reverse:$reverse,filters:$filters){nodes{...TetherProduct}pageInfo{hasNextPage endCursor}}}}`;
1171
1622
  var SEARCH_QUERY = `query TetherSearch($first:Int!,$after:String,$query:String,$sortKey:ProductSortKeys,$reverse:Boolean){products(first:$first,after:$after,query:$query,sortKey:$sortKey,reverse:$reverse){nodes{...TetherProduct}pageInfo{hasNextPage endCursor}}}`;
1172
1623
  async function getProducts({
1173
1624
  collection,
1174
- query,
1625
+ query: query2,
1175
1626
  first = cfg.collectionFirst,
1176
1627
  after = null,
1177
1628
  sortKey,
1178
1629
  reverse = false,
1179
1630
  filters
1180
1631
  } = {}) {
1181
- const fragment = productFragment();
1182
1632
  if (collection) {
1183
- const data2 = await gql(`${COLLECTION_QUERY}
1184
- ${fragment}`, {
1185
- handle: collection,
1186
- first,
1187
- after,
1188
- sortKey: sortKey || void 0,
1189
- reverse,
1190
- filters: filters ?? void 0
1191
- });
1633
+ const data2 = await gql(
1634
+ withProductFragment(COLLECTION_QUERY),
1635
+ {
1636
+ handle: collection,
1637
+ first,
1638
+ after,
1639
+ sortKey: sortKey || void 0,
1640
+ reverse,
1641
+ filters: filters ?? void 0
1642
+ },
1643
+ isCollectionAnswer
1644
+ );
1192
1645
  if (!data2.collection) throw new TetherError(`Collection not found: ${collection}`, { code: "NOT_FOUND" });
1193
1646
  const { products, ...rest } = data2.collection;
1194
1647
  products.nodes.forEach(primeProduct);
1195
1648
  return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
1196
1649
  }
1197
- const data = await gql(`${SEARCH_QUERY}
1198
- ${fragment}`, {
1199
- first,
1200
- after,
1201
- query: query || void 0,
1202
- sortKey: sortKey || void 0,
1203
- reverse
1204
- });
1650
+ const data = await gql(
1651
+ withProductFragment(SEARCH_QUERY),
1652
+ { first, after, query: query2 || void 0, sortKey: sortKey || void 0, reverse },
1653
+ isSearchAnswer
1654
+ );
1205
1655
  data.products.nodes.forEach(primeProduct);
1206
1656
  return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
1207
1657
  }
@@ -1209,45 +1659,30 @@ ${fragment}`, {
1209
1659
  const key = normalizeProductKey(typeof product === "object" ? product?.id : product);
1210
1660
  const byId = key?.startsWith("gid://") ?? false;
1211
1661
  const data = await gql(
1212
- `query TetherRecs(${byId ? "$id: ID!" : "$handle: String!"},$intent:ProductRecommendationIntent){productRecommendations(${byId ? "productId: $id" : "productHandle: $handle"},intent:$intent){...TetherProduct}}${productFragment()}`,
1213
- byId ? { id: key, intent } : { handle: key, intent }
1662
+ withProductFragment(
1663
+ `query TetherRecs(${byId ? "$id: ID!" : "$handle: String!"},$intent:ProductRecommendationIntent){productRecommendations(${byId ? "productId: $id" : "productHandle: $handle"},intent:$intent){...TetherProduct}}`
1664
+ ),
1665
+ byId ? { id: key, intent } : { handle: key, intent },
1666
+ isRecommendationsAnswer
1214
1667
  );
1215
1668
  const list = data.productRecommendations ?? [];
1216
1669
  list.forEach(primeProduct);
1217
1670
  return list;
1218
1671
  }
1219
1672
 
1220
- // src/amounts.js
1221
- var DECIMAL = /^(-?)(\d+)(?:\.(\d+))?$/;
1222
- function toScaledInteger(amount, fractionDigits) {
1223
- const match = DECIMAL.exec(amount.trim());
1224
- if (!match) return null;
1225
- const [, sign = "", whole = "0", fraction = ""] = match;
1226
- const digits = BigInt(`${whole}${fraction.padEnd(fractionDigits, "0")}`);
1227
- return sign === "-" ? -digits : digits;
1228
- }
1229
- var fractionLength = (amount) => amount.split(".")[1]?.length ?? 0;
1230
- function isGreaterAmount(amount, other) {
1231
- if (amount == null || other == null) return false;
1232
- const fractionDigits = Math.max(fractionLength(amount.trim()), fractionLength(other.trim()));
1233
- const scaled = toScaledInteger(amount, fractionDigits);
1234
- const scaledOther = toScaledInteger(other, fractionDigits);
1235
- if (scaled === null || scaledOther === null) return false;
1236
- return scaled > scaledOther;
1237
- }
1238
-
1239
- // src/cart/render.js
1673
+ // src/cart/render.ts
1240
1674
  var DEFAULT_TITLE = "Default Title";
1241
1675
  var quantityOf = (line, pending2) => pending2.get(line.id)?.quantity ?? line.quantity;
1242
1676
  var numericId = (gid) => gid.split("/").pop() ?? gid;
1243
1677
  function lineUrl(line) {
1244
1678
  const variant = line.merchandise;
1245
1679
  const base = productUrl2(variant.product.handle, variant.product.id);
1246
- const query = new URLSearchParams();
1247
- query.set("variant", numericId(variant.id));
1680
+ if (!base) return null;
1681
+ const query2 = new URLSearchParams();
1682
+ query2.set("variant", numericId(variant.id));
1248
1683
  const planId = line.sellingPlanAllocation?.sellingPlan.id;
1249
- if (planId) query.set("selling_plan", numericId(planId));
1250
- return `${base}${base.includes("?") ? "&" : "?"}${query.toString()}`;
1684
+ if (planId) query2.set("selling_plan", numericId(planId));
1685
+ return `${base}${base.includes("?") ? "&" : "?"}${query2.toString()}`;
1251
1686
  }
1252
1687
  var productTitle = (line) => line.merchandise.product.title;
1253
1688
  var LINE_TEXT = {
@@ -1305,19 +1740,17 @@ ${fragment}`, {
1305
1740
  qsa(node, sel("line", "warning")).forEach((el) => {
1306
1741
  setTextTracked(el, warning?.message ?? "");
1307
1742
  });
1308
- const conditions = {
1309
- subscription: isSubscription,
1310
- "one-time": !isSubscription,
1311
- "on-sale": isGreaterAmount(line.cost.compareAtAmountPerQuantity?.amount, line.cost.amountPerQuantity.amount),
1312
- "has-components": (line.lineComponents ?? []).length > 0,
1313
- warning: Boolean(warning)
1314
- };
1743
+ const conditions = /* @__PURE__ */ new Map([
1744
+ ["subscription", isSubscription],
1745
+ ["one-time", !isSubscription],
1746
+ ["on-sale", isGreaterAmount(line.cost.compareAtAmountPerQuantity?.amount, line.cost.amountPerQuantity.amount)],
1747
+ ["has-components", (line.lineComponents ?? []).length > 0],
1748
+ ["warning", Boolean(warning)]
1749
+ ]);
1315
1750
  qsa(node, sel("line-show")).forEach((el) => {
1316
1751
  const key = get(el, "line-show");
1317
- if (key !== null && Object.hasOwn(conditions, key)) show(el, conditions[
1318
- /** @type {keyof conditions} */
1319
- key
1320
- ]);
1752
+ const visible = key === null ? void 0 : conditions.get(key);
1753
+ if (visible !== void 0) show(el, visible);
1321
1754
  });
1322
1755
  }
1323
1756
  function renderLine(node, line, view) {
@@ -1330,7 +1763,8 @@ ${fragment}`, {
1330
1763
  return;
1331
1764
  }
1332
1765
  if (key === "url") {
1333
- el.setAttribute("href", lineUrl(line));
1766
+ const url2 = lineUrl(line);
1767
+ if (url2) el.setAttribute("href", url2);
1334
1768
  return;
1335
1769
  }
1336
1770
  const value = lineValue(line, key, view);
@@ -1339,12 +1773,7 @@ ${fragment}`, {
1339
1773
  renderLineState(node, line, view);
1340
1774
  qsa(node, `select${sel("line-plan")}`).forEach((select) => {
1341
1775
  getProduct(variant.product.handle).then((product) => {
1342
- fillPlanSelect(
1343
- /** @type {HTMLSelectElement} */
1344
- select,
1345
- line,
1346
- product
1347
- );
1776
+ fillPlanSelect(select, line, product);
1348
1777
  }).catch((error) => {
1349
1778
  reportError(error, "line plan options");
1350
1779
  });
@@ -1353,22 +1782,20 @@ ${fragment}`, {
1353
1782
  function renderConditions(cart, view) {
1354
1783
  const count = cart?.totalQuantity ?? 0;
1355
1784
  const hasSubscription = (cart?.lines.nodes ?? []).some((line) => line.sellingPlanAllocation);
1356
- const conditions = {
1357
- "cart-empty": count === 0,
1358
- "cart-filled": count > 0,
1359
- "cart-has-subscription": hasSubscription,
1360
- "cart-no-subscription": !hasSubscription,
1361
- "cart-has-discount": (cart?.discountCodes ?? []).some((code) => code.applicable),
1362
- "cart-error": view.failed,
1363
- "cart-warning": view.warnings.length > 0
1364
- };
1785
+ const conditions = /* @__PURE__ */ new Map([
1786
+ ["cart-empty", count === 0],
1787
+ ["cart-filled", count > 0],
1788
+ ["cart-has-subscription", hasSubscription],
1789
+ ["cart-no-subscription", !hasSubscription],
1790
+ ["cart-has-discount", (cart?.discountCodes ?? []).some((code) => code.applicable)],
1791
+ ["cart-error", view.failed],
1792
+ ["cart-warning", view.warnings.length > 0]
1793
+ ]);
1365
1794
  qsa(document, sel("show")).forEach((el) => {
1366
1795
  const key = get(el, "show");
1367
- if (key === null || !Object.hasOwn(conditions, key) || el.closest(sel("product"))) return;
1368
- show(el, conditions[
1369
- /** @type {keyof conditions} */
1370
- key
1371
- ]);
1796
+ const visible = key === null ? void 0 : conditions.get(key);
1797
+ if (visible === void 0 || el.closest(sel("product"))) return;
1798
+ show(el, visible);
1372
1799
  });
1373
1800
  qsa(document, sel("checkout")).forEach((el) => {
1374
1801
  el.classList.toggle("is-disabled", !count);
@@ -1424,15 +1851,35 @@ ${fragment}`, {
1424
1851
  }
1425
1852
  function showSavedCount(count) {
1426
1853
  qsa(document, sel("cart-count")).forEach((el) => {
1427
- if (/^\s*0?\s*$/.test(el.textContent)) el.textContent = count;
1854
+ if (/^\s*0?\s*$/.test(el.textContent)) el.textContent = String(count);
1428
1855
  });
1429
1856
  }
1430
1857
 
1431
- // src/cart/store.js
1858
+ // src/storage.ts
1859
+ function getStored(key, area = "localStorage") {
1860
+ try {
1861
+ return window[area].getItem(key);
1862
+ } catch {
1863
+ return null;
1864
+ }
1865
+ }
1866
+ function setStored(key, value, area = "localStorage") {
1867
+ try {
1868
+ window[area].setItem(key, value);
1869
+ } catch {
1870
+ }
1871
+ }
1872
+ function removeStored(key, area = "localStorage") {
1873
+ try {
1874
+ window[area].removeItem(key);
1875
+ } catch {
1876
+ }
1877
+ }
1878
+
1879
+ // src/cart/store.ts
1432
1880
  var status = { kind: "unknown" };
1433
1881
  var warnings = [];
1434
1882
  var pendingQuantities = /* @__PURE__ */ new Map();
1435
- var customer = { accessToken: null, beforeCheckout: null };
1436
1883
  var cartStatus = () => status;
1437
1884
  function currentCart() {
1438
1885
  switch (status.kind) {
@@ -1447,27 +1894,13 @@ ${fragment}`, {
1447
1894
  var currentWarnings = () => warnings;
1448
1895
  var statusOf = (cart) => cart ? { kind: "ready", cart } : { kind: "empty" };
1449
1896
  var seenKey = () => `${cfg.storageKey}:seen`;
1450
- function readStorageIfAllowed(key) {
1451
- try {
1452
- return localStorage.getItem(key);
1453
- } catch {
1454
- return null;
1455
- }
1456
- }
1457
- function writeStorageIfAllowed(key, value) {
1458
- try {
1459
- if (value === null) localStorage.removeItem(key);
1460
- else localStorage.setItem(key, value);
1461
- } catch {
1462
- }
1463
- }
1464
- var savedCartId = () => readStorageIfAllowed(cfg.storageKey) || null;
1897
+ var savedCartId = () => getStored(cfg.storageKey) || null;
1465
1898
  function forgetSavedCart() {
1466
- writeStorageIfAllowed(cfg.storageKey, null);
1899
+ removeStored(cfg.storageKey);
1467
1900
  }
1468
1901
  function keepQuietly(cart) {
1469
1902
  status = statusOf(cart);
1470
- if (cart) writeStorageIfAllowed(cfg.storageKey, cart.id);
1903
+ if (cart) setStored(cfg.storageKey, cart.id);
1471
1904
  }
1472
1905
  function render() {
1473
1906
  renderCart({
@@ -1483,7 +1916,7 @@ ${fragment}`, {
1483
1916
  }
1484
1917
  function commit(cart, reason) {
1485
1918
  keepQuietly(cart);
1486
- writeStorageIfAllowed(seenKey(), `${Date.now()}|${cart?.totalQuantity ?? 0}`);
1919
+ setStored(seenKey(), `${Date.now()}|${cart?.totalQuantity ?? 0}`);
1487
1920
  announce(reason);
1488
1921
  return cart;
1489
1922
  }
@@ -1491,14 +1924,15 @@ ${fragment}`, {
1491
1924
  status = outcome.kind === "loaded" ? statusOf(outcome.cart) : { kind: "error", error: outcome.error, cart: null };
1492
1925
  announce("load");
1493
1926
  }
1494
- function setFailure(error) {
1495
- const cart = currentCart();
1496
- status = error == null ? statusOf(cart) : { kind: "error", error, cart };
1927
+ function lastKnownCount() {
1928
+ const text2 = getStored(seenKey())?.split("|")[1] ?? "";
1929
+ const count = Number(text2);
1930
+ return /^\d+$/.test(text2) && Number.isSafeInteger(count) ? count : null;
1497
1931
  }
1498
1932
  function showLastKnownCount() {
1499
1933
  if (status.kind !== "unknown") return;
1500
- const count = readStorageIfAllowed(seenKey())?.split("|")[1];
1501
- if (count) showSavedCount(count);
1934
+ const count = lastKnownCount();
1935
+ if (count !== null) showSavedCount(count);
1502
1936
  }
1503
1937
  function setWarnings(notes) {
1504
1938
  warnings = notes.filter((note, index) => notes.findIndex((other) => other.message === note.message) === index).map((note) => ({ ...note, message: cfg.warningText?.(note) || note.message }));
@@ -1536,7 +1970,7 @@ ${fragment}`, {
1536
1970
  open: isOpen(),
1537
1971
  count: currentCart()?.totalQuantity ?? 0,
1538
1972
  loaded: status.kind !== "unknown",
1539
- warnings
1973
+ warnings: [...warnings]
1540
1974
  });
1541
1975
  function subscribe(listener) {
1542
1976
  const events = ["cart-updated", "cart-toggled"].map((name) => `${cfg.prefix}:${name}`);
@@ -1554,15 +1988,16 @@ ${fragment}`, {
1554
1988
  };
1555
1989
  }
1556
1990
 
1557
- // src/cart/mutations.js
1991
+ // src/cart/mutations.ts
1558
1992
  var UNKNOWN_CART_MESSAGE = /does not exist|not found|invalid id/i;
1993
+ var isPayloadOf = (name) => shaped({ [name]: orNull({ cart: orNull(CART) }) });
1559
1994
  function isCartGone(error) {
1560
1995
  if (!(error instanceof TetherError)) return false;
1561
1996
  if (error.userErrors?.some((userError) => userError.field?.[0] === "cartId")) return true;
1562
1997
  return (error.errors ?? []).some((graphQLError) => UNKNOWN_CART_MESSAGE.test(graphQLError.message));
1563
1998
  }
1564
1999
  function toLineInput(input, quantity = 1, options = {}) {
1565
- const spec = typeof input === "object" && input !== null ? input : { ...options, variantId: input, quantity };
2000
+ const spec = typeof input === "object" && input !== null ? input : { ...options, quantity, ...input === null || input === void 0 ? {} : { variantId: input } };
1566
2001
  const merchandiseId = GID.variant(spec.merchandiseId || spec.variantId || spec.id);
1567
2002
  if (!merchandiseId) throw new TetherError("add(): variantId is required");
1568
2003
  const asked = parseInt(String(spec.quantity ?? 1), 10);
@@ -1574,23 +2009,25 @@ ${fragment}`, {
1574
2009
  return line;
1575
2010
  }
1576
2011
  function mutationText(name, args, inner) {
1577
- return `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}warnings{code message target}}}${cartFragment()}`;
2012
+ return (access) => `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}warnings{code message target}}}${cartFragment(access)}`;
1578
2013
  }
1579
2014
  async function send(name, args, inner, variables) {
1580
- const data = await gql(mutationText(name, args, inner), variables);
2015
+ const data = await gql(mutationText(name, args, inner), variables, isPayloadOf(name));
1581
2016
  const payload = unwrap(data[name], name);
1582
2017
  setWarnings(payload.warnings ?? []);
1583
2018
  return payload.cart;
1584
2019
  }
1585
2020
  function createCart(input) {
1586
- const buyerIdentity = {
1587
- ...cfg.country ? { countryCode: cfg.country.toUpperCase() } : {},
1588
- ...customer.accessToken ? { customerAccessToken: customer.accessToken } : {}
1589
- };
1590
- const withIdentity = Object.keys(buyerIdentity).length ? { buyerIdentity, ...input } : input;
2021
+ const withIdentity = cfg.country ? { buyerIdentity: { countryCode: cfg.country }, ...input } : input;
1591
2022
  return send("cartCreate", "$input: CartInput!", "input: $input", { input: withIdentity });
1592
2023
  }
1593
- async function mutateCart({ name, args, inner, variables, createInput }) {
2024
+ async function mutateCart({
2025
+ name,
2026
+ args,
2027
+ inner,
2028
+ variables,
2029
+ createInput
2030
+ }) {
1594
2031
  const cartId = savedCartId();
1595
2032
  if (!cartId) return createInput ? createCart(createInput) : null;
1596
2033
  try {
@@ -1604,11 +2041,19 @@ ${fragment}`, {
1604
2041
  }
1605
2042
  }
1606
2043
  async function fetchCart(id) {
1607
- const data = await gql(`query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${cartFragment()}`, { id });
2044
+ const data = await gql(
2045
+ (access) => `query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${cartFragment(access)}`,
2046
+ { id },
2047
+ shaped({ cart: orNull(CART) })
2048
+ );
1608
2049
  return data.cart;
1609
2050
  }
1610
2051
  async function fetchCheckoutUrl(id) {
1611
- const data = await gql("query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }", { id });
2052
+ const data = await gql(
2053
+ "query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }",
2054
+ { id },
2055
+ shaped({ cart: orNull({ checkoutUrl: "string" }) })
2056
+ );
1612
2057
  return data.cart?.checkoutUrl ?? null;
1613
2058
  }
1614
2059
  function toLineUpdate(lineId, patch) {
@@ -1638,21 +2083,20 @@ ${fragment}`, {
1638
2083
  });
1639
2084
  var setLineQuantity = (lineId, target) => target > 0 ? updateLines([{ id: lineId, quantity: target }]) : removeLines([lineId]);
1640
2085
 
1641
- // src/cart/checkout.js
2086
+ // src/cart/checkout.ts
2087
+ var resolveAccountCheckout = null;
2088
+ function setCheckoutResolver(resolver) {
2089
+ resolveAccountCheckout = resolver;
2090
+ }
1642
2091
  var askCheckoutUrl = (cartId) => fetchCheckoutUrl(cartId).then(
1643
- (url) => ({ kind: (
1644
- /** @type {const} */
1645
- "url"
1646
- ), url }),
1647
- (error) => ({ kind: (
1648
- /** @type {const} */
1649
- "failed"
1650
- ), error })
2092
+ (url2) => ({ kind: "url", url: url2 }),
2093
+ (error) => ({ kind: "failed", error })
1651
2094
  );
1652
2095
  async function accountCheckoutUrl() {
1653
- if (!customer.beforeCheckout) return null;
2096
+ if (!resolveAccountCheckout) return null;
1654
2097
  try {
1655
- return await customer.beforeCheckout();
2098
+ const url2 = await resolveAccountCheckout();
2099
+ return url2 && `${url2}${url2.includes("?") ? "&" : "?"}logged_in=true`;
1656
2100
  } catch (error) {
1657
2101
  reportError(error, "account checkout");
1658
2102
  return null;
@@ -1660,15 +2104,12 @@ ${fragment}`, {
1660
2104
  }
1661
2105
  async function checkoutUrl(cartId) {
1662
2106
  const cartAnswer = askCheckoutUrl(cartId);
1663
- let url = await accountCheckoutUrl();
1664
- if (!url) {
1665
- const answer = await cartAnswer;
1666
- if (answer.kind === "failed") throw answer.error;
1667
- url = answer.url;
1668
- }
1669
- if (!url) throw new TetherError("No checkoutUrl (cart empty or expired)");
1670
- if (!customer.accessToken) return url;
1671
- return `${url}${url.includes("?") ? "&" : "?"}logged_in=true`;
2107
+ const accountUrl = await accountCheckoutUrl();
2108
+ if (accountUrl) return accountUrl;
2109
+ const answer = await cartAnswer;
2110
+ if (answer.kind === "failed") throw answer.error;
2111
+ if (!answer.url) throw new TetherError("No checkoutUrl (cart empty or expired)");
2112
+ return answer.url;
1672
2113
  }
1673
2114
  function detachOpenerIfAllowed(tab) {
1674
2115
  try {
@@ -1683,12 +2124,12 @@ ${fragment}`, {
1683
2124
  const tab = target === "_blank" ? window.open("", "_blank") : null;
1684
2125
  if (tab) detachOpenerIfAllowed(tab);
1685
2126
  try {
1686
- const url = await checkoutUrl(cartId);
1687
- if (!isHttpsUrl(url)) throw new TetherError("The checkout URL is not https: not opened");
1688
- emit("checkout", { url, cart: currentCart() });
1689
- if (tab && !tab.closed) tab.location.href = url;
1690
- else window.location.href = url;
1691
- return url;
2127
+ const url2 = await checkoutUrl(cartId);
2128
+ if (!isHttpsUrl(url2)) throw new TetherError("The checkout URL is not https: not opened");
2129
+ emit("checkout", { url: url2, cart: currentCart() });
2130
+ if (tab && !tab.closed) tab.location.href = url2;
2131
+ else window.location.href = url2;
2132
+ return url2;
1692
2133
  } catch (error) {
1693
2134
  tab?.close();
1694
2135
  reportError(error, "checkout failed");
@@ -1696,7 +2137,7 @@ ${fragment}`, {
1696
2137
  }
1697
2138
  }
1698
2139
 
1699
- // src/cart.js
2140
+ // src/cart.ts
1700
2141
  var currentLines = () => currentCart()?.lines.nodes ?? [];
1701
2142
  function changeQuantity(lineId, delta) {
1702
2143
  const line = currentLines().find((candidate) => candidate.id === lineId);
@@ -1753,83 +2194,67 @@ ${fragment}`, {
1753
2194
  }
1754
2195
  var Cart = {
1755
2196
  init,
1756
- /** @returns {string | null} The saved cart's id. */
2197
+ /** The saved cart's id. */
1757
2198
  id: savedCartId,
1758
- /** @returns {CartStatus} */
1759
2199
  get status() {
1760
2200
  return cartStatus();
1761
2201
  },
1762
- /** @returns {ShopifyCart | null} */
1763
2202
  get state() {
1764
2203
  return currentCart();
1765
2204
  },
1766
- /** @returns {readonly ShopifyCartLine[]} */
2205
+ /** The cart's lines, a copy: changing it changes nothing in the cart. */
1767
2206
  get lines() {
1768
- return currentLines();
2207
+ return [...currentLines()];
1769
2208
  },
1770
- /** @returns {number} */
1771
2209
  get count() {
1772
2210
  return currentCart()?.totalQuantity ?? 0;
1773
2211
  },
1774
- /** @returns {boolean} True once the saved cart, or that there is none, is known. */
2212
+ /** True once the saved cart, or that there is none, is known. */
1775
2213
  get loaded() {
1776
2214
  return cartStatus().kind !== "unknown";
1777
2215
  },
1778
- /** @returns {unknown} The failure of the last load or change, until a change succeeds. */
2216
+ /**
2217
+ * The failure of the last load or change, until a change succeeds. Requests fail with an `Error`; anything else
2218
+ * thrown is wrapped, so the public type holds.
2219
+ */
1779
2220
  get lastError() {
1780
2221
  const current = cartStatus();
1781
- return current.kind === "error" ? current.error : null;
1782
- },
1783
- set lastError(error) {
1784
- setFailure(error);
2222
+ if (current.kind !== "error") return null;
2223
+ return current.error instanceof Error ? current.error : new Error(String(current.error));
1785
2224
  },
1786
- /** @returns {readonly CartWarning[]} Shopify's notes on the last change. */
2225
+ /** Shopify's notes on the last change, a copy. */
1787
2226
  get warnings() {
1788
- return currentWarnings();
2227
+ return [...currentWarnings()];
1789
2228
  },
1790
- /** @returns {ReadonlyMap<string, { quantity: number }>} Line id → the quantity quick clicks asked for. */
2229
+ /** Line id → the quantity quick clicks asked for. */
1791
2230
  get pendingQty() {
1792
2231
  return pendingQuantities;
1793
2232
  },
1794
- /** @returns {string | null} The logged-in customer's Customer Account API token (set by the account part). */
1795
- get customerAccessToken() {
1796
- return customer.accessToken;
1797
- },
1798
- set customerAccessToken(token) {
1799
- customer.accessToken = token || null;
1800
- },
1801
- /** @returns {(() => Promise<string | null>) | null} The account's own checkout URL (set by the account part). */
1802
- get beforeCheckout() {
1803
- return customer.beforeCheckout;
1804
- },
1805
- set beforeCheckout(fetchUrl) {
1806
- customer.beforeCheckout = typeof fetchUrl === "function" ? fetchUrl : null;
1807
- },
1808
2233
  /**
1809
- * Links the cart to a logged-in customer, or unlinks it with `null`s. Carts created from now on belong to the
1810
- * customer; `beforeCheckout` gives the checkout URL (`null` from it uses the cart's).
1811
- *
1812
- * @param {Partial<CustomerLink>} link Fields left out keep their value.
2234
+ * Sets where a logged-in customer checks out, or clears it with `null`: the account part's resolver answers the
2235
+ * customer's checkout URL for the cart (`null` uses the cart's own).
2236
+ */
2237
+ setCheckoutResolver,
2238
+ /**
2239
+ * Forgets the saved cart on this device, as if there were none: the page shows an empty cart and
2240
+ * `tether:cart-updated` fires with reason `load`. The next add starts a new cart.
1813
2241
  */
1814
- setCustomer(link) {
1815
- if (link.accessToken !== void 0) customer.accessToken = link.accessToken;
1816
- if (link.beforeCheckout !== void 0) customer.beforeCheckout = link.beforeCheckout;
2242
+ forgetSaved() {
2243
+ forgetSavedCart();
2244
+ settleLoad({ kind: "loaded", cart: null });
1817
2245
  },
1818
- /** Keeps a cart and shows it, as a change does; tests seed the cart with it. */
1819
- _commit: commit,
1820
2246
  render,
1821
2247
  subscribe,
1822
2248
  /**
1823
2249
  * `add(variantId, 2)`, `add({ variantId, quantity, sellingPlanId, attributes })`, or `add([line, line])` for
1824
2250
  * bundles in one request. Creates the cart when there is none.
1825
2251
  *
1826
- * @param {string | number | LineSpec | (string | number | LineSpec)[]} input
1827
- * @param {number} [quantity]
1828
- * @param {Omit<LineSpec, 'variantId' | 'quantity'>} [options]
1829
- * @returns {Promise<ShopifyCart | null>}
2252
+ * @throws {TetherError} (rejected) When the add fails, or Shopify answers it without a cart.
1830
2253
  */
1831
2254
  add(input, quantity = 1, options = {}) {
1832
- const lines = (Array.isArray(input) ? input : [input]).map((line) => toLineInput(line, quantity, options));
2255
+ const lines = (Array.isArray(input) ? input : [input]).map(
2256
+ (line) => toLineInput(line, quantity, options)
2257
+ );
1833
2258
  return enqueue(async () => {
1834
2259
  const cart = await mutateCart({
1835
2260
  name: "cartLinesAdd",
@@ -1838,6 +2263,7 @@ ${fragment}`, {
1838
2263
  variables: { lines },
1839
2264
  createInput: { lines }
1840
2265
  });
2266
+ if (!cart) throw new TetherError("Shopify answered the add without a cart");
1841
2267
  commit(cart, "add");
1842
2268
  emit("added", { cart, lines });
1843
2269
  if (cfg.openCartOnAdd) openCart();
@@ -1847,9 +2273,7 @@ ${fragment}`, {
1847
2273
  /**
1848
2274
  * `updateLine(lineId, 3)` or `updateLine(lineId, { quantity, sellingPlanId, attributes })`.
1849
2275
  *
1850
- * @param {string} lineId
1851
- * @param {number | LinePatch} patch A quantity of 0 or less removes the line.
1852
- * @returns {Promise<ShopifyCart | null>}
2276
+ * @param patch A quantity of 0 or less removes the line.
1853
2277
  */
1854
2278
  updateLine(lineId, patch) {
1855
2279
  const quantity = typeof patch === "number" ? patch : patch.quantity;
@@ -1858,29 +2282,17 @@ ${fragment}`, {
1858
2282
  return enqueue(async () => commit(await updateLines([line]), "update"));
1859
2283
  },
1860
2284
  changeQuantity,
1861
- /**
1862
- * @param {ShopifyCartLine} line
1863
- * @returns {number} The quantity to show: the one asked for while an update is on its way.
1864
- */
2285
+ /** The quantity to show: the one asked for while an update is on its way. */
1865
2286
  quantityOf: pendingQuantityOf,
1866
- /**
1867
- * @param {string | string[]} lineIds
1868
- * @returns {Promise<ShopifyCart | null>}
1869
- */
1870
2287
  removeLine(lineIds) {
1871
2288
  const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
1872
2289
  return enqueue(async () => commit(await removeLines(ids), "remove"));
1873
2290
  },
1874
- /** @returns {Promise<ShopifyCart | null>} */
1875
2291
  clear() {
1876
2292
  const ids = currentLines().map((line) => line.id);
1877
2293
  if (!ids.length) return Promise.resolve(currentCart());
1878
2294
  return Cart.removeLine(ids);
1879
2295
  },
1880
- /**
1881
- * @param {string | null | undefined} note
1882
- * @returns {Promise<ShopifyCart | null>}
1883
- */
1884
2296
  setNote(note) {
1885
2297
  const variables = { note: note ?? "" };
1886
2298
  return enqueue(
@@ -1899,8 +2311,7 @@ ${fragment}`, {
1899
2311
  /**
1900
2312
  * Cart attributes, merged with the cart's (as it is when the change runs, so quick calls don't drop each other).
1901
2313
  *
1902
- * @param {Attributes} attributes The value `''` removes a key.
1903
- * @returns {Promise<ShopifyCart | null>}
2314
+ * @param attributes The value `''` removes a key.
1904
2315
  */
1905
2316
  setAttributes(attributes) {
1906
2317
  return enqueue(async () => {
@@ -1915,12 +2326,7 @@ ${fragment}`, {
1915
2326
  return commit(cart, "attributes");
1916
2327
  });
1917
2328
  },
1918
- /**
1919
- * Replaces the discount codes; `[]` removes them all.
1920
- *
1921
- * @param {string[] | string | ((current: string[]) => string[])} codes
1922
- * @returns {Promise<ShopifyCart | null>}
1923
- */
2329
+ /** Replaces the discount codes; `[]` removes them all. */
1924
2330
  setDiscountCodes(codes) {
1925
2331
  return enqueue(async () => {
1926
2332
  const variables = { discountCodes: discountCodesFrom(codes) };
@@ -1937,8 +2343,6 @@ ${fragment}`, {
1937
2343
  /**
1938
2344
  * Adds a code. A code Shopify doesn't accept is removed again, so it doesn't warn on every later change.
1939
2345
  *
1940
- * @param {string} code
1941
- * @returns {Promise<ShopifyCart | null>}
1942
2346
  * @throws {TetherError} `INVALID_DISCOUNT` (rejected) when Shopify doesn't accept the code.
1943
2347
  */
1944
2348
  async applyDiscount(code) {
@@ -1953,10 +2357,6 @@ ${fragment}`, {
1953
2357
  }
1954
2358
  throw new TetherError(`Discount code not valid: ${code}`, { code: "INVALID_DISCOUNT" });
1955
2359
  },
1956
- /**
1957
- * @param {string} code
1958
- * @returns {Promise<ShopifyCart | null>}
1959
- */
1960
2360
  removeDiscount(code) {
1961
2361
  const unwanted = code.toLowerCase();
1962
2362
  return Cart.setDiscountCodes((current) => current.filter((existing) => existing.toLowerCase() !== unwanted));
@@ -1964,9 +2364,7 @@ ${fragment}`, {
1964
2364
  /**
1965
2365
  * Sets who is buying; a country changes the cart's currency and prices to that market's.
1966
2366
  *
1967
- * @param {{ countryCode?: string, email?: string, phone?: string, customerAccessToken?: string | null }} buyerIdentity
1968
- * @param {{ create?: boolean }} [options] `create: false` forgets an expired cart instead of creating an empty one.
1969
- * @returns {Promise<ShopifyCart | null>}
2367
+ * @param options `create: false` forgets an expired cart instead of creating an empty one.
1970
2368
  */
1971
2369
  setBuyerIdentity(buyerIdentity, { create = true } = {}) {
1972
2370
  return enqueue(async () => {
@@ -1991,22 +2389,8 @@ ${fragment}`, {
1991
2389
  toggle: toggleCart
1992
2390
  };
1993
2391
 
1994
- // src/product.js
1995
- var product_exports = {};
1996
- __export(product_exports, {
1997
- ProductScope: () => ProductScope,
1998
- rememberOptionNames: () => rememberOptionNames,
1999
- scopes: () => scopes
2000
- });
2001
-
2002
- // src/product-state.js
2003
- var MINOR_UNITS_PER_UNIT = 100;
2004
- var PERCENT = 100;
2005
- var toMinorUnits = (money) => Math.round(parseFloat(money.amount) * MINOR_UNITS_PER_UNIT);
2006
- var fromMinorUnits = (minorUnits, currencyCode) => ({
2007
- amount: (minorUnits / MINOR_UNITS_PER_UNIT).toFixed(2),
2008
- currencyCode
2009
- });
2392
+ // src/product-state.ts
2393
+ var PERCENT2 = 100;
2010
2394
  var timesQuantity = (money, quantity) => fromMinorUnits(toMinorUnits(money) * quantity, money.currencyCode);
2011
2395
  var selectionOf = (variant) => Object.fromEntries(variant.selectedOptions.map((option) => [option.name, option.value]));
2012
2396
  function findVariant(product, selected) {
@@ -2021,14 +2405,18 @@ ${fragment}`, {
2021
2405
  )
2022
2406
  );
2023
2407
  }
2024
- function normalisePurchase(product, variant, wanted) {
2408
+ var purchasePlanId = (purchase) => purchase.kind === "subscription" ? purchase.planId : purchase.offeredPlanId;
2409
+ function linePurchase(purchase, variant) {
2410
+ if (purchase.kind === "subscription" && allocationFor(variant, purchase.planId)) return purchase;
2411
+ return { kind: "one-time" };
2412
+ }
2413
+ function normalisePurchase(product, variant, kind, preferredPlanId) {
2025
2414
  const plans = allPlans(product);
2026
- const known = plans.find((plan) => plan.id === wanted.planId) ?? plans[0];
2027
- if (!known) return { kind: "one-time", planId: null };
2415
+ const known = plans.find((plan) => plan.id === preferredPlanId) ?? plans[0];
2416
+ if (!known) return { kind: "one-time", offeredPlanId: null };
2028
2417
  const onVariant = allocationFor(variant, known.id) ? known : plans.find((plan) => allocationFor(variant, plan.id)) ?? known;
2029
- if (product.requiresSellingPlan || wanted.kind === "subscription")
2030
- return { kind: "subscription", planId: onVariant.id };
2031
- return { kind: "one-time", planId: onVariant.id };
2418
+ if (product.requiresSellingPlan || kind === "subscription") return { kind: "subscription", planId: onVariant.id };
2419
+ return { kind: "one-time", offeredPlanId: onVariant.id };
2032
2420
  }
2033
2421
  function initialChoice(product, { variant, sellingPlan, defaultPlan, defaultPurchase }) {
2034
2422
  const variants = product.variants.nodes;
@@ -2039,13 +2427,14 @@ ${fragment}`, {
2039
2427
  const kind = fromUrl || defaultPurchase === "subscription" ? "subscription" : "one-time";
2040
2428
  return {
2041
2429
  selected: start ? selectionOf(start) : {},
2042
- purchase: normalisePurchase(product, start, { kind, planId: offered?.id ?? null })
2430
+ purchase: normalisePurchase(product, start, kind, offered?.id ?? null)
2043
2431
  };
2044
2432
  }
2045
2433
  function showConditions(variant, lowStock, variantCount, sale, requiresSellingPlan) {
2046
2434
  const stock = variant.quantityAvailable;
2047
2435
  const low = stock != null && stock > 0 && stock <= lowStock;
2048
2436
  const onSale = sale.compareAtPrice !== null;
2437
+ const onPlan = sale.line.kind === "subscription";
2049
2438
  return {
2050
2439
  "in-stock": variant.availableForSale,
2051
2440
  "out-of-stock": !variant.availableForSale,
@@ -2060,23 +2449,24 @@ ${fragment}`, {
2060
2449
  "subscription-unavailable": !sale.subscriptionAvailable,
2061
2450
  "subscription-only": requiresSellingPlan,
2062
2451
  "one-time-available": !requiresSellingPlan,
2063
- "subscription-selected": sale.isSubscription,
2064
- "one-time-selected": !sale.isSubscription,
2065
- "has-savings": sale.isSubscription ? sale.savings.percentage > 0 : onSale
2452
+ "subscription-selected": onPlan,
2453
+ "one-time-selected": !onPlan,
2454
+ "has-savings": onPlan ? sale.savings.percentage > 0 : onSale
2066
2455
  };
2067
2456
  }
2068
2457
  function productView(product, choice, lowStock) {
2069
2458
  const exact = findVariant(product, choice.selected);
2070
2459
  const variant = exact ?? product.variants.nodes[0];
2071
2460
  if (!variant) return null;
2072
- const purchase = normalisePurchase(product, variant, choice.purchase);
2073
- const { planId } = purchase;
2461
+ const purchase = normalisePurchase(product, variant, choice.purchase.kind, purchasePlanId(choice.purchase));
2462
+ const planId = purchasePlanId(purchase);
2074
2463
  const plans = allPlans(product);
2075
- const allocation = purchase.kind === "subscription" ? allocationFor(variant, planId) : null;
2464
+ const line = linePurchase(purchase, variant);
2465
+ const allocation = line.kind === "subscription" ? allocationFor(variant, line.planId) : null;
2076
2466
  const price = allocation ? planPrice(variant, planId) ?? variant.price : variant.price;
2077
2467
  const compareAt = allocation?.priceAdjustments[0]?.compareAtPrice ?? variant.compareAtPrice;
2078
2468
  const sale = {
2079
- isSubscription: allocation !== null,
2469
+ line,
2080
2470
  subscriptionAvailable: plans.some((plan) => allocationFor(variant, plan.id)),
2081
2471
  compareAtPrice: compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount) ? compareAt : null,
2082
2472
  savings: planSavings(variant, planId)
@@ -2094,15 +2484,15 @@ ${fragment}`, {
2094
2484
  }
2095
2485
  function variantBindTexts(view, format) {
2096
2486
  const { variant, plan, price, compareAtPrice, quantity } = view;
2097
- const planId = view.purchase.planId;
2487
+ const planId = purchasePlanId(view.purchase);
2098
2488
  const forQuantity = (money) => timesQuantity(money, Math.max(1, quantity));
2099
2489
  const checkoutCharge = () => {
2100
- const charge = view.isSubscription ? planCheckoutCharge(variant, planId) ?? price : price;
2490
+ const charge = view.line.kind === "subscription" ? planCheckoutCharge(variant, planId) ?? price : price;
2101
2491
  return format.money(forQuantity(charge));
2102
2492
  };
2103
2493
  const discountPercentage = () => {
2104
2494
  if (!compareAtPrice) return "0%";
2105
- return `${Math.round((1 - parseFloat(price.amount) / parseFloat(compareAtPrice.amount)) * PERCENT)}%`;
2495
+ return `${Math.round((1 - parseFloat(price.amount) / parseFloat(compareAtPrice.amount)) * PERCENT2)}%`;
2106
2496
  };
2107
2497
  const discountAmount = () => {
2108
2498
  if (!compareAtPrice) return "";
@@ -2151,7 +2541,7 @@ ${fragment}`, {
2151
2541
  return new Map(Object.entries(texts));
2152
2542
  }
2153
2543
 
2154
- // src/product-render.js
2544
+ // src/product-render.ts
2155
2545
  var NTH_IMAGE_BIND = /^image:(\d+)$/;
2156
2546
  var METAFIELD_BIND = "metafield:";
2157
2547
  var MULTI_LINE_METAFIELD = /^(rich_text|multi_line_text)_field$/;
@@ -2172,7 +2562,7 @@ ${fragment}`, {
2172
2562
  const dot = ref.indexOf(".");
2173
2563
  const namespace = ref.slice(0, dot);
2174
2564
  const key = ref.slice(dot + 1);
2175
- const metafield = (product.metafields ?? []).find((field) => field?.namespace === namespace && field.key === key);
2565
+ const metafield = (product.metafields ?? []).find((field2) => field2?.namespace === namespace && field2.key === key);
2176
2566
  if (!metafield) {
2177
2567
  setText(el, "");
2178
2568
  return;
@@ -2205,9 +2595,9 @@ ${fragment}`, {
2205
2595
  });
2206
2596
  }
2207
2597
  function renderProductBind(el, key, product) {
2208
- const text = PRODUCT_TEXTS.get(key);
2209
- if (text) {
2210
- setText(el, text(product));
2598
+ const text2 = PRODUCT_TEXTS.get(key);
2599
+ if (text2) {
2600
+ setText(el, text2(product));
2211
2601
  return;
2212
2602
  }
2213
2603
  switch (key) {
@@ -2218,10 +2608,12 @@ ${fragment}`, {
2218
2608
  setImage(el, product.featuredImage?.url, product.featuredImage?.altText || product.title);
2219
2609
  break;
2220
2610
  case "url":
2221
- case "link":
2222
- el.setAttribute("href", productUrl2(product.handle, product.id));
2611
+ case "link": {
2612
+ const url2 = productUrl2(product.handle, product.id);
2613
+ if (url2) el.setAttribute("href", url2);
2223
2614
  if (key === "url" && !el.firstElementChild && !el.textContent.trim()) setText(el, product.title);
2224
2615
  break;
2616
+ }
2225
2617
  case "images":
2226
2618
  renderGallery(el, product);
2227
2619
  break;
@@ -2252,14 +2644,11 @@ ${fragment}`, {
2252
2644
  }
2253
2645
  repeat(container, option.optionValues, (node, value) => {
2254
2646
  node.setAttribute(attr("option-value"), value.name);
2255
- const text = qsa(node, sel("bind", "option-value"))[0];
2256
- if (text) setText(text, value.name);
2647
+ const text2 = qsa(node, sel("bind", "option-value"))[0];
2648
+ if (text2) setText(text2, value.name);
2257
2649
  else if (!node.firstElementChild) setText(node, value.name);
2258
2650
  const swatchEl = qsa(node, sel("bind", "swatch"))[0];
2259
- const swatch = (
2260
- /** @type {Swatch} */
2261
- value.swatch
2262
- );
2651
+ const { swatch } = value;
2263
2652
  if (!swatchEl || !swatch) return;
2264
2653
  if (swatch.color) swatchEl.style.backgroundColor = swatch.color;
2265
2654
  if (swatch.image?.previewImage) setImage(swatchEl, swatch.image.previewImage.url, value.name);
@@ -2306,16 +2695,7 @@ ${fragment}`, {
2306
2695
  renderPlanLists(scope);
2307
2696
  }
2308
2697
 
2309
- // src/status.js
2310
- var status_exports = {};
2311
- __export(status_exports, {
2312
- STATUS_KEYS: () => STATUS_KEYS,
2313
- boundary: () => boundary,
2314
- isNotFound: () => isNotFound,
2315
- ownedBy: () => ownedBy,
2316
- resolveProductKey: () => resolveProductKey,
2317
- setScopeStatus: () => setScopeStatus
2318
- });
2698
+ // src/status.ts
2319
2699
  var URL_SOURCE = "url:";
2320
2700
  function decodePathSegment(segment) {
2321
2701
  try {
@@ -2325,17 +2705,17 @@ ${fragment}`, {
2325
2705
  throw error;
2326
2706
  }
2327
2707
  }
2328
- function resolveProductKey(el, location = window.location, name = "product") {
2708
+ function resolveProductKey(el, location2 = window.location, name = "product") {
2329
2709
  const raw = (get(el, name) ?? "").trim();
2330
2710
  if (!raw.toLowerCase().startsWith(URL_SOURCE)) return { key: raw || null, fromUrl: false, raw };
2331
2711
  const source = raw.slice(URL_SOURCE.length).trim();
2332
2712
  let key;
2333
2713
  if (source.toLowerCase() === "path") {
2334
- const segments = location.pathname.split("/").filter(Boolean);
2714
+ const segments = location2.pathname.split("/").filter(Boolean);
2335
2715
  const segment = segments[segments.length - 1];
2336
2716
  key = segment === void 0 ? null : decodePathSegment(segment);
2337
2717
  } else {
2338
- key = new URLSearchParams(location.search).get(source || "handle");
2718
+ key = new URLSearchParams(location2.search).get(source || "handle");
2339
2719
  }
2340
2720
  return { key: key?.trim() || null, fromUrl: true, raw };
2341
2721
  }
@@ -2367,7 +2747,7 @@ ${fragment}`, {
2367
2747
  });
2368
2748
  }
2369
2749
 
2370
- // src/variant-render.js
2750
+ // src/variant-render.ts
2371
2751
  var FORMAT = { money: formatMoney2, percent: formatPercent };
2372
2752
  var isInput2 = (el) => el.tagName === "INPUT";
2373
2753
  function addButtonLabel(button) {
@@ -2400,7 +2780,7 @@ ${fragment}`, {
2400
2780
  });
2401
2781
  }
2402
2782
  function renderPurchaseStates(scope, view) {
2403
- const { variant, purchase, isSubscription } = view;
2783
+ const { variant, purchase, line } = view;
2404
2784
  scope.$(sel("purchase")).forEach((el) => {
2405
2785
  if (isSelect(el)) {
2406
2786
  el.value = purchase.kind;
@@ -2413,17 +2793,17 @@ ${fragment}`, {
2413
2793
  });
2414
2794
  scope.$(sel("plan-value")).forEach((node) => {
2415
2795
  const planId = get(node, "plan-value") ?? "";
2416
- setSelected(node, isSubscription && planId === purchase.planId);
2796
+ setSelected(node, line.kind === "subscription" && planId === line.planId);
2417
2797
  node.classList.toggle("is-unavailable", !allocationFor(variant, planId));
2418
2798
  const plan = scope.plans.find((candidate) => candidate.id === planId);
2419
2799
  const texts = planBindTexts(variant, planId, plan, FORMAT);
2420
2800
  qsa(node, sel("plan-bind")).forEach((el) => {
2421
- const text = texts.get(get(el, "plan-bind") ?? "");
2422
- if (text) setText(el, text());
2801
+ const text2 = texts.get(get(el, "plan-bind") ?? "");
2802
+ if (text2) setText(el, text2());
2423
2803
  });
2424
2804
  });
2425
2805
  selectsIn(scope, sel("plan")).forEach((select) => {
2426
- select.value = isSubscription ? purchase.planId ?? "" : "";
2806
+ select.value = line.kind === "subscription" ? line.planId : "";
2427
2807
  });
2428
2808
  }
2429
2809
  function renderConditions2(scope, view) {
@@ -2447,10 +2827,10 @@ ${fragment}`, {
2447
2827
  const subscribeText = get(button, "subscribe-text");
2448
2828
  if (!soldOutText && !subscribeText) return;
2449
2829
  const { label, original } = addButtonLabel(button);
2450
- let text = original;
2451
- if (view.isSubscription && subscribeText) text = subscribeText;
2452
- if (disabled && soldOutText) text = soldOutText;
2453
- if (button.getAttribute("aria-busy") !== "true") setText(label, text);
2830
+ let text2 = original;
2831
+ if (view.line.kind === "subscription" && subscribeText) text2 = subscribeText;
2832
+ if (disabled && soldOutText) text2 = soldOutText;
2833
+ if (button.getAttribute("aria-busy") !== "true") setText(label, text2);
2454
2834
  });
2455
2835
  }
2456
2836
  function writeVariant(scope, view) {
@@ -2463,32 +2843,29 @@ ${fragment}`, {
2463
2843
  setImage(el, variant.image?.url || product.featuredImage?.url, variant.image?.altText || product.title);
2464
2844
  return;
2465
2845
  }
2466
- const text = texts.get(key);
2467
- if (text) setText(el, text());
2846
+ const text2 = texts.get(key);
2847
+ if (text2) setText(el, text2());
2468
2848
  });
2469
2849
  renderOptionStates(scope, view);
2470
2850
  renderPurchaseStates(scope, view);
2471
2851
  renderConditions2(scope, view);
2472
- scope.root.classList.toggle("is-subscription", view.isSubscription);
2473
- scope.root.setAttribute(attr("purchase-mode"), view.isSubscription ? "subscription" : "one-time");
2852
+ scope.root.classList.toggle("is-subscription", view.line.kind === "subscription");
2853
+ scope.root.setAttribute(attr("purchase-mode"), view.line.kind);
2474
2854
  renderAddButtons(scope, view);
2475
2855
  }
2476
2856
 
2477
- // src/product-events.js
2857
+ // src/product-events.ts
2478
2858
  var productSelector = () => sel("product");
2479
2859
  var isField = (el) => el.tagName === "INPUT" || el.tagName === "SELECT" || el.tagName === "TEXTAREA";
2480
2860
  var isElement = (target) => target instanceof window.Element;
2481
2861
  function closestOwn(scope, target, selector) {
2482
- const el = (
2483
- /** @type {HTMLElement | null} */
2484
- target.closest(selector)
2485
- );
2862
+ const el = target.closest(selector);
2486
2863
  return el?.closest(productSelector()) === scope.root ? el : null;
2487
2864
  }
2488
2865
  function onAddClick(scope, button) {
2489
2866
  const { label, original } = addButtonLabel(button);
2490
2867
  const addingText = get(button, "adding-text");
2491
- void busy(button, async () => {
2868
+ busy(button, async () => {
2492
2869
  if (addingText) setText(label, addingText);
2493
2870
  try {
2494
2871
  await scope.addToCart();
@@ -2500,6 +2877,8 @@ ${fragment}`, {
2500
2877
  }
2501
2878
  }).then(() => {
2502
2879
  scope.renderVariant();
2880
+ }).catch((error) => {
2881
+ reportError(error, "add to cart render");
2503
2882
  });
2504
2883
  }
2505
2884
  function onClick(scope, event, target) {
@@ -2519,7 +2898,7 @@ ${fragment}`, {
2519
2898
  const purchase = closestOwn(scope, target, `${sel("purchase")}:not(select)`);
2520
2899
  if (purchase) {
2521
2900
  if (purchase.tagName !== "INPUT") event.preventDefault();
2522
- scope.setPurchase(get(purchase, "purchase"));
2901
+ scope.setPurchase(purchaseKind(get(purchase, "purchase")));
2523
2902
  return;
2524
2903
  }
2525
2904
  const plus = closestOwn(scope, target, sel("qty-plus"));
@@ -2535,6 +2914,7 @@ ${fragment}`, {
2535
2914
  onAddClick(scope, add);
2536
2915
  }
2537
2916
  }
2917
+ var purchaseKind = (value) => value === "subscription" ? "subscription" : "one-time";
2538
2918
  function onChange(scope, target) {
2539
2919
  const value = isField(target) ? target.value : "";
2540
2920
  if (target.matches(`select${sel("option")}`)) {
@@ -2545,7 +2925,7 @@ ${fragment}`, {
2545
2925
  if (value) scope.selectPlan(value);
2546
2926
  else scope.setPurchase("one-time");
2547
2927
  } else if (target.matches(`select${sel("purchase")}`)) {
2548
- scope.setPurchase(value);
2928
+ scope.setPurchase(purchaseKind(value));
2549
2929
  } else if (target.matches(sel("qty"))) {
2550
2930
  scope.setQuantity(value);
2551
2931
  }
@@ -2560,7 +2940,7 @@ ${fragment}`, {
2560
2940
  });
2561
2941
  }
2562
2942
 
2563
- // src/product.js
2943
+ // src/product.ts
2564
2944
  var scopes = /* @__PURE__ */ new WeakMap();
2565
2945
  var productSelector2 = () => sel("product");
2566
2946
  var knownOptionNames = /* @__PURE__ */ new Map();
@@ -2585,11 +2965,15 @@ ${fragment}`, {
2585
2965
  return isField2(el) && el.value || null;
2586
2966
  }
2587
2967
  var ProductScope = class {
2588
- /**
2589
- * @param {HTMLElement} root
2590
- * @param {Product} product
2591
- */
2592
2968
  constructor(root, product) {
2969
+ __publicField(this, "root");
2970
+ __publicField(this, "product");
2971
+ __publicField(this, "plans");
2972
+ __publicField(this, "selected");
2973
+ /** How the visitor buys; `setPurchase` and `selectPlan` change it. */
2974
+ __publicField(this, "purchase");
2975
+ __publicField(this, "variant");
2976
+ __publicField(this, "quantity");
2593
2977
  this.root = root;
2594
2978
  this.product = product;
2595
2979
  this.plans = allPlans(product);
@@ -2601,7 +2985,7 @@ ${fragment}`, {
2601
2985
  defaultPurchase: get(root, "default-purchase") || cfg.defaultPurchase
2602
2986
  });
2603
2987
  this.selected = choice.selected;
2604
- this.purchaseChoice = choice.purchase;
2988
+ this.purchase = choice.purchase;
2605
2989
  this.variant = null;
2606
2990
  this.quantity = has(root, "quantity") ? Math.max(0, parseInt(get(root, "quantity") ?? "", 10) || 0) : 1;
2607
2991
  scopes.set(root, this);
@@ -2614,11 +2998,7 @@ ${fragment}`, {
2614
2998
  notifyLayoutChanged("product");
2615
2999
  emit("product-ready", { product, scope: this }, root);
2616
3000
  }
2617
- /**
2618
- * New data for the same product (a country switch). Keeps the visitor's choice.
2619
- *
2620
- * @param {Product} product
2621
- */
3001
+ /** New data for the same product (a country switch). Keeps the visitor's choice. */
2622
3002
  update(product) {
2623
3003
  const variantId = this.variant?.id;
2624
3004
  this.product = product;
@@ -2632,17 +3012,14 @@ ${fragment}`, {
2632
3012
  notifyLayoutChanged("product");
2633
3013
  emit("product-updated", { product, scope: this }, this.root);
2634
3014
  }
2635
- /**
2636
- * @param {string} selector
2637
- * @returns {HTMLElement[]} Matches in this scope, including the root itself (a card link with tether-bind="link").
2638
- */
3015
+ /** @returns Matches in this scope, including the root itself (a card link with tether-bind="link"). */
2639
3016
  $(selector) {
2640
3017
  const inner = scoped(this.root, selector, productSelector2());
2641
3018
  return this.root.matches(selector) ? [this.root, ...inner] : inner;
2642
3019
  }
2643
3020
  /**
2644
- * @param {string | null} authored An option name in the markup.
2645
- * @returns {string} The product's option of that name, or at that position in the build's language.
3021
+ * @param authored An option name in the markup.
3022
+ * @returns The product's option of that name, or at that position in the build's language.
2646
3023
  */
2647
3024
  optName(authored) {
2648
3025
  if (!authored) return "";
@@ -2651,50 +3028,31 @@ ${fragment}`, {
2651
3028
  const index = (knownOptionNames.get(this.product.handle) ?? []).indexOf(authored);
2652
3029
  return options[index]?.name ?? authored;
2653
3030
  }
2654
- /** @returns {'one-time' | 'subscription'} */
2655
- get purchase() {
2656
- return this.purchaseChoice.kind;
2657
- }
2658
- /** @param {string} kind */
2659
- set purchase(kind) {
2660
- const wanted = kind === "subscription" ? "subscription" : "one-time";
2661
- this.purchaseChoice = normalisePurchase(this.product, this.variant, { kind: wanted, planId: this.planId });
2662
- }
2663
- /** @returns {string | null} The plan bought on, or offered while buying once. */
2664
- get planId() {
2665
- return this.purchaseChoice.planId;
2666
- }
2667
- /** @param {string | null} planId */
2668
- set planId(planId) {
2669
- this.purchaseChoice = normalisePurchase(this.product, this.variant, { kind: this.purchase, planId });
2670
- }
2671
- /** @returns {PlanInGroup | null} */
2672
3031
  get plan() {
2673
- return this.plans.find((plan) => plan.id === this.planId) ?? null;
3032
+ const planId = purchasePlanId(this.purchase);
3033
+ return this.plans.find((plan) => plan.id === planId) ?? null;
2674
3034
  }
2675
3035
  /** True when the current variant can be bought on a plan. */
2676
3036
  get subscriptionAvailable() {
2677
3037
  return this.plans.some((plan) => allocationFor(this.variant, plan.id));
2678
3038
  }
3039
+ /** True when the line is bought on a plan: subscribing, and the variant is on the plan. */
2679
3040
  get isSubscription() {
2680
- return this.purchase === "subscription" && allocationFor(this.variant, this.planId) !== null;
3041
+ return linePurchase(this.purchase, this.variant).kind === "subscription";
2681
3042
  }
2682
- /** @returns {CartLineInput | null} What would be added to the cart right now. */
3043
+ /** @returns What would be added to the cart right now. */
2683
3044
  getLine() {
2684
3045
  if (!this.variant) return null;
2685
3046
  const line = { merchandiseId: this.variant.id, quantity: this.quantity };
2686
- if (this.isSubscription && this.planId) line.sellingPlanId = this.planId;
3047
+ const bought = linePurchase(this.purchase, this.variant);
3048
+ if (bought.kind === "subscription") line.sellingPlanId = bought.planId;
2687
3049
  const attributes = this.collectAttributes();
2688
3050
  if (attributes.length) line.attributes = attributes;
2689
3051
  return line;
2690
3052
  }
2691
- /** @returns {{ key: string, value: string }[]} `tether-line-attributes` and the filled `tether-line-attribute` fields. */
3053
+ /** @returns `tether-line-attributes` and the filled `tether-line-attribute` fields. */
2692
3054
  collectAttributes() {
2693
- const json = (
2694
- /** @type {Parameters<typeof toAttributes>[0]} */
2695
- parseJSONAttr(this.root, "line-attributes")
2696
- );
2697
- const fixed = toAttributes(json);
3055
+ const fixed = attributesFromJson(parseJSONAttr(this.root, "line-attributes"));
2698
3056
  const fields = this.$(sel("line-attribute")).flatMap((el) => {
2699
3057
  const value = lineAttributeValue(el);
2700
3058
  return value === null ? [] : [{ key: get(el, "line-attribute") ?? "", value }];
@@ -2704,9 +3062,6 @@ ${fragment}`, {
2704
3062
  /**
2705
3063
  * Selects an option value. A combination no variant has moves to the closest variant with the value: the most other
2706
3064
  * choices kept, then one for sale.
2707
- *
2708
- * @param {string} name
2709
- * @param {string} value
2710
3065
  */
2711
3066
  select(name, value) {
2712
3067
  if (this.selected[name] === value) return;
@@ -2732,29 +3087,26 @@ ${fragment}`, {
2732
3087
  this.selected = next;
2733
3088
  this.renderVariant();
2734
3089
  }
2735
- /** @param {string} id A variant gid or numeric id. */
3090
+ /** @param id A variant gid or numeric id. */
2736
3091
  selectVariant(id) {
2737
3092
  const variant = this.product.variants.nodes.find((candidate) => candidate.id === GID.variant(id));
2738
3093
  if (!variant) return;
2739
3094
  this.selected = selectionOf(variant);
2740
3095
  this.renderVariant();
2741
3096
  }
2742
- /** @param {string | null} mode `subscription`, else buying once. */
2743
- setPurchase(mode) {
2744
- const kind = mode === "subscription" ? "subscription" : "one-time";
2745
- const next = normalisePurchase(this.product, this.variant, { kind, planId: this.planId });
2746
- if (next.kind === this.purchase) return;
2747
- this.purchaseChoice = next;
3097
+ setPurchase(kind) {
3098
+ const next = normalisePurchase(this.product, this.variant, kind, purchasePlanId(this.purchase));
3099
+ if (next.kind === this.purchase.kind) return;
3100
+ this.purchase = next;
2748
3101
  this.renderVariant();
2749
3102
  }
2750
- /** @param {string | null} ref A plan id, numeric id, name or option value. */
3103
+ /** @param ref A plan id, numeric id, name or option value. */
2751
3104
  selectPlan(ref) {
2752
3105
  const plan = findPlan(this.product, ref);
2753
3106
  if (!plan) return;
2754
- this.purchaseChoice = { kind: "subscription", planId: plan.id };
3107
+ this.purchase = { kind: "subscription", planId: plan.id };
2755
3108
  this.renderVariant();
2756
3109
  }
2757
- /** @param {number | string} quantity */
2758
3110
  setQuantity(quantity) {
2759
3111
  const minimum = has(this.root, "quantity") ? 0 : 1;
2760
3112
  this.quantity = Math.max(minimum, parseInt(String(quantity), 10) || 0);
@@ -2763,7 +3115,7 @@ ${fragment}`, {
2763
3115
  });
2764
3116
  this.renderVariant();
2765
3117
  }
2766
- /** @returns {Promise<unknown>} The cart after adding; `null` when the variant can't be added. */
3118
+ /** @returns The cart after adding; `null` when the variant can't be added. */
2767
3119
  async addToCart() {
2768
3120
  if (!this.variant?.availableForSale) return null;
2769
3121
  const line = this.getLine();
@@ -2789,7 +3141,7 @@ ${fragment}`, {
2789
3141
  renderVariant() {
2790
3142
  const view = productView(
2791
3143
  this.product,
2792
- { selected: this.selected, purchase: this.purchaseChoice, quantity: this.quantity },
3144
+ { selected: this.selected, purchase: this.purchase, quantity: this.quantity },
2793
3145
  cfg.lowStock
2794
3146
  );
2795
3147
  if (!view) {
@@ -2798,7 +3150,7 @@ ${fragment}`, {
2798
3150
  }
2799
3151
  this.variant = view.variant;
2800
3152
  this.selected = view.selected;
2801
- this.purchaseChoice = view.purchase;
3153
+ this.purchase = view.purchase;
2802
3154
  writeVariant(this, view);
2803
3155
  emit(
2804
3156
  "variant-changed",
@@ -2807,32 +3159,28 @@ ${fragment}`, {
2807
3159
  variant: view.variant,
2808
3160
  selected: { ...view.selected },
2809
3161
  purchase: view.purchase.kind,
2810
- sellingPlanId: view.isSubscription ? view.purchase.planId : null
3162
+ sellingPlanId: view.line.kind === "subscription" ? view.line.planId : null
2811
3163
  },
2812
3164
  this.root
2813
3165
  );
2814
3166
  }
2815
3167
  };
2816
3168
 
2817
- // src/collection.js
2818
- var collection_exports = {};
2819
- __export(collection_exports, {
2820
- ListScope: () => ListScope,
2821
- listSelector: () => listSelector
2822
- });
3169
+ // src/collection.ts
2823
3170
  var URL_QUERY_SOURCE = "url:";
2824
3171
  var listSelector = () => `${sel("collection")},${sel("products")},${sel("recommendations")}`;
2825
3172
  var isElement2 = (target) => target instanceof window.Element;
2826
3173
  var toLimit = (raw) => parseInt(raw ?? "", 10) || void 0;
2827
3174
  var toIntent = (raw) => raw === "COMPLEMENTARY" ? "COMPLEMENTARY" : "RELATED";
2828
3175
  var ListScope = class {
2829
- /** @param {HTMLElement} root */
2830
3176
  constructor(root) {
3177
+ __publicField(this, "root");
3178
+ __publicField(this, "products", []);
3179
+ __publicField(this, "pageInfo", null);
3180
+ __publicField(this, "collection", null);
3181
+ /** Counts loads, so only the latest one renders. */
3182
+ __publicField(this, "loadToken", 0);
2831
3183
  this.root = root;
2832
- this.products = [];
2833
- this.pageInfo = null;
2834
- this.collection = null;
2835
- this.loadToken = 0;
2836
3184
  setScopeStatus(root, "loading");
2837
3185
  root.addEventListener("click", (event) => {
2838
3186
  if (!isElement2(event.target)) return;
@@ -2842,31 +3190,29 @@ ${fragment}`, {
2842
3190
  void busy(more, () => this.load(true));
2843
3191
  });
2844
3192
  }
2845
- /** @param {string} selector */
2846
3193
  $(selector) {
2847
3194
  return scoped(this.root, selector, listSelector());
2848
3195
  }
2849
- /** @returns {ProductQuery} The collection or search query of the list's attributes. */
3196
+ /** The collection or search query of the list's attributes. */
2850
3197
  options() {
2851
3198
  const { root } = this;
2852
- const query = { reverse: get(root, "reverse") === "true" };
3199
+ const query2 = { reverse: get(root, "reverse") === "true" };
2853
3200
  const first = toLimit(get(root, "limit"));
2854
- if (first) query.first = first;
3201
+ if (first) query2.first = first;
2855
3202
  const sortKey = get(root, "sort");
2856
- if (sortKey) query.sortKey = sortKey;
3203
+ if (sortKey) query2.sortKey = sortKey;
2857
3204
  if (has(root, "collection")) {
2858
- query.collection = get(root, "collection") ?? "";
2859
- return query;
3205
+ query2.collection = get(root, "collection") ?? "";
3206
+ return query2;
2860
3207
  }
2861
3208
  const raw = get(root, "products") ?? "";
2862
- query.query = raw.startsWith(URL_QUERY_SOURCE) ? new URL(window.location.href).searchParams.get(raw.slice(URL_QUERY_SOURCE.length)) || "" : raw;
2863
- return query;
3209
+ query2.query = raw.startsWith(URL_QUERY_SOURCE) ? new URL(window.location.href).searchParams.get(raw.slice(URL_QUERY_SOURCE.length)) || "" : raw;
3210
+ return query2;
2864
3211
  }
2865
3212
  /**
2866
3213
  * The product of a recommendations list: a handle, `url:…` (one dynamic product page), or empty inside a product
2867
3214
  * scope for that product.
2868
3215
  *
2869
- * @returns {Promise<Product[]>}
2870
3216
  * @throws {TetherError} `NOT_FOUND` when there is no product to recommend for.
2871
3217
  */
2872
3218
  async loadRecommendations() {
@@ -2876,7 +3222,7 @@ ${fragment}`, {
2876
3222
  if (!key) throw new TetherError("No product for recommendations", { code: "NOT_FOUND" });
2877
3223
  return getRecommendations(key, toIntent(get(this.root, "intent")));
2878
3224
  }
2879
- /** @param {boolean} [more] Appends the next page instead of replacing the products. */
3225
+ /** @param more Appends the next page instead of replacing the products. */
2880
3226
  async load(more = false) {
2881
3227
  this.loadToken += 1;
2882
3228
  const token = this.loadToken;
@@ -2942,21 +3288,7 @@ ${fragment}`, {
2942
3288
  }
2943
3289
  };
2944
3290
 
2945
- // src/mounting.js
2946
- var mounting_exports = {};
2947
- __export(mounting_exports, {
2948
- hydrateFromPage: () => hydrateFromPage,
2949
- liveLists: () => liveLists,
2950
- liveProducts: () => liveProducts,
2951
- mountProduct: () => mountProduct,
2952
- mountedLists: () => mountedLists,
2953
- mountedProducts: () => mountedProducts,
2954
- pruneLive: () => pruneLive,
2955
- retryFailed: () => retryFailed,
2956
- settleMarket: () => settleMarket,
2957
- trackMarket: () => trackMarket,
2958
- within: () => within
2959
- });
3291
+ // src/mounting.ts
2960
3292
  var HYDRATE_SELECTOR = 'script[type="application/json"][data-tether-hydrate]';
2961
3293
  var mountedProducts = /* @__PURE__ */ new WeakSet();
2962
3294
  var mountedLists = /* @__PURE__ */ new WeakSet();
@@ -2972,10 +3304,7 @@ ${fragment}`, {
2972
3304
  });
2973
3305
  }
2974
3306
  function within(root, selector) {
2975
- const self = root.matches?.(selector) ? [
2976
- /** @type {HTMLElement} */
2977
- root
2978
- ] : [];
3307
+ const self = root.matches?.(selector) ? [root] : [];
2979
3308
  return [...self, ...qsa(root, selector)];
2980
3309
  }
2981
3310
  function applyPageTitle(el, product) {
@@ -3056,12 +3385,8 @@ ${fragment}`, {
3056
3385
  function flushRefresh() {
3057
3386
  const keys = [...refreshQueue];
3058
3387
  refreshQueue = /* @__PURE__ */ new Set();
3059
- refreshProducts(keys).forEach((promise, index) => {
3060
- const key = (
3061
- /** @type {string} */
3062
- keys[index]
3063
- );
3064
- promise.then((fresh) => {
3388
+ keys.forEach((key) => {
3389
+ refreshProduct(key).then((fresh) => {
3065
3390
  applyRefresh(key, fresh);
3066
3391
  }).catch((error) => {
3067
3392
  reportError(error, `refresh ${key}`);
@@ -3086,11 +3411,19 @@ ${fragment}`, {
3086
3411
  document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
3087
3412
  });
3088
3413
  }
3089
- var isUsable = (product) => Boolean(product?.id && product.handle);
3090
- var sameCode = (a, b) => (a ?? "").toUpperCase() === (b ?? "").toUpperCase();
3414
+ function toHydratePayload(json) {
3415
+ if (!isRecord(json)) return { country: void 0, language: void 0, products: [] };
3416
+ const { products } = json;
3417
+ const entries = Array.isArray(products) ? products : [];
3418
+ return {
3419
+ country: toCountry(json["country"]) ?? void 0,
3420
+ language: toLanguage(json["language"]) ?? void 0,
3421
+ products: entries.filter(isProductShape)
3422
+ };
3423
+ }
3091
3424
  var readScripts = /* @__PURE__ */ new WeakSet();
3092
3425
  function hydrateFromPage(root = document) {
3093
- qsa(root.ownerDocument ?? root, HYDRATE_SELECTOR).forEach((script) => {
3426
+ within(root, HYDRATE_SELECTOR).forEach((script) => {
3094
3427
  if (readScripts.has(script)) return;
3095
3428
  readScripts.add(script);
3096
3429
  let json;
@@ -3100,14 +3433,10 @@ ${fragment}`, {
3100
3433
  warn("bad data-tether-hydrate", error);
3101
3434
  return;
3102
3435
  }
3103
- const { country, language, products } = (
3104
- /** @type {HydratePayload | null} */
3105
- json ?? {}
3106
- );
3107
- const usable = Array.isArray(products) ? products.filter(isUsable) : [];
3108
- rememberOptionNames(usable);
3109
- if (sameCode(country, cfg.country) && sameCode(language, cfg.language)) {
3110
- hydrateProducts(usable);
3436
+ const { country, language, products } = toHydratePayload(json);
3437
+ rememberOptionNames(products);
3438
+ if (country === cfg.country && language === cfg.language) {
3439
+ hydrateProducts(products);
3111
3440
  return;
3112
3441
  }
3113
3442
  log("hydration skipped: other market");
@@ -3118,53 +3447,108 @@ ${fragment}`, {
3118
3447
  });
3119
3448
  }
3120
3449
 
3121
- // src/lazy-iife.js
3122
- var { cfg: cfg2 } = config_exports;
3450
+ // src/part-integrity.ts
3451
+ var PART_INTEGRITY = { "tether.diagnose.js": "sha384-OKLlZJrSZ5IF9SOjKnEe485fi5xZlb4oLLvSfGjAssABzqd3h9JH1yA/hDczhkjJ", "tether.markets.js": "sha384-QVvTGiZJKx2hOPIpQPrGycWnKVTfDMEJ8z9heHIYSqib+6WyUoTmzKf2oBiFE3le", "tether.search.js": "sha384-P63pnKFjWTIVS8kEbBr4sfZZI91u1tImDTe+y50l2pdOM+W/KQsMymB6YRH801Dc", "tether.extras.js": "sha384-DJwYPZWf7o63ooQuDRvvhLrl5quZwhWO1YQ9RpySNQKjlDIS3uCwxDpDfZYiEjla", "tether.account.js": "sha384-q3UfPH5vDMbKm05wkvEPTDoA/aaIPmRQBDrqmXA4cNF4VFZAo4dgt61TmrNM69mi", "tether.account-pages.js": "sha384-YiSgDwM6qeHXJBcbWLRuk50h2ntAamNadS0MrZByc3cl5PKXZO297l38zYWm0lLe", "tether.analytics.js": "sha384-I93erLFdRG9uy3t+mnaDk/MgECoYOSG3q+9TfIQ39NROWZLvFjYrpHOAnBfsXaJc" };
3452
+
3453
+ // src/lazy-iife.ts
3123
3454
  var coreModules = {
3124
- "config.js": config_exports,
3125
- "client.js": client_exports,
3126
- "dom.js": dom_exports,
3127
- "fragments.js": fragments_exports,
3128
- "loader.js": loader_exports,
3129
- "selling-plans.js": selling_plans_exports,
3130
- "status.js": status_exports,
3131
- "cart.js": cart_exports,
3132
- "product.js": product_exports,
3133
- "collection.js": collection_exports,
3134
- "mounting.js": mounting_exports,
3135
- "market-codes.js": market_codes_exports,
3136
- "shopify-values.js": shopify_values_exports,
3137
- "store-html.js": store_html_exports
3455
+ get "config.ts"() {
3456
+ return [cfg, isConfigured, log, MARKET_SOURCES, parseMarkets, reportError, warn];
3457
+ },
3458
+ get "client.ts"() {
3459
+ return [endpoint, gql, orNull, shaped, TetherError];
3460
+ },
3461
+ get "dom.ts"() {
3462
+ return [
3463
+ ANALYTICS_EVENTS,
3464
+ attr,
3465
+ attributesFromJson,
3466
+ emit,
3467
+ formatDate,
3468
+ formatMoney2,
3469
+ get,
3470
+ has,
3471
+ hasTemplate,
3472
+ isOwnEvent,
3473
+ parseJSONAttr,
3474
+ productUrl2,
3475
+ qsa,
3476
+ repeat,
3477
+ sel,
3478
+ setImage,
3479
+ setText,
3480
+ show
3481
+ ];
3482
+ },
3483
+ get "loader.ts"() {
3484
+ return [clearProductCache, getProduct];
3485
+ },
3486
+ get "selling-plans.ts"() {
3487
+ return [allPlans];
3488
+ },
3489
+ get "status.ts"() {
3490
+ return [boundary, resolveProductKey];
3491
+ },
3492
+ get "cart.ts"() {
3493
+ return [Cart];
3494
+ },
3495
+ get "product.ts"() {
3496
+ return [scopes];
3497
+ },
3498
+ get "collection.ts"() {
3499
+ return [listSelector];
3500
+ },
3501
+ get "mounting.ts"() {
3502
+ return [liveLists, liveProducts, mountedProducts, mountProduct, pruneLive, within];
3503
+ },
3504
+ get "market-codes.ts"() {
3505
+ return [toCountry, toLanguage];
3506
+ },
3507
+ get "shopify-values.ts"() {
3508
+ return [isHttpsUrl];
3509
+ },
3510
+ // Parts that load other parts (account → account-pages) use this same loader.
3511
+ get "lazy.ts"() {
3512
+ return [loadPart];
3513
+ }
3138
3514
  };
3139
3515
  var MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
3140
3516
  var coreScript = typeof document === "undefined" ? "" : document.currentScript?.getAttribute("src") ?? "";
3141
3517
  var coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : "";
3142
3518
  var parts = /* @__PURE__ */ new Map();
3143
3519
  function partsFolder() {
3144
- if (!cfg2.partsUrl) return coreScriptUrl.replace(/[^/]*$/, "");
3145
- const folder = new URL(cfg2.partsUrl, document.baseURI);
3146
- if (folder.protocol !== "https:" && folder.origin !== window.location.origin) {
3147
- throw new Error(`partsUrl must be https or on this site: ${folder.href}`);
3148
- }
3149
- return folder.href;
3520
+ if (cfg.partsUrl) return new URL(cfg.partsUrl, document.baseURI).href;
3521
+ if (!coreScriptUrl) return null;
3522
+ return coreScriptUrl.replace(/[^/]*$/, "");
3523
+ }
3524
+ function applyIntegrity(script, integrity) {
3525
+ script.integrity = integrity;
3526
+ if (new URL(script.src).origin !== window.location.origin) script.crossOrigin = "anonymous";
3150
3527
  }
3151
- function loadScript(name, url) {
3528
+ function loadScript(name, folder, file, integrity) {
3152
3529
  return new Promise((resolve, reject) => {
3530
+ const url2 = `${folder.replace(/\/?$/, "/")}${file}`;
3153
3531
  const script = document.createElement("script");
3154
- script.src = url;
3532
+ script.src = url2;
3155
3533
  script.async = true;
3534
+ applyIntegrity(script, integrity);
3156
3535
  script.onload = () => {
3157
- const { Tether } = (
3158
- /** @type {Window & { Tether?: { __parts?: Record<string, unknown> } }} */
3159
- window
3160
- );
3161
- const part = Tether?.__parts?.[name];
3162
- if (part) resolve(part);
3163
- else reject(new Error(`${url} did not register`));
3536
+ const { Tether } = window;
3537
+ const part = Tether?.__parts[name];
3538
+ if (part) {
3539
+ resolve(part);
3540
+ return;
3541
+ }
3542
+ parts.delete(name);
3543
+ reject(new Error(`${url2} did not register`));
3164
3544
  };
3165
3545
  script.onerror = () => {
3166
3546
  parts.delete(name);
3167
- reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`));
3547
+ reject(
3548
+ new Error(
3549
+ `Could not load ${url2}, or it is not the file built with this Tether script. Host the same release's files next to the Tether script or at partsUrl.`
3550
+ )
3551
+ );
3168
3552
  };
3169
3553
  document.head.appendChild(script);
3170
3554
  });
@@ -3172,39 +3556,30 @@ ${fragment}`, {
3172
3556
  function loadPart(name) {
3173
3557
  let loading = parts.get(name);
3174
3558
  if (!loading) {
3175
- let folder;
3176
- try {
3177
- folder = partsFolder();
3178
- } catch (error) {
3179
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
3559
+ const folder = partsFolder();
3560
+ if (folder === null) {
3561
+ return Promise.reject(
3562
+ new Error(`The Tether script's own location is unknown, so its ${name} part can't be found: set partsUrl.`)
3563
+ );
3180
3564
  }
3181
3565
  const suffix = MINIFIED_SCRIPT.test(coreScriptUrl) || !coreScriptUrl ? ".min" : "";
3182
- loading = loadScript(name, `${folder.replace(/\/?$/, "/")}tether.${name}${suffix}.js`);
3566
+ const file = `tether.${name}${suffix}.js`;
3567
+ const integrity = PART_INTEGRITY[file];
3568
+ if (integrity === void 0) {
3569
+ return Promise.reject(new Error(`This Tether script was built without ${file}, so it can't load that part.`));
3570
+ }
3571
+ loading = loadScript(name, folder, file, integrity);
3183
3572
  parts.set(name, loading);
3184
3573
  }
3185
- return (
3186
- /** @type {Promise<Parts[Name]>} */
3187
- loading
3188
- );
3574
+ return loading;
3189
3575
  }
3190
- coreModules["lazy.js"] = { loadPart, coreModules };
3191
3576
 
3192
- // src/markets.js
3577
+ // src/markets.ts
3193
3578
  var storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
3194
- function localStorageIfAllowed() {
3195
- if (typeof window === "undefined") return null;
3579
+ var pageStorage = { getItem: (key) => getStored(key) };
3580
+ function parseJsonOrNull(text2) {
3196
3581
  try {
3197
- return window.localStorage;
3198
- } catch {
3199
- return null;
3200
- }
3201
- }
3202
- function parseJsonOrNull(text) {
3203
- try {
3204
- return (
3205
- /** @type {unknown} */
3206
- JSON.parse(text)
3207
- );
3582
+ return JSON.parse(text2);
3208
3583
  } catch (error) {
3209
3584
  if (error instanceof SyntaxError) return null;
3210
3585
  throw error;
@@ -3214,22 +3589,19 @@ ${fragment}`, {
3214
3589
  if (typeof value !== "object" || value === null) return null;
3215
3590
  const country = toCountry(Reflect.get(value, "country"));
3216
3591
  if (!country) return null;
3217
- const locale = (
3218
- /** @type {unknown} */
3219
- Reflect.get(value, "locale")
3220
- );
3592
+ const locale = Reflect.get(value, "locale");
3221
3593
  return {
3222
3594
  country,
3223
3595
  language: toLanguage(Reflect.get(value, "language")) ?? void 0,
3224
3596
  locale: typeof locale === "string" && locale ? locale : void 0
3225
3597
  };
3226
3598
  }
3227
- function readSavedMarket(storage = localStorageIfAllowed()) {
3228
- const text = storage?.getItem(storageKey());
3229
- return text ? toMarket(parseJsonOrNull(text)) : null;
3599
+ function readSavedMarket(storage = pageStorage) {
3600
+ const text2 = storage.getItem(storageKey());
3601
+ return text2 ? toMarket(parseJsonOrNull(text2)) : null;
3230
3602
  }
3231
3603
 
3232
- // src/page-market.js
3604
+ // src/page-market.ts
3233
3605
  var configuredMarket = { country: void 0, language: void 0, locale: void 0 };
3234
3606
  var lastPageHadOwnMarket = false;
3235
3607
  function pageMarket() {
@@ -3266,21 +3638,33 @@ ${fragment}`, {
3266
3638
  if (market.locale && !overrides.locale) cfg.locale = market.locale;
3267
3639
  log("market", market);
3268
3640
  }
3269
- var sameCode2 = (a, b) => (a ?? "").toUpperCase() === (b ?? "").toUpperCase();
3641
+ var sameCode = (a, b) => (a ?? "").toUpperCase() === (b ?? "").toUpperCase();
3270
3642
  function marketForArrival() {
3271
3643
  const page = pageMarket();
3272
3644
  const leftOwnMarket = page === null && lastPageHadOwnMarket;
3273
3645
  lastPageHadOwnMarket = page !== null;
3274
3646
  const target = page ?? (leftOwnMarket ? readSavedMarket() ?? configuredMarket : null);
3275
3647
  if (!target?.country) return null;
3276
- if (sameCode2(target.country, cfg.country) && sameCode2(target.language, cfg.language)) return null;
3648
+ if (sameCode(target.country, cfg.country) && sameCode(target.language, cfg.language)) return null;
3277
3649
  return { country: target.country, language: target.language, locale: target.locale };
3278
3650
  }
3279
3651
  function setCountry(code, options) {
3280
3652
  return loadPart("markets").then((part) => part.setCountry(code, options));
3281
3653
  }
3282
3654
 
3283
- // src/account-api.js
3655
+ // src/account-marks.ts
3656
+ var hintKey = () => `${cfg.prefix}:account-hint:${cfg.domain}`;
3657
+ function removeFromBothAreas(key) {
3658
+ removeStored(key, "sessionStorage");
3659
+ removeStored(key, "localStorage");
3660
+ }
3661
+ function forgetBrowserModeTokens() {
3662
+ removeFromBothAreas(`${cfg.prefix}:account:${cfg.domain}`);
3663
+ removeFromBothAreas(`${cfg.prefix}:account-login:${cfg.domain}`);
3664
+ }
3665
+ var isLoginOutcome = (value) => value === "in" || value === "expired" || value === "error";
3666
+
3667
+ // src/account-api.ts
3284
3668
  var ACCOUNT_ATTRIBUTES = [
3285
3669
  "account-login",
3286
3670
  "account-bind",
@@ -3293,20 +3677,12 @@ ${fragment}`, {
3293
3677
  "address-form"
3294
3678
  ];
3295
3679
  var accountSelector = () => [...ACCOUNT_ATTRIBUTES.map((name) => `[${attr(name)}]`), `[${attr("show")}^="logged-"]`].join(",");
3296
- function hasSavedSession() {
3297
- const sessionKey = `${cfg.prefix}:account:${cfg.domain}`;
3298
- const hintKey = `${cfg.prefix}:account-hint:${cfg.domain}`;
3299
- try {
3300
- return Boolean(
3301
- sessionStorage.getItem(sessionKey) || localStorage.getItem(sessionKey) || localStorage.getItem(hintKey)
3302
- );
3303
- } catch {
3304
- return false;
3305
- }
3680
+ function hasLoginHint() {
3681
+ return Boolean(getStored(hintKey()));
3306
3682
  }
3307
3683
  function mountAccountIfUsed(root) {
3308
- if (!cfg.account?.clientId && !cfg.account?.server) return;
3309
- const needed = within(root, accountSelector()).length > 0 || /[?&]code=/.test(window.location.search) || hasSavedSession();
3684
+ if (!cfg.account) return;
3685
+ const needed = within(root, accountSelector()).length > 0 || isLoginOutcome(new URLSearchParams(window.location.search).get("account")) || hasLoginHint();
3310
3686
  if (!needed) return;
3311
3687
  loadPart("account").then((part) => part.mountAccount(root)).catch((error) => {
3312
3688
  reportError(error, "account");
@@ -3314,22 +3690,14 @@ ${fragment}`, {
3314
3690
  }
3315
3691
  function fromPart(name) {
3316
3692
  return (...args) => loadPart("account").then((part) => {
3317
- const call = (
3318
- /**
3319
- * @type {(...callArgs: Parameters<AccountPart[Name]>) => AccountResult<Name> | PromiseLike<AccountResult<Name>>}
3320
- */
3321
- part[name]
3322
- );
3693
+ const call = part[name];
3323
3694
  return call(...args);
3324
3695
  });
3325
3696
  }
3326
3697
  var accountApi = {
3327
3698
  login: fromPart("login"),
3328
3699
  logout: fromPart("logout"),
3329
- handleCallback: fromPart("handleCallback"),
3330
- getToken: fromPart("getToken"),
3331
3700
  isLoggedIn: fromPart("isLoggedIn"),
3332
- query: fromPart("query"),
3333
3701
  customer: fromPart("customer"),
3334
3702
  orders: fromPart("orders"),
3335
3703
  order: fromPart("order"),
@@ -3344,7 +3712,7 @@ ${fragment}`, {
3344
3712
  mountAccount: fromPart("mountAccount")
3345
3713
  };
3346
3714
 
3347
- // src/controls/cart-controls.js
3715
+ // src/controls/cart-controls.ts
3348
3716
  var lineKeyAttribute = () => `data-${cfg.prefix}-key`;
3349
3717
  var isElement3 = (target) => target instanceof window.Element;
3350
3718
  function lineOf(target) {
@@ -3441,7 +3809,7 @@ ${fragment}`, {
3441
3809
  document.addEventListener("keydown", onKeydown);
3442
3810
  }
3443
3811
 
3444
- // src/controls/discount.js
3812
+ // src/controls/discount.ts
3445
3813
  var isElement4 = (target) => target instanceof window.Element;
3446
3814
  function codeInputOf(form) {
3447
3815
  const input = form.querySelector(sel("discount-input")) ?? form.querySelector("input");
@@ -3499,7 +3867,7 @@ ${fragment}`, {
3499
3867
  document.addEventListener("click", onClick3);
3500
3868
  }
3501
3869
 
3502
- // src/controls/extras.js
3870
+ // src/controls/extras.ts
3503
3871
  var addGroup = (button) => loadPart("extras").then((part) => part.addGroup(button));
3504
3872
  var reportExtrasError = (error) => {
3505
3873
  reportError(error, "extras");
@@ -3518,6 +3886,7 @@ ${fragment}`, {
3518
3886
  }).catch(reportExtrasError);
3519
3887
  }
3520
3888
  function onVariantChanged(event) {
3889
+ if (!isOwnEvent(event, "variant-changed")) return;
3521
3890
  const { target } = event;
3522
3891
  if (!(target instanceof window.Element)) return;
3523
3892
  if (!cfg.variantUrl && !target.hasAttribute(attr("variant-url"))) return;
@@ -3528,14 +3897,10 @@ ${fragment}`, {
3528
3897
  function bindExtrasControls() {
3529
3898
  document.addEventListener("click", onClick4);
3530
3899
  window.addEventListener("storage", onStorage);
3531
- document.addEventListener(
3532
- `${cfg.prefix}:variant-changed`,
3533
- /** @type {EventListener} */
3534
- onVariantChanged
3535
- );
3900
+ document.addEventListener(`${cfg.prefix}:variant-changed`, onVariantChanged);
3536
3901
  }
3537
3902
 
3538
- // src/controls/navigation.js
3903
+ // src/controls/navigation.ts
3539
3904
  var mountedBody = null;
3540
3905
  function rememberDocumentMount() {
3541
3906
  mountedBody = document.body;
@@ -3556,7 +3921,9 @@ ${fragment}`, {
3556
3921
  }
3557
3922
  function bindAstroRouter(hooks) {
3558
3923
  document.addEventListener("astro:page-load", () => {
3559
- void onPageLoad(hooks);
3924
+ onPageLoad(hooks).catch((error) => {
3925
+ reportError(error, "page mount");
3926
+ });
3560
3927
  });
3561
3928
  document.addEventListener("astro:before-swap", () => {
3562
3929
  mountedBody = null;
@@ -3583,11 +3950,13 @@ ${fragment}`, {
3583
3950
  window.addEventListener("online", onOnline);
3584
3951
  }
3585
3952
 
3586
- // src/index.js
3587
- var VERSION = "0.6.8";
3588
- var ANALYTICS_EVENTS = ["product-ready", "list-ready", "cart-updated", "cart-toggled", "checkout", "search"];
3953
+ // src/index.ts
3954
+ var VERSION = "0.7.0";
3589
3955
  var OBSERVE_DEBOUNCE_MS = 50;
3590
- var pageGlobals = () => window;
3956
+ function pageConfig() {
3957
+ const config = pageGlobal("TETHER_CONFIG");
3958
+ return typeof config === "object" && config !== null ? Object.fromEntries(Object.entries(config)) : {};
3959
+ }
3591
3960
  var initPromise = null;
3592
3961
  var observer = null;
3593
3962
  var analyticsQueue = null;
@@ -3601,10 +3970,7 @@ ${fragment}`, {
3601
3970
  const list = new ListScope(el);
3602
3971
  liveLists.add(list);
3603
3972
  el.addEventListener(`${cfg.prefix}:list-rendered`, (event) => {
3604
- mount(
3605
- /** @type {CustomEvent<{ container: ParentNode }>} */
3606
- event.detail.container
3607
- );
3973
+ if (isOwnEvent(event, "list-rendered")) mount(event.detail.container);
3608
3974
  });
3609
3975
  void trackMarket(list.load());
3610
3976
  });
@@ -3625,21 +3991,39 @@ ${fragment}`, {
3625
3991
  });
3626
3992
  }
3627
3993
  }
3628
- function mount(root = document) {
3629
- if (!isConfigured()) return 0;
3630
- if (root === document) rememberDocumentMount();
3631
- pruneLive();
3632
- hydrateFromPage(root.ownerDocument ?? document);
3994
+ function mountWithin(root) {
3633
3995
  let count = mountLists(root);
3634
3996
  within(root, sel("product")).forEach((el) => {
3635
3997
  if (mountProduct(el)) count++;
3636
3998
  });
3637
3999
  mountParts(root);
4000
+ return count;
4001
+ }
4002
+ function mount(root = document) {
4003
+ if (!isConfigured()) return 0;
4004
+ if (root === document) rememberDocumentMount();
4005
+ pruneLive();
4006
+ hydrateFromPage(root.ownerDocument ?? document);
4007
+ const count = mountWithin(root);
3638
4008
  settleMarket();
3639
4009
  Cart.render();
3640
4010
  log(`mounted ${count} scope(s)`, root);
3641
4011
  return count;
3642
4012
  }
4013
+ function mountAdded(added) {
4014
+ const roots = added.filter(
4015
+ (root) => root.isConnected && !added.some((other) => other !== root && other.contains(root))
4016
+ );
4017
+ if (!roots.length || !isConfigured()) return;
4018
+ pruneLive();
4019
+ roots.forEach((root) => {
4020
+ hydrateFromPage(root);
4021
+ });
4022
+ const count = roots.reduce((sum, root) => sum + mountWithin(root), 0);
4023
+ settleMarket();
4024
+ Cart.render();
4025
+ log(`mounted ${count} scope(s) in ${roots.length} added element(s)`);
4026
+ }
3643
4027
  function unmount() {
3644
4028
  Cart.close();
3645
4029
  }
@@ -3661,13 +4045,13 @@ ${fragment}`, {
3661
4045
  ANALYTICS_EVENTS.forEach((name) => {
3662
4046
  document.addEventListener(`${cfg.prefix}:${name}`, (event) => queue.push(event));
3663
4047
  });
3664
- document.addEventListener("astro:page-load", () => queue.push(new CustomEvent(`${cfg.prefix}:page`)));
4048
+ document.addEventListener("astro:page-load", (event) => queue.push(event));
3665
4049
  loadPart("analytics").then((part) => part.startAnalytics(queue)).catch((error) => {
3666
4050
  reportError(error, "analytics");
3667
4051
  });
3668
4052
  }
3669
4053
  async function cartInPageMarket(cart) {
3670
- const country = cfg.country?.toUpperCase();
4054
+ const country = cfg.country;
3671
4055
  const cartCountry = cart?.buyerIdentity.countryCode;
3672
4056
  if (!cart || !country || !cartCountry || cartCountry === country) return cart;
3673
4057
  try {
@@ -3679,12 +4063,13 @@ ${fragment}`, {
3679
4063
  }
3680
4064
  function init2(overrides = {}) {
3681
4065
  if (initPromise && !Object.keys(overrides).length) return initPromise;
3682
- configure({ ...pageGlobals().TETHER_CONFIG, ...overrides });
4066
+ configure({ ...pageConfig(), ...overrides });
3683
4067
  if (!isConfigured()) {
3684
4068
  warn("TETHER_CONFIG is missing domain or token. Tether is idle.");
3685
4069
  initPromise = Promise.resolve(null);
3686
4070
  return initPromise;
3687
4071
  }
4072
+ forgetBrowserModeTokens();
3688
4073
  bindControls();
3689
4074
  if (cfg.observe) startObserver();
3690
4075
  if (cfg.analytics && !analyticsQueue) startAnalyticsQueue();
@@ -3706,10 +4091,9 @@ ${fragment}`, {
3706
4091
  });
3707
4092
  clearTimeout(timer);
3708
4093
  timer = setTimeout(() => {
3709
- queued.forEach((node) => {
3710
- if (node.isConnected) mount(node);
3711
- });
4094
+ const added = [...queued];
3712
4095
  queued.clear();
4096
+ mountAdded(added);
3713
4097
  }, OBSERVE_DEBOUNCE_MS);
3714
4098
  });
3715
4099
  observer.observe(document.body, { childList: true, subtree: true });
@@ -3718,32 +4102,28 @@ ${fragment}`, {
3718
4102
  var getLocalization = () => loadPart("markets").then((part) => part.getLocalization());
3719
4103
  var diagnose = (options) => loadPart("diagnose").then((part) => part.diagnose({ ...options, version: VERSION }));
3720
4104
  function on(name, listener) {
4105
+ const type = `${cfg.prefix}:${name}`;
3721
4106
  const handler = (event) => {
3722
- const custom = (
3723
- /** @type {CustomEvent} Tether's events are CustomEvents. */
3724
- event
3725
- );
3726
- listener(custom.detail, custom);
4107
+ if (isOwnEvent(event, name)) listener(event.detail, event);
3727
4108
  };
3728
- document.addEventListener(`${cfg.prefix}:${name}`, handler);
4109
+ document.addEventListener(type, handler);
3729
4110
  return () => {
3730
- document.removeEventListener(`${cfg.prefix}:${name}`, handler);
4111
+ document.removeEventListener(type, handler);
3731
4112
  };
3732
4113
  }
3733
- var api = {
4114
+ var publicApi = {
3734
4115
  VERSION,
3735
4116
  init: init2,
3736
4117
  mount,
3737
4118
  unmount,
3738
- /** @returns {TetherConfig} */
4119
+ /** The live config, read-only. */
3739
4120
  get config() {
3740
- return cfg;
4121
+ return configView;
3741
4122
  },
3742
- /** @returns {Promise<TetherApi | null>} */
3743
4123
  get ready() {
3744
4124
  return initPromise ?? Promise.resolve(null);
3745
4125
  },
3746
- query: gql,
4126
+ query,
3747
4127
  withContext,
3748
4128
  cart: Cart,
3749
4129
  getProduct,
@@ -3764,19 +4144,15 @@ ${fragment}`, {
3764
4144
  detectMarket,
3765
4145
  getLocalization,
3766
4146
  diagnose,
3767
- __core: coreModules,
3768
- // The `<script>` build's parts register here when their file has loaded.
3769
- __parts: (
3770
- /** @type {Record<string, unknown>} */
3771
- {}
3772
- ),
3773
4147
  on
3774
4148
  };
4149
+ var internals = { __core: coreModules, __parts: {} };
4150
+ var api = Object.assign(publicApi, internals);
3775
4151
  var index_default = api;
3776
4152
  function autoStart() {
3777
- const globals = pageGlobals();
3778
- globals.Tether = api;
3779
- if (!globals.TETHER_CONFIG || globals.TETHER_CONFIG.autoMount === false || globals.TETHER_INTEGRATION) return;
4153
+ Reflect.set(window, "Tether", api);
4154
+ const config = pageGlobal("TETHER_CONFIG");
4155
+ if (!config || field(config, "autoMount") === false || pageGlobal("TETHER_INTEGRATION")) return;
3780
4156
  const start = () => {
3781
4157
  init2().then(() => mount(document)).catch((error) => {
3782
4158
  reportError(error, "init");