@occluze/tether 0.6.8 → 0.7.1

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 (215) hide show
  1. package/README.md +28 -28
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
  4. package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
  5. package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
  6. package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
  7. package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
  8. package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
  9. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  10. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  11. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  12. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  13. package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
  14. package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
  15. package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
  16. package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
  17. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  18. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  19. package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
  20. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  21. package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
  22. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/presets/barba.esm.js +19 -23
  26. package/dist/tether.account-pages.js +332 -367
  27. package/dist/tether.account-pages.min.js +1 -1
  28. package/dist/tether.account.js +253 -492
  29. package/dist/tether.account.min.js +1 -1
  30. package/dist/tether.analytics.js +82 -89
  31. package/dist/tether.analytics.min.js +1 -1
  32. package/dist/tether.diagnose.js +52 -34
  33. package/dist/tether.diagnose.min.js +2 -2
  34. package/dist/tether.extras.js +42 -37
  35. package/dist/tether.extras.min.js +1 -1
  36. package/dist/tether.js +1361 -935
  37. package/dist/tether.markets.js +89 -91
  38. package/dist/tether.markets.min.js +1 -1
  39. package/dist/tether.min.js +0 -0
  40. package/dist/tether.min.js.map +4 -4
  41. package/dist/tether.search.js +60 -69
  42. package/dist/tether.search.min.js +1 -1
  43. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  44. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  45. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  46. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  47. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  48. package/dist/types/server/src/shared/json.d.ts +25 -0
  49. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  50. package/dist/types/src/account/addresses-view.d.ts +5 -0
  51. package/dist/types/src/account/answers.d.ts +40 -0
  52. package/dist/types/src/account/markup.d.ts +19 -0
  53. package/dist/types/src/account/operations.d.ts +41 -0
  54. package/dist/types/src/account/order-lines.d.ts +13 -0
  55. package/dist/types/src/account/order-list.d.ts +12 -0
  56. package/dist/types/src/account/order-page.d.ts +6 -0
  57. package/dist/types/src/account/page-answers.d.ts +182 -0
  58. package/dist/types/src/account/page-events.d.ts +2 -0
  59. package/dist/types/src/account/server-client.d.ts +60 -0
  60. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  61. package/dist/types/src/account-api.d.ts +18 -0
  62. package/dist/types/src/account-marks.d.ts +15 -0
  63. package/dist/types/src/account-pages.d.ts +8 -0
  64. package/dist/types/src/account.d.ts +64 -0
  65. package/dist/types/src/amounts.d.ts +12 -0
  66. package/dist/types/src/analytics-shopify.d.ts +41 -0
  67. package/dist/types/src/analytics.d.ts +2 -0
  68. package/dist/types/src/api-version.d.ts +28 -0
  69. package/dist/types/src/astro.d.ts +42 -0
  70. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  71. package/dist/types/src/cart/checkout.d.ts +16 -0
  72. package/dist/types/src/cart/drawer.d.ts +18 -0
  73. package/dist/types/src/cart/mutations.d.ts +85 -0
  74. package/dist/types/src/cart/render.d.ts +17 -0
  75. package/dist/types/src/cart/store.d.ts +185 -0
  76. package/dist/types/src/cart.d.ts +106 -0
  77. package/dist/types/src/client.d.ts +134 -0
  78. package/dist/types/src/collection.d.ts +25 -0
  79. package/dist/types/src/config-sections.d.ts +40 -0
  80. package/dist/types/src/config.d.ts +190 -0
  81. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  82. package/dist/types/src/controls/discount.d.ts +2 -0
  83. package/dist/types/src/controls/extras.d.ts +5 -0
  84. package/dist/types/src/controls/navigation.d.ts +12 -0
  85. package/dist/types/src/diagnose-markup.d.ts +19 -0
  86. package/dist/types/src/diagnose.d.ts +60 -0
  87. package/dist/types/src/dom.d.ts +75 -0
  88. package/dist/types/src/extras.d.ts +21 -0
  89. package/dist/types/src/fragments.d.ts +158 -0
  90. package/dist/types/src/globals.d.ts +15 -0
  91. package/dist/types/src/images.d.ts +20 -0
  92. package/dist/types/src/index.d.ts +167 -0
  93. package/dist/types/src/initial.d.ts +56 -0
  94. package/dist/types/src/lazy.d.ts +28 -0
  95. package/dist/types/src/loader.d.ts +84 -0
  96. package/dist/types/src/market-codes.d.ts +6 -0
  97. package/dist/types/src/markets-ui.d.ts +32 -0
  98. package/dist/types/src/markets.d.ts +72 -0
  99. package/dist/types/src/markup.d.ts +131 -0
  100. package/dist/types/src/mounting.d.ts +46 -0
  101. package/dist/types/src/page-market.d.ts +27 -0
  102. package/dist/types/src/presets/barba.d.ts +56 -0
  103. package/dist/types/src/product-events.d.ts +2 -0
  104. package/dist/types/src/product-render.d.ts +17 -0
  105. package/dist/types/src/product-shape.d.ts +90 -0
  106. package/dist/types/src/product-state.d.ts +110 -0
  107. package/dist/types/src/product.d.ts +66 -0
  108. package/dist/types/src/repeaters.d.ts +16 -0
  109. package/dist/types/src/search.d.ts +33 -0
  110. package/dist/types/src/selling-plans.d.ts +64 -0
  111. package/dist/types/src/server.d.ts +88 -0
  112. package/dist/types/src/shopify-values.d.ts +46 -0
  113. package/dist/types/src/status.d.ts +33 -0
  114. package/dist/types/src/storage.d.ts +12 -0
  115. package/dist/types/src/store-html.d.ts +39 -0
  116. package/dist/types/src/storefront/catalog.d.ts +38 -0
  117. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  118. package/dist/types/src/variant-render.d.ts +14 -0
  119. package/package.json +24 -24
  120. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  121. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  122. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  123. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  124. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  125. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  126. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  127. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  128. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  129. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  130. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  131. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  132. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  133. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  134. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  135. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  136. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  137. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  138. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  139. package/src/account/addresses-view.js +0 -92
  140. package/src/account/browser-session.js +0 -359
  141. package/src/account/markup.js +0 -48
  142. package/src/account/operations.js +0 -180
  143. package/src/account/order-lines.js +0 -96
  144. package/src/account/order-list.js +0 -91
  145. package/src/account/order-page.js +0 -192
  146. package/src/account/page-events.js +0 -288
  147. package/src/account/server-client.js +0 -172
  148. package/src/account/shared.js +0 -111
  149. package/src/account/shopify-auth.js +0 -150
  150. package/src/account/subscriptions-view.js +0 -158
  151. package/src/account-api.js +0 -118
  152. package/src/account-links.js +0 -35
  153. package/src/account-pages.js +0 -89
  154. package/src/account.js +0 -342
  155. package/src/amounts.js +0 -36
  156. package/src/analytics-shopify.js +0 -241
  157. package/src/analytics.js +0 -352
  158. package/src/api-version.js +0 -85
  159. package/src/astro.js +0 -266
  160. package/src/cart/checkout.js +0 -97
  161. package/src/cart/drawer.js +0 -138
  162. package/src/cart/mutations.js +0 -234
  163. package/src/cart/render.js +0 -294
  164. package/src/cart/store.js +0 -326
  165. package/src/cart.js +0 -391
  166. package/src/client.js +0 -267
  167. package/src/collection.js +0 -183
  168. package/src/config.js +0 -349
  169. package/src/controls/cart-controls.js +0 -155
  170. package/src/controls/discount.js +0 -96
  171. package/src/controls/extras.js +0 -63
  172. package/src/controls/navigation.js +0 -83
  173. package/src/diagnose-markup.js +0 -333
  174. package/src/diagnose.js +0 -295
  175. package/src/dom.js +0 -287
  176. package/src/extras.js +0 -145
  177. package/src/fragments.js +0 -237
  178. package/src/images.js +0 -104
  179. package/src/index.js +0 -353
  180. package/src/initial.js +0 -87
  181. package/src/lazy-iife.js +0 -116
  182. package/src/lazy.js +0 -39
  183. package/src/loader.js +0 -337
  184. package/src/market-codes.js +0 -35
  185. package/src/markets-ui.js +0 -204
  186. package/src/markets.js +0 -334
  187. package/src/markup.js +0 -64
  188. package/src/mounting.js +0 -277
  189. package/src/page-market.js +0 -116
  190. package/src/parts/account-pages.js +0 -6
  191. package/src/parts/account.js +0 -6
  192. package/src/parts/analytics.js +0 -6
  193. package/src/parts/diagnose.js +0 -6
  194. package/src/parts/extras.js +0 -6
  195. package/src/parts/markets.js +0 -6
  196. package/src/parts/search.js +0 -6
  197. package/src/presets/barba.js +0 -190
  198. package/src/product-events.js +0 -130
  199. package/src/product-render.js +0 -257
  200. package/src/product-state.js +0 -318
  201. package/src/product.js +0 -344
  202. package/src/repeaters.js +0 -173
  203. package/src/search.js +0 -323
  204. package/src/selling-plans.js +0 -154
  205. package/src/server.js +0 -215
  206. package/src/shopify-values.js +0 -89
  207. package/src/status.js +0 -126
  208. package/src/store-html.js +0 -0
  209. package/src/storefront/catalog.js +0 -199
  210. package/src/storefront/json-ld.js +0 -167
  211. package/src/variant-render.js +0 -165
  212. package/types/astro.d.ts +0 -28
  213. package/types/barba.d.ts +0 -16
  214. package/types/index.d.ts +0 -577
  215. 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,76 @@
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]) => inner === void 0 || 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 operationOf(document2) {
466
+ return /\bmutation\b/.test(document2.replace(STRINGS_AND_COMMENTS, "")) ? "mutation" : "query";
467
+ }
468
+ function inContext(query2, { country, language }) {
241
469
  const args = [];
242
- const country = toCountry(ctx.country);
243
- const language = toLanguage(ctx.language);
244
470
  if (country) args.push(`country: ${country}`);
245
471
  if (language) args.push(`language: ${language}`);
246
- if (!args.length || query.includes("@inContext")) return query;
472
+ if (!args.length) return query2;
247
473
  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}`);
474
+ const blanked = query2.replace(STRINGS_AND_COMMENTS, (text2) => " ".repeat(text2.length));
475
+ const selectionStart = blanked.indexOf("{");
476
+ if (blanked.slice(0, selectionStart).includes("@inContext")) return query2;
477
+ if (blanked.trimStart().startsWith("{")) return `query ${tag} ${query2.slice(selectionStart)}`;
478
+ const header = OPERATION_START.exec(blanked);
479
+ if (!header) return query2;
480
+ const end = header[0].length;
481
+ return `${query2.slice(0, end).trimEnd()} ${tag} ${query2.slice(end).trimStart()}`;
482
+ }
483
+ function withContext(query2, ctx = cfg) {
484
+ const country = toCountry(ctx.country) ?? void 0;
485
+ const language = toLanguage(ctx.language) ?? void 0;
486
+ return inContext(query2, { country, language });
250
487
  }
251
488
  var RETRY_BACKOFF_MS = 300;
252
489
  var THROTTLE_BACKOFF_MS = 500;
253
490
  var SECURITY_REJECTION = 430;
491
+ var MS_PER_SECOND = 1e3;
254
492
  var INVENTORY_FIELD = "quantityAvailable";
255
493
  var storesWithoutInventory = /* @__PURE__ */ new Set();
256
494
  var storeOf = (conf) => `${conf.domain} ${conf.apiVersion} ${conf.token}`;
495
+ function queryText(conf, query2, access, { context, operation }) {
496
+ const written = typeof query2 === "string" ? query2 : query2(access);
497
+ const text2 = context ? inContext(written, conf) : written;
498
+ if (conf.maxAge <= 0 || operation === "mutation") return text2;
499
+ return `${text2} #${Math.floor(Date.now() / (conf.maxAge * MS_PER_SECOND))}`;
500
+ }
257
501
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
258
502
  function statusText(status2, conf) {
259
503
  if (status2 === 401 || status2 === 403) {
@@ -265,75 +509,88 @@
265
509
  }
266
510
  return `HTTP ${status2} from ${conf.domain}`;
267
511
  }
512
+ function toGraphQLError(value) {
513
+ const message = field(value, "message");
514
+ const code = field(field(value, "extensions"), "code");
515
+ const text2 = typeof message === "string" ? message : "";
516
+ return typeof code === "string" ? { message: text2, extensions: { code } } : { message: text2 };
517
+ }
518
+ function toGraphQLResponse(json) {
519
+ if (!isRecord(json)) return null;
520
+ const { data, errors } = json;
521
+ if (errors === void 0 || errors === null) return { data };
522
+ if (!Array.isArray(errors)) return null;
523
+ return { data, errors: errors.map(toGraphQLError) };
524
+ }
525
+ function invalidResponse(res, conf) {
526
+ return res.ok ? new TetherError(`Invalid response (${res.status})`, { code: "INVALID_RESPONSE", status: res.status }) : new TetherError(statusText(res.status, conf), { status: res.status });
527
+ }
268
528
  function graphQLFailure(res, errors, conf) {
269
529
  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 });
530
+ let text2 = message || "Storefront API error";
531
+ if (!res.ok) text2 = res.status === 401 || res.status === 403 || !message ? statusText(res.status, conf) : message;
532
+ return new TetherError(text2, { status: res.status, errors });
533
+ }
534
+ function networkFailure(error, retry) {
535
+ if (retry === "any") return { kind: "retry", backoffMs: RETRY_BACKOFF_MS };
536
+ throw new TetherError(`Network error: ${messageOf(error)}`, { code: "NETWORK" });
273
537
  }
274
- async function attempt(conf, query, variables, canRetry) {
538
+ async function attempt(conf, query2, variables, isData, retry) {
275
539
  let res;
276
540
  try {
277
541
  res = await (conf.fetch ?? fetch)(endpoint(conf), {
278
542
  method: "POST",
279
543
  headers: { "Content-Type": "application/json", "X-Shopify-Storefront-Access-Token": conf.token },
280
- body: JSON.stringify({ query, variables })
544
+ body: JSON.stringify({ query: query2, variables })
281
545
  });
282
546
  } catch (error) {
283
- if (canRetry) return { kind: "retry", backoffMs: RETRY_BACKOFF_MS };
284
- throw new TetherError(`Network error: ${error instanceof Error ? error.message : String(error)}`, {
285
- code: "NETWORK"
286
- });
547
+ return networkFailure(error, retry);
287
548
  }
288
549
  if (res.status === SECURITY_REJECTION) {
289
550
  throw new TetherError("Shopify security rejection (430)", { status: SECURITY_REJECTION });
290
551
  }
291
- if (canRetry && (res.status === 429 || res.status >= 500)) {
552
+ if (retry !== "none" && res.status === 429 || retry === "any" && res.status >= 500) {
292
553
  return { kind: "retry", backoffMs: RETRY_BACKOFF_MS };
293
554
  }
294
555
  let json;
295
556
  try {
296
557
  json = await res.json();
297
- } catch {
298
- const text = res.ok ? `Invalid response (${res.status})` : statusText(res.status, conf);
299
- throw new TetherError(text, { status: res.status });
558
+ } catch (error) {
559
+ if (error instanceof SyntaxError) throw invalidResponse(res, conf);
560
+ return networkFailure(error, retry);
300
561
  }
301
- const { data, errors = [] } = (
302
- /** @type {GraphQLResponse | null} */
303
- json ?? {}
304
- );
562
+ const body = toGraphQLResponse(json);
563
+ if (!body) throw invalidResponse(res, conf);
564
+ const { data, errors = [] } = body;
305
565
  if (errors.length) {
306
- if (query.includes(INVENTORY_FIELD) && errors.some((error) => error.message.includes(INVENTORY_FIELD))) {
307
- return { kind: "inventory-refused" };
566
+ if (query2.includes(INVENTORY_FIELD) && errors.some((error) => error.message.includes(INVENTORY_FIELD))) {
567
+ return { kind: "inventory-refused", error: graphQLFailure(res, errors, conf) };
308
568
  }
309
- if (canRetry && errors.some((error) => error.extensions?.code === "THROTTLED")) {
569
+ if (retry !== "none" && errors.some((error) => error.extensions?.code === "THROTTLED")) {
310
570
  return { kind: "retry", backoffMs: THROTTLE_BACKOFF_MS };
311
571
  }
312
572
  throw graphQLFailure(res, errors, conf);
313
573
  }
314
574
  if (!res.ok) throw new TetherError(statusText(res.status, conf), { status: res.status });
575
+ if (!isData(data)) throw invalidResponse(res, conf);
315
576
  return { kind: "data", data };
316
577
  }
317
- async function request(conf, query, variables = {}, { context = true } = {}) {
578
+ async function request(conf, query2, variables, isData, { context = true, operation = "query" } = {}) {
318
579
  if (!conf.domain || !conf.token) {
319
580
  throw new TetherError("Tether is not configured: domain and token are required", { code: "NOT_CONFIGURED" });
320
581
  }
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
582
  const store = storeOf(conf);
583
+ const retryWhileLeft = operation === "mutation" ? "not-run" : "any";
326
584
  let retries = 0;
327
585
  for (; ; ) {
328
- const sent = storesWithoutInventory.has(store) ? text.replaceAll(INVENTORY_FIELD, "") : text;
329
- const outcome = await attempt(conf, sent, variables, retries < conf.retries);
586
+ const sent = queryText(conf, query2, { inventory: !storesWithoutInventory.has(store) }, { context, operation });
587
+ const retry = retries < conf.retries ? retryWhileLeft : "none";
588
+ const outcome = await attempt(conf, sent, variables, isData, retry);
330
589
  switch (outcome.kind) {
331
590
  case "data":
332
- return (
333
- /** @type {T} */
334
- outcome.data
335
- );
591
+ return outcome.data;
336
592
  case "inventory-refused":
593
+ if (typeof query2 === "string") throw outcome.error;
337
594
  log(`the token can't read inventory: ${INVENTORY_FIELD} is left out from now on`);
338
595
  storesWithoutInventory.add(store);
339
596
  break;
@@ -345,7 +602,14 @@
345
602
  }
346
603
  }
347
604
  }
348
- var gql = (query, variables, options) => request(cfg, query, variables, options);
605
+ function gql(query2, variables, isData, options) {
606
+ return request(cfg, query2, variables, isData, options);
607
+ }
608
+ var hasData = (data) => typeof data === "object" && data !== null;
609
+ function query(text2, variables = {}, options) {
610
+ const operation = operationOf(typeof text2 === "string" ? text2 : text2({ inventory: true }));
611
+ return gql(text2, variables, hasData, { ...options, operation });
612
+ }
349
613
  function unwrap(payload, name) {
350
614
  if (!payload) throw new TetherError(`${name}: empty response`);
351
615
  const userErrors = payload.userErrors ?? [];
@@ -355,133 +619,237 @@
355
619
  return payload;
356
620
  }
357
621
 
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
622
+ // src/markup.ts
399
623
  var attr = (name) => `${cfg.prefix}-${name}`;
400
624
  var sel = (name, value) => value === void 0 ? `[${attr(name)}]` : `[${attr(name)}="${value}"]`;
401
625
  var get = (el, name) => el.getAttribute(attr(name));
402
626
  var has = (el, name) => el.hasAttribute(attr(name));
403
627
  function qsa(root, selector) {
404
628
  if (!root) return [];
405
- return [.../** @type {NodeListOf<HTMLElement>} */
406
- root.querySelectorAll(selector)];
629
+ return [...root.querySelectorAll(selector)];
407
630
  }
408
631
  var scoped = (root, selector, scopeSelector) => qsa(root, selector).filter((el) => el.closest(scopeSelector) === root);
632
+ var ANALYTICS_EVENTS = [
633
+ "product-ready",
634
+ "list-ready",
635
+ "cart-updated",
636
+ "cart-toggled",
637
+ "checkout",
638
+ "search"
639
+ ];
640
+ var ownEvents = /* @__PURE__ */ new WeakMap();
641
+ function ownEvent(name, detail) {
642
+ const event = new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true });
643
+ ownEvents.set(event, name);
644
+ return event;
645
+ }
646
+ function isOwnEvent(event, name) {
647
+ return ownEvents.get(event) === name;
648
+ }
409
649
  function emit(name, detail, target = document) {
410
- target.dispatchEvent(new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true }));
650
+ target.dispatchEvent(ownEvent(name, detail));
411
651
  }
412
652
 
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"
653
+ // src/store-html.ts
654
+ var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
655
+ var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
656
+ ...["h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "div", "br", "hr", "blockquote", "code", "pre"],
657
+ ...["strong", "em", "b", "i", "u", "s", "small", "sub", "sup"],
658
+ ...["ul", "ol", "li", "dl", "dt", "dd"],
659
+ ...["table", "caption", "colgroup", "col", "thead", "tbody", "tfoot", "tr", "th", "td"],
660
+ ...["a", "img", "picture", "source", "video", "figure", "figcaption", "iframe"]
430
661
  ]);
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
- }
662
+ var REMOVED_WITH_CONTENT = /* @__PURE__ */ new Set(["script", "style", "template", "noscript", "textarea", "select", "title"]);
663
+ var GLOBAL_ATTRIBUTES = /* @__PURE__ */ new Set(["style", "title", "lang", "dir", "role", "align", "width", "height"]);
664
+ var ELEMENT_ATTRIBUTES = /* @__PURE__ */ new Map([
665
+ ["a", /* @__PURE__ */ new Set(["href", "target", "rel", "hreflang"])],
666
+ ["img", /* @__PURE__ */ new Set(["src", "srcset", "sizes", "alt", "loading", "decoding"])],
667
+ ["source", /* @__PURE__ */ new Set(["src", "srcset", "sizes", "type", "media"])],
668
+ ["video", /* @__PURE__ */ new Set(["src", "poster", "controls", "autoplay", "muted", "loop", "playsinline", "preload"])],
669
+ ["iframe", /* @__PURE__ */ new Set(["src", "allow", "allowfullscreen", "frameborder", "loading"])],
670
+ ["blockquote", /* @__PURE__ */ new Set(["cite"])],
671
+ ["ol", /* @__PURE__ */ new Set(["start", "type", "reversed"])],
672
+ ["li", /* @__PURE__ */ new Set(["value"])],
673
+ ["table", /* @__PURE__ */ new Set(["border", "cellpadding", "cellspacing", "summary"])],
674
+ ["col", /* @__PURE__ */ new Set(["span"])],
675
+ ["colgroup", /* @__PURE__ */ new Set(["span"])],
676
+ ["th", /* @__PURE__ */ new Set(["colspan", "rowspan", "headers", "scope", "valign"])],
677
+ ["td", /* @__PURE__ */ new Set(["colspan", "rowspan", "headers", "valign"])]
678
+ ]);
679
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "poster", "cite"]);
680
+ var SCRIPT_URL = /^(javascript|vbscript|data):/i;
681
+ var DATA_IMAGE = /^data:image\/(png|gif|jpeg|webp|avif);/i;
682
+ var OUT_OF_PLACE_PROPERTIES = /* @__PURE__ */ new Set([
683
+ "position",
684
+ "top",
685
+ "right",
686
+ "bottom",
687
+ "left",
688
+ "z-index",
689
+ "-webkit-box-reflect"
690
+ ]);
691
+ var OUT_OF_PLACE_PREFIXES = ["inset", "transform", "translate", "rotate", "scale", "offset"];
692
+ var VIEWPORT_LENGTH = /\d(?:[sld]?v|cq)(?:w|h|i|b|min|max)\b/i;
693
+ var PAINTS_BEYOND = /^(?:outline|box-shadow|text-shadow|filter|text-decoration|text-underline-offset|text-stroke)|^(?:border-image|mask-border|mask-box-image)(?:-outset)?$/;
694
+ var MAX_PAINT_PX = 40;
695
+ var NEGATIVE_LIMITS = /* @__PURE__ */ new Map([
696
+ ["letter-spacing", { px: 4, em: 0.25 }],
697
+ ["word-spacing", { px: 4, em: 0.25 }],
698
+ ["text-indent", { px: MAX_PAINT_PX }]
699
+ ]);
700
+ var NUMBER = /(?<![\w#.-])[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?(%|[a-z]+)?/gi;
701
+ var NON_LENGTH_FUNCTION = /(?<![\w-])(?!drop-shadow\(|blur\()[\w-]+\([^()]*\)/g;
702
+ var COMPUTED_VALUE = /(?:var|calc|min|max|clamp|env|attr)\(/i;
703
+ var FETCHING_VALUE = /url\(|expression\(|@import/i;
704
+ var IGNORED_IN_SCHEME = /[\u0000- ]/g;
705
+ var IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-popups allow-presentation";
706
+ var IFRAME_FEATURES = /* @__PURE__ */ new Set([
707
+ ...["autoplay", "encrypted-media", "picture-in-picture", "fullscreen", "clipboard-write"],
708
+ ...["accelerometer", "gyroscope", "web-share"]
709
+ ]);
710
+ var IFRAME_REFERRER_POLICY = "strict-origin-when-cross-origin";
711
+ var EMBED_ORIGINS = [
712
+ "https://www.youtube.com",
713
+ "https://www.youtube-nocookie.com",
714
+ "https://player.vimeo.com",
715
+ "https://www.loom.com",
716
+ "https://fast.wistia.net"
717
+ ];
718
+ function isAllowedAttribute(tag, name) {
719
+ return GLOBAL_ATTRIBUTES.has(name) || name.startsWith("aria-") || (ELEMENT_ATTRIBUTES.get(tag)?.has(name) ?? false);
720
+ }
721
+ function isSafeUrl(tag, name, value) {
722
+ const url2 = value.replace(IGNORED_IN_SCHEME, "");
723
+ if (!SCRIPT_URL.test(url2)) return true;
724
+ return tag === "img" && name === "src" && DATA_IMAGE.test(url2);
725
+ }
726
+ function urlOrNull(value, base) {
727
+ try {
728
+ return new URL(value, base);
729
+ } catch {
730
+ return null;
731
+ }
732
+ }
733
+ function pageOf(document2, styleParser, embedOrigins) {
734
+ const baseUrl = document2.baseURI;
735
+ const origin = urlOrNull(baseUrl)?.origin ?? "null";
736
+ return {
737
+ origin: origin === "null" ? null : origin,
738
+ baseUrl,
739
+ styleParser,
740
+ embedOrigins: /* @__PURE__ */ new Set([...EMBED_ORIGINS, ...embedOrigins])
741
+ };
742
+ }
743
+ function isAllowedEmbed(value, page) {
744
+ const url2 = urlOrNull(value.trim(), page.baseUrl);
745
+ return url2 !== null && url2.origin !== page.origin && page.embedOrigins.has(url2.origin);
746
+ }
747
+ function allowedFeatures(value) {
748
+ const features = value.split(";").map((directive) => directive.trim().split(/\s+/)[0]?.toLowerCase() ?? "");
749
+ return [...new Set(features.filter((feature) => IFRAME_FEATURES.has(feature)))].join("; ");
750
+ }
751
+ function isOutOfPlace(property, value) {
752
+ if (OUT_OF_PLACE_PROPERTIES.has(property) || OUT_OF_PLACE_PREFIXES.some((prefix) => property.startsWith(prefix))) {
753
+ return true;
754
+ }
755
+ if (VIEWPORT_LENGTH.test(value)) return true;
756
+ const limits = NEGATIVE_LIMITS.get(property);
757
+ if (limits) return COMPUTED_VALUE.test(value) || movesBackTooFar(value, limits);
758
+ return property.startsWith("margin") && value.includes("-");
759
+ }
760
+ function movesBackTooFar(value, limits) {
761
+ return [...value.matchAll(NUMBER)].some(([number = "", unit = ""]) => {
762
+ const amount = parseFloat(number);
763
+ if (amount >= 0) return false;
764
+ const limit = limits[unit.toLowerCase()];
765
+ return limit === void 0 || -amount > limit;
766
+ });
767
+ }
768
+ function paintsTooFar(property, value) {
769
+ if (!PAINTS_BEYOND.test(property.replace(/^-webkit-/, ""))) return false;
770
+ if (COMPUTED_VALUE.test(value)) return true;
771
+ return [...value.replace(NON_LENGTH_FUNCTION, "").matchAll(NUMBER)].some(([number = "", unit = ""]) => {
772
+ const amount = Math.abs(parseFloat(number));
773
+ return amount !== 0 && (unit.toLowerCase() !== "px" || amount > MAX_PAINT_PX);
774
+ });
775
+ }
776
+ function cleanStyle(value, styleParser) {
777
+ const style = styleParser.style;
778
+ style.cssText = value;
779
+ const properties = Array.from({ length: style.length }, (_, index) => style.item(index));
780
+ for (const property of properties) {
781
+ const value2 = style.getPropertyValue(property);
782
+ const remove = isOutOfPlace(property, value2) || paintsTooFar(property, value2) || FETCHING_VALUE.test(value2);
783
+ if (remove) style.removeProperty(property);
784
+ }
785
+ const cleaned = style.cssText;
786
+ styleParser.removeAttribute("style");
787
+ return cleaned;
788
+ }
789
+ function cleanAttributes(element, page) {
790
+ const tag = element.localName;
791
+ for (const attribute of [...element.attributes]) {
792
+ const name = attribute.name.toLowerCase();
793
+ const isUrl = URL_ATTRIBUTES.has(name);
794
+ const keep = isAllowedAttribute(tag, name) && (!isUrl || isSafeUrl(tag, name, attribute.value));
795
+ if (!keep) element.removeAttribute(attribute.name);
796
+ }
797
+ const style = element.getAttribute("style");
798
+ if (style === null) return;
799
+ const cleaned = cleanStyle(style, page.styleParser);
800
+ if (cleaned === "") element.removeAttribute("style");
801
+ else element.setAttribute("style", cleaned);
802
+ }
803
+ function hardenEmbedsAndLinks(element) {
804
+ if (element.localName === "iframe") {
805
+ const allow = allowedFeatures(element.getAttribute("allow") ?? "");
806
+ if (allow === "") element.removeAttribute("allow");
807
+ else element.setAttribute("allow", allow);
808
+ element.setAttribute("sandbox", IFRAME_SANDBOX);
809
+ element.setAttribute("referrerpolicy", IFRAME_REFERRER_POLICY);
810
+ return;
811
+ }
812
+ if (element.localName !== "a" || element.getAttribute("target")?.toLowerCase() !== "_blank") return;
813
+ const rel = new Set((element.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean));
814
+ rel.add("noopener");
815
+ rel.add("noreferrer");
816
+ element.setAttribute("rel", [...rel].join(" "));
817
+ }
818
+ function cleanElement(element, page) {
819
+ const tag = element.localName;
820
+ const isHtml = element.namespaceURI === HTML_NAMESPACE;
821
+ if (isHtml && REMOVED_WITH_CONTENT.has(tag)) {
822
+ element.remove();
823
+ return;
824
+ }
825
+ if (!isHtml || !ALLOWED_ELEMENTS.has(tag)) {
826
+ element.replaceWith(...element.childNodes);
827
+ return;
828
+ }
829
+ if (tag === "iframe" && !isAllowedEmbed(element.getAttribute("src") ?? "", page)) {
830
+ element.remove();
831
+ return;
451
832
  }
833
+ cleanAttributes(element, page);
834
+ hardenEmbedsAndLinks(element);
452
835
  }
453
- function storeHtmlFragment(html, document2 = globalThis.document) {
836
+ function storeHtmlFragment(html, document2 = globalThis.document, embedOrigins = []) {
454
837
  const template = document2.createElement("template");
455
838
  template.innerHTML = html ?? "";
456
- clean(template.content);
839
+ const page = pageOf(document2, template, embedOrigins);
840
+ for (const element of [...template.content.querySelectorAll("*")]) cleanElement(element, page);
457
841
  return template.content;
458
842
  }
459
843
 
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;
844
+ // src/shopify-values.ts
845
+ function isHttpsUrl(url2) {
846
+ if (typeof url2 !== "string") return false;
476
847
  try {
477
- return new URL(url).protocol === "https:";
848
+ return new URL(url2).protocol === "https:";
478
849
  } catch {
479
850
  return false;
480
851
  }
481
852
  }
482
- function isSecretShopifyToken(token) {
483
- return /^shp[a-z]{2}_/.test(token.trim());
484
- }
485
853
  function formatMoney(money, locale) {
486
854
  if (!money) return "";
487
855
  const amount = parseFloat(money.amount);
@@ -495,8 +863,12 @@
495
863
  }
496
864
  }
497
865
  function productUrl(productPath, handle, id) {
498
- if (!/\{(handle|id)\}/.test(productPath)) return `${productPath.replace(/\/$/, "")}/${handle ?? String(handle)}`;
499
- const numericId2 = id ? id.split("/").pop() ?? "" : "";
866
+ if (!/\{(handle|id)\}/.test(productPath)) {
867
+ return handle ? `${productPath.replace(/\/$/, "")}/${encodeURIComponent(handle)}` : null;
868
+ }
869
+ const numericId2 = id ? id.split("/").at(-1) ?? "" : "";
870
+ if (productPath.includes("{handle}") && !handle) return null;
871
+ if (productPath.includes("{id}") && !numericId2) return null;
500
872
  return productPath.replace(/\{handle\}/g, encodeURIComponent(handle ?? "")).replace(/\{id\}/g, encodeURIComponent(numericId2));
501
873
  }
502
874
  function productKey(raw) {
@@ -507,43 +879,44 @@
507
879
  return /\/products\/([^/?#]+)/.exec(value)?.[1] ?? value;
508
880
  }
509
881
 
510
- // src/images.js
511
- function imageUrl(url, { width, height, crop } = {}) {
512
- if (!url) return url;
882
+ // src/images.ts
883
+ function imageUrl(url2, { width, height, crop } = {}) {
884
+ if (!url2) return url2;
513
885
  let parsed;
514
886
  try {
515
- parsed = new URL(url);
887
+ parsed = new URL(url2);
516
888
  } catch (error) {
517
- if (error instanceof TypeError) return url;
889
+ if (error instanceof TypeError) return url2;
518
890
  throw error;
519
891
  }
520
892
  if (width) parsed.searchParams.set("width", String(width));
521
893
  if (height) parsed.searchParams.set("height", String(height));
522
894
  if (crop) parsed.searchParams.set("crop", crop);
523
- return (
524
- /** @type {Url} */
525
- parsed.toString()
526
- );
895
+ return parsed.toString();
527
896
  }
528
897
  function sizeAttribute(el, name) {
529
898
  return el.getAttribute(name) ?? el.getAttribute(attr(name)) ?? el.getAttribute(`data-${name}`);
530
899
  }
531
- function cdnUrl(url, el) {
532
- if (!url || !el) return url;
900
+ function cdnUrl(url2, el) {
901
+ if (!url2 || !el) return url2;
533
902
  const width = sizeAttribute(el, "max-width");
534
903
  const height = sizeAttribute(el, "max-height");
535
- return imageUrl(url, { width, height, crop: width || height ? sizeAttribute(el, "crop") : null });
904
+ return imageUrl(url2, { width, height, crop: width || height ? sizeAttribute(el, "crop") : null });
536
905
  }
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)} `);
906
+ var withoutQuery = (url2) => url2.replace(/\?.*$/, "");
907
+ function sameImage(a, b) {
908
+ if (!a || !b) return false;
909
+ return withoutQuery(a) === withoutQuery(b);
910
+ }
911
+ function cssUrl(url2) {
912
+ const hasScheme = /^\s*[a-z][a-z0-9+.-]*:/i.test(url2);
913
+ if (hasScheme && !/^\s*(https?:|data:image\/)/i.test(url2)) return "none";
914
+ const escaped = url2.replace(/["\\\n\r\f]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
542
915
  return `url("${escaped}")`;
543
916
  }
544
- function setImage(el, url, alt) {
545
- if (!url) return;
546
- const src = cdnUrl(url, el);
917
+ function setImage(el, url2, alt) {
918
+ if (!url2) return;
919
+ const src = cdnUrl(url2, el);
547
920
  if (el.tagName !== "IMG") {
548
921
  el.style.backgroundImage = cssUrl(src);
549
922
  return;
@@ -559,21 +932,15 @@
559
932
  if (!el.getAttribute("loading")) el.setAttribute("loading", "lazy");
560
933
  }
561
934
 
562
- // src/repeaters.js
935
+ // src/repeaters.ts
563
936
  var templates = /* @__PURE__ */ new WeakMap();
564
937
  var hasTemplate = (container) => Boolean(templates.get(container));
565
- var copyOf = (template) => (
566
- /** @type {HTMLElement} */
567
- template.cloneNode(true)
568
- );
938
+ var copyOf = (template) => template.cloneNode(true);
569
939
  function templateOf(container) {
570
940
  const known = templates.get(container);
571
941
  if (known !== void 0) return known;
572
942
  const first = container.firstElementChild;
573
- const template = first ? copyOf(
574
- /** @type {HTMLElement} */
575
- first
576
- ) : null;
943
+ const template = first ? copyOf(first) : null;
577
944
  templates.set(container, template);
578
945
  container.replaceChildren();
579
946
  return template;
@@ -628,8 +995,7 @@
628
995
  }
629
996
  const keyAttribute = `data-${cfg.prefix}-key`;
630
997
  const leavingAttribute = `data-${cfg.prefix}-leaving`;
631
- const live = () => [.../** @type {HTMLCollectionOf<HTMLElement>} */
632
- container.children].filter(
998
+ const live = () => [...container.children].filter(
633
999
  (node) => node.hasAttribute(keyAttribute) && !node.hasAttribute(leavingAttribute)
634
1000
  );
635
1001
  const existing = new Map(live().map((node) => [node.getAttribute(keyAttribute), node]));
@@ -667,7 +1033,7 @@
667
1033
  });
668
1034
  }
669
1035
 
670
- // src/dom.js
1036
+ // src/dom.ts
671
1037
  var SEEN = "data-tether-seen";
672
1038
  var SHOWN = "data-tether-shown";
673
1039
  var PREVIOUS_DISPLAY = "data-tether-prev-display";
@@ -698,14 +1064,14 @@
698
1064
  return formatOr(() => new Date(iso).toLocaleDateString(cfg.locale || void 0), "");
699
1065
  }
700
1066
  function setStoreHtml(el, html) {
701
- el.replaceChildren(storeHtmlFragment(html, el.ownerDocument));
1067
+ el.replaceChildren(storeHtmlFragment(html, el.ownerDocument, cfg.embedOrigins));
702
1068
  }
703
1069
  var isTextField = (el) => el.tagName === "INPUT" || el.tagName === "TEXTAREA";
704
1070
  var isInput = (el) => el.tagName === "INPUT";
705
1071
  function setText(el, value) {
706
- const text = value == null ? "" : String(value);
707
- if (isTextField(el)) el.value = text;
708
- else el.textContent = text;
1072
+ const text2 = value == null ? "" : String(value);
1073
+ if (isTextField(el)) el.value = text2;
1074
+ else el.textContent = text2;
709
1075
  }
710
1076
  var readText = (el) => isTextField(el) ? el.value : el.textContent;
711
1077
  function setTextTracked(el, value) {
@@ -754,15 +1120,22 @@
754
1120
  if (input && isInput(input)) input.checked = selected;
755
1121
  }
756
1122
  var layoutTimer;
1123
+ var pageGlobal = (name) => Reflect.get(window, name);
1124
+ function refreshScrollTrigger() {
1125
+ const scrollTrigger = pageGlobal("ScrollTrigger");
1126
+ if (typeof scrollTrigger !== "function") return;
1127
+ const refresh = Reflect.get(scrollTrigger, "refresh");
1128
+ if (typeof refresh === "function") Reflect.apply(refresh, scrollTrigger, []);
1129
+ }
757
1130
  function notifyLayoutChanged(reason) {
758
1131
  clearTimeout(layoutTimer);
759
1132
  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?.();
1133
+ try {
1134
+ if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
1135
+ else refreshScrollTrigger();
1136
+ } catch (error) {
1137
+ reportError(error, "layout-changed");
1138
+ }
766
1139
  emit("layout-changed", { reason });
767
1140
  }, LAYOUT_DEBOUNCE_MS);
768
1141
  }
@@ -789,15 +1162,27 @@
789
1162
  const raw = el ? get(el, name) : null;
790
1163
  if (!raw) return null;
791
1164
  try {
792
- return (
793
- /** @type {unknown} */
794
- JSON.parse(raw)
795
- );
1165
+ const json = JSON.parse(raw);
1166
+ return json;
796
1167
  } catch {
797
1168
  warn(`invalid JSON in ${attr(name)}`, el);
798
1169
  return null;
799
1170
  }
800
1171
  }
1172
+ function attributeText(value) {
1173
+ if (value === null || value === void 0) return "";
1174
+ if (typeof value === "string") return value;
1175
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1176
+ return JSON.stringify(value);
1177
+ }
1178
+ function attributesFromJson(json) {
1179
+ if (Array.isArray(json)) {
1180
+ const entries = json;
1181
+ return entries.filter(isRecord).map((entry) => ({ key: attributeText(entry["key"]), value: attributeText(entry["value"]) }));
1182
+ }
1183
+ if (!isRecord(json)) return [];
1184
+ return Object.entries(json).map(([key, value]) => ({ key, value: attributeText(value) }));
1185
+ }
801
1186
  function toAttributes(attributes) {
802
1187
  if (!attributes) return [];
803
1188
  if (Array.isArray(attributes)) {
@@ -806,14 +1191,10 @@
806
1191
  return Object.entries(attributes).map(([key, value]) => ({ key, value: String(value ?? "") }));
807
1192
  }
808
1193
 
809
- // src/fragments.js
810
- var fragments_exports = {};
811
- __export(fragments_exports, {
812
- cartFragment: () => cartFragment,
813
- collectionFields: () => collectionFields,
814
- productFragment: () => productFragment
815
- });
1194
+ // src/fragments.ts
816
1195
  var MONEY = "amount currencyCode";
1196
+ var FULL_ACCESS = { inventory: true };
1197
+ var inventoryField = (access) => access.inventory ? "quantityAvailable" : "";
817
1198
  var IMAGE = "url altText width height";
818
1199
  function metafieldsField(metafields) {
819
1200
  const identifiers = [];
@@ -827,11 +1208,11 @@
827
1208
  }
828
1209
  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
1210
  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}}}}}}`;
1211
+ function productFragment(o = cfg, access = FULL_ACCESS) {
1212
+ 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
1213
  }
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}}}}}}}}`;
1214
+ function cartFragment(access = FULL_ACCESS) {
1215
+ 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
1216
  }
836
1217
  function collectionFields() {
837
1218
  return `
@@ -840,7 +1221,7 @@
840
1221
  seo { title description }`;
841
1222
  }
842
1223
 
843
- // src/selling-plans.js
1224
+ // src/selling-plans.ts
844
1225
  var selling_plans_exports = {};
845
1226
  __export(selling_plans_exports, {
846
1227
  GID: () => GID,
@@ -855,19 +1236,59 @@
855
1236
  plansForVariant: () => plansForVariant,
856
1237
  toGid: () => toGid
857
1238
  });
1239
+
1240
+ // src/amounts.ts
1241
+ var DECIMAL = /^(-?)(\d+)(?:\.(\d+))?$/;
1242
+ function toScaledInteger(amount, fractionDigits) {
1243
+ const match = DECIMAL.exec(amount.trim());
1244
+ if (!match) return null;
1245
+ const [, sign = "", whole = "0", fraction = ""] = match;
1246
+ const digits = BigInt(`${whole}${fraction.padEnd(fractionDigits, "0")}`);
1247
+ return sign === "-" ? -digits : digits;
1248
+ }
1249
+ var fractionLength = (amount) => amount.split(".")[1]?.length ?? 0;
1250
+ function isGreaterAmount(amount, other) {
1251
+ if (amount == null || other == null) return false;
1252
+ const fractionDigits = Math.max(fractionLength(amount.trim()), fractionLength(other.trim()));
1253
+ const scaled = toScaledInteger(amount, fractionDigits);
1254
+ const scaledOther = toScaledInteger(other, fractionDigits);
1255
+ if (scaled === null || scaledOther === null) return false;
1256
+ return scaled > scaledOther;
1257
+ }
1258
+ var DEFAULT_FRACTION_DIGITS = 2;
1259
+ var fractionDigitsByCurrency = /* @__PURE__ */ new Map();
1260
+ function currencyFractionDigits(currencyCode) {
1261
+ const known = fractionDigitsByCurrency.get(currencyCode);
1262
+ if (known !== void 0) return known;
1263
+ let digits = DEFAULT_FRACTION_DIGITS;
1264
+ try {
1265
+ const format = new Intl.NumberFormat("en", { style: "currency", currency: currencyCode });
1266
+ digits = format.resolvedOptions().maximumFractionDigits ?? DEFAULT_FRACTION_DIGITS;
1267
+ } catch {
1268
+ }
1269
+ fractionDigitsByCurrency.set(currencyCode, digits);
1270
+ return digits;
1271
+ }
1272
+ function toMinorUnits(money) {
1273
+ return Math.round(parseFloat(money.amount) * 10 ** currencyFractionDigits(money.currencyCode));
1274
+ }
1275
+ function fromMinorUnits(minorUnits, currencyCode) {
1276
+ const digits = currencyFractionDigits(currencyCode);
1277
+ return { amount: (minorUnits / 10 ** digits).toFixed(digits), currencyCode };
1278
+ }
1279
+
1280
+ // src/selling-plans.ts
1281
+ var PERCENT = 100;
858
1282
  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;
1283
+ const text2 = typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
1284
+ if (!text2) return null;
1285
+ if (text2.startsWith("gid://")) return text2;
1286
+ if (/^\d+$/.test(text2)) return `gid://shopify/${type}/${text2}`;
1287
+ return text2;
864
1288
  }
865
1289
  var GID = {
866
- /** @param {unknown} value */
867
1290
  product: (value) => toGid(value, "Product"),
868
- /** @param {unknown} value */
869
1291
  variant: (value) => toGid(value, "ProductVariant"),
870
- /** @param {unknown} value */
871
1292
  sellingPlan: (value) => toGid(value, "SellingPlan")
872
1293
  };
873
1294
  function allPlans(product) {
@@ -894,14 +1315,17 @@
894
1315
  return allocation?.checkoutChargeAmount ?? allocation?.priceAdjustments[0]?.price ?? variant?.price ?? null;
895
1316
  }
896
1317
  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) {
1318
+ const onPlanPrice = planPrice(variant, planId);
1319
+ if (!variant || !onPlanPrice) return { amount: null, percentage: 0 };
1320
+ const oneTime = parseFloat(variant.price.amount);
1321
+ const onPlan = parseFloat(onPlanPrice.amount);
1322
+ if (!Number.isFinite(oneTime) || !Number.isFinite(onPlan) || oneTime <= 0 || onPlan >= oneTime) {
900
1323
  return { amount: null, percentage: 0 };
901
1324
  }
1325
+ const saved = toMinorUnits(variant.price) - toMinorUnits(onPlanPrice);
902
1326
  return {
903
- amount: { amount: (oneTime - onPlan).toFixed(2), currencyCode: variant.price.currencyCode },
904
- percentage: Math.round((1 - onPlan / oneTime) * 100)
1327
+ amount: fromMinorUnits(saved, variant.price.currencyCode),
1328
+ percentage: Math.round((1 - onPlan / oneTime) * PERCENT)
905
1329
  };
906
1330
  }
907
1331
  function planAdjustment(plan) {
@@ -926,7 +1350,43 @@
926
1350
  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
1351
  }
928
1352
 
929
- // src/cart/drawer.js
1353
+ // src/product-shape.ts
1354
+ var MONEY2 = { amount: "string", currencyCode: "string" };
1355
+ var PRODUCT = {
1356
+ id: "string",
1357
+ handle: "string",
1358
+ title: "string",
1359
+ options: ["object"],
1360
+ images: { nodes: ["object"] },
1361
+ priceRange: { minVariantPrice: MONEY2, maxVariantPrice: MONEY2 },
1362
+ variants: { nodes: [{ id: "string", price: MONEY2, selectedOptions: ["object"] }] }
1363
+ };
1364
+ var PRODUCT_CONNECTION = {
1365
+ nodes: [PRODUCT],
1366
+ pageInfo: { hasNextPage: "boolean" }
1367
+ };
1368
+ var isProductShape = (value) => field(value, "id") !== "" && field(value, "handle") !== "" && matches(value, PRODUCT);
1369
+ var PRODUCT_NODE = (node) => isRecord(node) && (node["id"] === void 0 || matches(node, PRODUCT));
1370
+
1371
+ // src/cart/cart-shape.ts
1372
+ var LINE = {
1373
+ id: "string",
1374
+ quantity: "number",
1375
+ attributes: ["object"],
1376
+ cost: { totalAmount: MONEY2, subtotalAmount: MONEY2, amountPerQuantity: MONEY2 },
1377
+ merchandise: { id: "string", selectedOptions: ["object"], product: "object" }
1378
+ };
1379
+ var CART = {
1380
+ id: "string",
1381
+ checkoutUrl: "string",
1382
+ totalQuantity: "number",
1383
+ attributes: ["object"],
1384
+ discountCodes: ["object"],
1385
+ cost: { totalAmount: MONEY2, subtotalAmount: MONEY2 },
1386
+ lines: { nodes: [LINE] }
1387
+ };
1388
+
1389
+ // src/cart/drawer.ts
930
1390
  var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
931
1391
  var FOCUS_AFTER_OPEN_MS = 60;
932
1392
  var openWithoutDrawer = false;
@@ -1014,19 +1474,7 @@
1014
1474
  (event.shiftKey ? last : first).focus();
1015
1475
  }
1016
1476
 
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
- });
1477
+ // src/loader.ts
1030
1478
  var MISS_CACHE_MS = 1e4;
1031
1479
  var FRESH_MS = 6e4;
1032
1480
  var productCache = /* @__PURE__ */ new Map();
@@ -1060,6 +1508,7 @@
1060
1508
  productCache.set(key, loading);
1061
1509
  return loading;
1062
1510
  }
1511
+ var withProductFragment = (operation) => (access) => `${operation}${productFragment(cfg, access)}`;
1063
1512
  function batchQuery(keys) {
1064
1513
  const ids = keys.filter(isGid);
1065
1514
  const handles = keys.filter((key) => !isGid(key));
@@ -1076,8 +1525,8 @@
1076
1525
  variables[`h${index}`] = handle;
1077
1526
  selections.push(`h${index}: product(handle: $h${index}) { ...TetherProduct }`);
1078
1527
  });
1079
- const query = `query TetherProducts(${definitions.join(",")}){${selections.join(" ")}}${productFragment()}`;
1080
- return { query, variables, handles };
1528
+ const query2 = withProductFragment(`query TetherProducts(${definitions.join(",")}){${selections.join(" ")}}`);
1529
+ return { query: query2, variables, handles };
1081
1530
  }
1082
1531
  function settleWaiting(key, settle) {
1083
1532
  const waiter = waiting.get(key);
@@ -1091,16 +1540,21 @@
1091
1540
  if (productCache.get(key) === miss) productCache.delete(key);
1092
1541
  }, MISS_CACHE_MS);
1093
1542
  }
1543
+ function isBatchData(data) {
1544
+ if (!isRecord(data)) return false;
1545
+ const { byId = [], ...byHandle } = data;
1546
+ return matches(byId, [orNull(PRODUCT_NODE)]) && Object.values(byHandle).every(orNull(PRODUCT));
1547
+ }
1094
1548
  async function flush() {
1095
1549
  flushScheduled = false;
1096
1550
  const batchGeneration = generation;
1097
1551
  const keys = pending.splice(0);
1098
1552
  if (!keys.length) return;
1099
- const { query, variables, handles } = batchQuery(keys);
1553
+ const { query: query2, variables, handles } = batchQuery(keys);
1100
1554
  log("fetch products", keys);
1101
1555
  let data;
1102
1556
  try {
1103
- data = await gql(query, variables);
1557
+ data = await gql(query2, variables, isBatchData);
1104
1558
  } catch (error) {
1105
1559
  keys.forEach((key) => {
1106
1560
  settleWaiting(key, (waiter) => {
@@ -1111,7 +1565,7 @@
1111
1565
  return;
1112
1566
  }
1113
1567
  const found = /* @__PURE__ */ new Map();
1114
- for (const product of data.byId ?? []) if (product) found.set(product.id, product);
1568
+ for (const product of data.byId ?? []) if (product?.id) found.set(product.id, product);
1115
1569
  handles.forEach((handle, index) => {
1116
1570
  const product = data[`h${index}`];
1117
1571
  if (product) found.set(handle, product);
@@ -1157,51 +1611,57 @@
1157
1611
  void productCache.get(key)?.then((product) => {
1158
1612
  stale.delete(product.handle);
1159
1613
  stale.delete(product.id);
1160
- });
1614
+ }, failureIsReportedByTheRender);
1161
1615
  return true;
1162
1616
  }
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));
1617
+ function failureIsReportedByTheRender() {
1618
+ }
1619
+ function refreshProduct(key) {
1620
+ const normalized = normalizeProductKey(key);
1621
+ if (normalized) productCache.delete(normalized);
1622
+ return getProduct(key);
1169
1623
  }
1624
+ var isCollectionAnswer = shaped({
1625
+ collection: orNull({ handle: "string", products: PRODUCT_CONNECTION })
1626
+ });
1627
+ var isSearchAnswer = shaped({ products: PRODUCT_CONNECTION });
1628
+ var isRecommendationsAnswer = shaped({
1629
+ productRecommendations: orNull([PRODUCT])
1630
+ });
1170
1631
  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
1632
  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
1633
  async function getProducts({
1173
1634
  collection,
1174
- query,
1635
+ query: query2,
1175
1636
  first = cfg.collectionFirst,
1176
1637
  after = null,
1177
1638
  sortKey,
1178
1639
  reverse = false,
1179
1640
  filters
1180
1641
  } = {}) {
1181
- const fragment = productFragment();
1182
1642
  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
- });
1643
+ const data2 = await gql(
1644
+ withProductFragment(COLLECTION_QUERY),
1645
+ {
1646
+ handle: collection,
1647
+ first,
1648
+ after,
1649
+ sortKey: sortKey || void 0,
1650
+ reverse,
1651
+ filters: filters ?? void 0
1652
+ },
1653
+ isCollectionAnswer
1654
+ );
1192
1655
  if (!data2.collection) throw new TetherError(`Collection not found: ${collection}`, { code: "NOT_FOUND" });
1193
1656
  const { products, ...rest } = data2.collection;
1194
1657
  products.nodes.forEach(primeProduct);
1195
1658
  return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
1196
1659
  }
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
- });
1660
+ const data = await gql(
1661
+ withProductFragment(SEARCH_QUERY),
1662
+ { first, after, query: query2 || void 0, sortKey: sortKey || void 0, reverse },
1663
+ isSearchAnswer
1664
+ );
1205
1665
  data.products.nodes.forEach(primeProduct);
1206
1666
  return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
1207
1667
  }
@@ -1209,45 +1669,30 @@ ${fragment}`, {
1209
1669
  const key = normalizeProductKey(typeof product === "object" ? product?.id : product);
1210
1670
  const byId = key?.startsWith("gid://") ?? false;
1211
1671
  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 }
1672
+ withProductFragment(
1673
+ `query TetherRecs(${byId ? "$id: ID!" : "$handle: String!"},$intent:ProductRecommendationIntent){productRecommendations(${byId ? "productId: $id" : "productHandle: $handle"},intent:$intent){...TetherProduct}}`
1674
+ ),
1675
+ byId ? { id: key, intent } : { handle: key, intent },
1676
+ isRecommendationsAnswer
1214
1677
  );
1215
1678
  const list = data.productRecommendations ?? [];
1216
1679
  list.forEach(primeProduct);
1217
1680
  return list;
1218
1681
  }
1219
1682
 
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
1683
+ // src/cart/render.ts
1240
1684
  var DEFAULT_TITLE = "Default Title";
1241
1685
  var quantityOf = (line, pending2) => pending2.get(line.id)?.quantity ?? line.quantity;
1242
1686
  var numericId = (gid) => gid.split("/").pop() ?? gid;
1243
1687
  function lineUrl(line) {
1244
1688
  const variant = line.merchandise;
1245
1689
  const base = productUrl2(variant.product.handle, variant.product.id);
1246
- const query = new URLSearchParams();
1247
- query.set("variant", numericId(variant.id));
1690
+ if (!base) return null;
1691
+ const query2 = new URLSearchParams();
1692
+ query2.set("variant", numericId(variant.id));
1248
1693
  const planId = line.sellingPlanAllocation?.sellingPlan.id;
1249
- if (planId) query.set("selling_plan", numericId(planId));
1250
- return `${base}${base.includes("?") ? "&" : "?"}${query.toString()}`;
1694
+ if (planId) query2.set("selling_plan", numericId(planId));
1695
+ return `${base}${base.includes("?") ? "&" : "?"}${query2.toString()}`;
1251
1696
  }
1252
1697
  var productTitle = (line) => line.merchandise.product.title;
1253
1698
  var LINE_TEXT = {
@@ -1305,19 +1750,17 @@ ${fragment}`, {
1305
1750
  qsa(node, sel("line", "warning")).forEach((el) => {
1306
1751
  setTextTracked(el, warning?.message ?? "");
1307
1752
  });
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
- };
1753
+ const conditions = /* @__PURE__ */ new Map([
1754
+ ["subscription", isSubscription],
1755
+ ["one-time", !isSubscription],
1756
+ ["on-sale", isGreaterAmount(line.cost.compareAtAmountPerQuantity?.amount, line.cost.amountPerQuantity.amount)],
1757
+ ["has-components", (line.lineComponents ?? []).length > 0],
1758
+ ["warning", Boolean(warning)]
1759
+ ]);
1315
1760
  qsa(node, sel("line-show")).forEach((el) => {
1316
1761
  const key = get(el, "line-show");
1317
- if (key !== null && Object.hasOwn(conditions, key)) show(el, conditions[
1318
- /** @type {keyof conditions} */
1319
- key
1320
- ]);
1762
+ const visible = key === null ? void 0 : conditions.get(key);
1763
+ if (visible !== void 0) show(el, visible);
1321
1764
  });
1322
1765
  }
1323
1766
  function renderLine(node, line, view) {
@@ -1330,7 +1773,8 @@ ${fragment}`, {
1330
1773
  return;
1331
1774
  }
1332
1775
  if (key === "url") {
1333
- el.setAttribute("href", lineUrl(line));
1776
+ const url2 = lineUrl(line);
1777
+ if (url2) el.setAttribute("href", url2);
1334
1778
  return;
1335
1779
  }
1336
1780
  const value = lineValue(line, key, view);
@@ -1339,12 +1783,7 @@ ${fragment}`, {
1339
1783
  renderLineState(node, line, view);
1340
1784
  qsa(node, `select${sel("line-plan")}`).forEach((select) => {
1341
1785
  getProduct(variant.product.handle).then((product) => {
1342
- fillPlanSelect(
1343
- /** @type {HTMLSelectElement} */
1344
- select,
1345
- line,
1346
- product
1347
- );
1786
+ fillPlanSelect(select, line, product);
1348
1787
  }).catch((error) => {
1349
1788
  reportError(error, "line plan options");
1350
1789
  });
@@ -1353,22 +1792,20 @@ ${fragment}`, {
1353
1792
  function renderConditions(cart, view) {
1354
1793
  const count = cart?.totalQuantity ?? 0;
1355
1794
  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
- };
1795
+ const conditions = /* @__PURE__ */ new Map([
1796
+ ["cart-empty", count === 0],
1797
+ ["cart-filled", count > 0],
1798
+ ["cart-has-subscription", hasSubscription],
1799
+ ["cart-no-subscription", !hasSubscription],
1800
+ ["cart-has-discount", (cart?.discountCodes ?? []).some((code) => code.applicable)],
1801
+ ["cart-error", view.failed],
1802
+ ["cart-warning", view.warnings.length > 0]
1803
+ ]);
1365
1804
  qsa(document, sel("show")).forEach((el) => {
1366
1805
  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
- ]);
1806
+ const visible = key === null ? void 0 : conditions.get(key);
1807
+ if (visible === void 0 || el.closest(sel("product"))) return;
1808
+ show(el, visible);
1372
1809
  });
1373
1810
  qsa(document, sel("checkout")).forEach((el) => {
1374
1811
  el.classList.toggle("is-disabled", !count);
@@ -1424,15 +1861,38 @@ ${fragment}`, {
1424
1861
  }
1425
1862
  function showSavedCount(count) {
1426
1863
  qsa(document, sel("cart-count")).forEach((el) => {
1427
- if (/^\s*0?\s*$/.test(el.textContent)) el.textContent = count;
1864
+ if (/^\s*0?\s*$/.test(el.textContent)) el.textContent = String(count);
1428
1865
  });
1429
1866
  }
1430
1867
 
1431
- // src/cart/store.js
1868
+ // src/storage.ts
1869
+ function getStored(key, area = "localStorage") {
1870
+ try {
1871
+ return window[area].getItem(key);
1872
+ } catch {
1873
+ return null;
1874
+ }
1875
+ }
1876
+ function setStored(key, value, area = "localStorage") {
1877
+ try {
1878
+ window[area].setItem(key, value);
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ function removeStored(key, area = "localStorage") {
1883
+ try {
1884
+ window[area].removeItem(key);
1885
+ } catch {
1886
+ }
1887
+ }
1888
+
1889
+ // server/src/shared/cart-id.ts
1890
+ var CART_ID = /^gid:\/\/shopify\/Cart\/[\w-]+(\?key=[\w-]+)?$/;
1891
+
1892
+ // src/cart/store.ts
1432
1893
  var status = { kind: "unknown" };
1433
1894
  var warnings = [];
1434
1895
  var pendingQuantities = /* @__PURE__ */ new Map();
1435
- var customer = { accessToken: null, beforeCheckout: null };
1436
1896
  var cartStatus = () => status;
1437
1897
  function currentCart() {
1438
1898
  switch (status.kind) {
@@ -1447,27 +1907,19 @@ ${fragment}`, {
1447
1907
  var currentWarnings = () => warnings;
1448
1908
  var statusOf = (cart) => cart ? { kind: "ready", cart } : { kind: "empty" };
1449
1909
  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
- }
1910
+ function savedCartId() {
1911
+ const stored = getStored(cfg.storageKey);
1912
+ if (!stored) return null;
1913
+ if (CART_ID.test(stored)) return stored;
1914
+ forgetSavedCart();
1915
+ return null;
1463
1916
  }
1464
- var savedCartId = () => readStorageIfAllowed(cfg.storageKey) || null;
1465
1917
  function forgetSavedCart() {
1466
- writeStorageIfAllowed(cfg.storageKey, null);
1918
+ removeStored(cfg.storageKey);
1467
1919
  }
1468
1920
  function keepQuietly(cart) {
1469
1921
  status = statusOf(cart);
1470
- if (cart) writeStorageIfAllowed(cfg.storageKey, cart.id);
1922
+ if (cart) setStored(cfg.storageKey, cart.id);
1471
1923
  }
1472
1924
  function render() {
1473
1925
  renderCart({
@@ -1483,7 +1935,7 @@ ${fragment}`, {
1483
1935
  }
1484
1936
  function commit(cart, reason) {
1485
1937
  keepQuietly(cart);
1486
- writeStorageIfAllowed(seenKey(), `${Date.now()}|${cart?.totalQuantity ?? 0}`);
1938
+ setStored(seenKey(), `${Date.now()}|${cart?.totalQuantity ?? 0}`);
1487
1939
  announce(reason);
1488
1940
  return cart;
1489
1941
  }
@@ -1491,14 +1943,15 @@ ${fragment}`, {
1491
1943
  status = outcome.kind === "loaded" ? statusOf(outcome.cart) : { kind: "error", error: outcome.error, cart: null };
1492
1944
  announce("load");
1493
1945
  }
1494
- function setFailure(error) {
1495
- const cart = currentCart();
1496
- status = error == null ? statusOf(cart) : { kind: "error", error, cart };
1946
+ function lastKnownCount() {
1947
+ const text2 = getStored(seenKey())?.split("|")[1] ?? "";
1948
+ const count = Number(text2);
1949
+ return /^\d+$/.test(text2) && Number.isSafeInteger(count) ? count : null;
1497
1950
  }
1498
1951
  function showLastKnownCount() {
1499
1952
  if (status.kind !== "unknown") return;
1500
- const count = readStorageIfAllowed(seenKey())?.split("|")[1];
1501
- if (count) showSavedCount(count);
1953
+ const count = lastKnownCount();
1954
+ if (count !== null) showSavedCount(count);
1502
1955
  }
1503
1956
  function setWarnings(notes) {
1504
1957
  warnings = notes.filter((note, index) => notes.findIndex((other) => other.message === note.message) === index).map((note) => ({ ...note, message: cfg.warningText?.(note) || note.message }));
@@ -1536,7 +1989,7 @@ ${fragment}`, {
1536
1989
  open: isOpen(),
1537
1990
  count: currentCart()?.totalQuantity ?? 0,
1538
1991
  loaded: status.kind !== "unknown",
1539
- warnings
1992
+ warnings: [...warnings]
1540
1993
  });
1541
1994
  function subscribe(listener) {
1542
1995
  const events = ["cart-updated", "cart-toggled"].map((name) => `${cfg.prefix}:${name}`);
@@ -1554,15 +2007,19 @@ ${fragment}`, {
1554
2007
  };
1555
2008
  }
1556
2009
 
1557
- // src/cart/mutations.js
2010
+ // src/cart/mutations.ts
1558
2011
  var UNKNOWN_CART_MESSAGE = /does not exist|not found|invalid id/i;
2012
+ var isPayloadOf = (name) => shaped({ [name]: orNull({ cart: orNull(CART) }) });
1559
2013
  function isCartGone(error) {
1560
2014
  if (!(error instanceof TetherError)) return false;
1561
2015
  if (error.userErrors?.some((userError) => userError.field?.[0] === "cartId")) return true;
1562
2016
  return (error.errors ?? []).some((graphQLError) => UNKNOWN_CART_MESSAGE.test(graphQLError.message));
1563
2017
  }
2018
+ function isUnclearFailure(error) {
2019
+ return error instanceof TetherError && (error.code === "NETWORK" || error.code === "INVALID_RESPONSE" || (error.status ?? 0) >= 500);
2020
+ }
1564
2021
  function toLineInput(input, quantity = 1, options = {}) {
1565
- const spec = typeof input === "object" && input !== null ? input : { ...options, variantId: input, quantity };
2022
+ const spec = typeof input === "object" && input !== null ? input : { ...options, quantity, ...input === null || input === void 0 ? {} : { variantId: input } };
1566
2023
  const merchandiseId = GID.variant(spec.merchandiseId || spec.variantId || spec.id);
1567
2024
  if (!merchandiseId) throw new TetherError("add(): variantId is required");
1568
2025
  const asked = parseInt(String(spec.quantity ?? 1), 10);
@@ -1574,23 +2031,25 @@ ${fragment}`, {
1574
2031
  return line;
1575
2032
  }
1576
2033
  function mutationText(name, args, inner) {
1577
- return `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}warnings{code message target}}}${cartFragment()}`;
2034
+ return (access) => `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}warnings{code message target}}}${cartFragment(access)}`;
1578
2035
  }
1579
2036
  async function send(name, args, inner, variables) {
1580
- const data = await gql(mutationText(name, args, inner), variables);
2037
+ const data = await gql(mutationText(name, args, inner), variables, isPayloadOf(name), { operation: "mutation" });
1581
2038
  const payload = unwrap(data[name], name);
1582
2039
  setWarnings(payload.warnings ?? []);
1583
2040
  return payload.cart;
1584
2041
  }
1585
2042
  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;
2043
+ const withIdentity = cfg.country ? { buyerIdentity: { countryCode: cfg.country }, ...input } : input;
1591
2044
  return send("cartCreate", "$input: CartInput!", "input: $input", { input: withIdentity });
1592
2045
  }
1593
- async function mutateCart({ name, args, inner, variables, createInput }) {
2046
+ async function mutateCart({
2047
+ name,
2048
+ args,
2049
+ inner,
2050
+ variables,
2051
+ createInput
2052
+ }) {
1594
2053
  const cartId = savedCartId();
1595
2054
  if (!cartId) return createInput ? createCart(createInput) : null;
1596
2055
  try {
@@ -1604,11 +2063,19 @@ ${fragment}`, {
1604
2063
  }
1605
2064
  }
1606
2065
  async function fetchCart(id) {
1607
- const data = await gql(`query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${cartFragment()}`, { id });
2066
+ const data = await gql(
2067
+ (access) => `query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${cartFragment(access)}`,
2068
+ { id },
2069
+ shaped({ cart: orNull(CART) })
2070
+ );
1608
2071
  return data.cart;
1609
2072
  }
1610
2073
  async function fetchCheckoutUrl(id) {
1611
- const data = await gql("query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }", { id });
2074
+ const data = await gql(
2075
+ "query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }",
2076
+ { id },
2077
+ shaped({ cart: orNull({ checkoutUrl: "string" }) })
2078
+ );
1612
2079
  return data.cart?.checkoutUrl ?? null;
1613
2080
  }
1614
2081
  function toLineUpdate(lineId, patch) {
@@ -1638,21 +2105,20 @@ ${fragment}`, {
1638
2105
  });
1639
2106
  var setLineQuantity = (lineId, target) => target > 0 ? updateLines([{ id: lineId, quantity: target }]) : removeLines([lineId]);
1640
2107
 
1641
- // src/cart/checkout.js
2108
+ // src/cart/checkout.ts
2109
+ var resolveAccountCheckout = null;
2110
+ function setCheckoutResolver(resolver) {
2111
+ resolveAccountCheckout = resolver;
2112
+ }
1642
2113
  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 })
2114
+ (url2) => ({ kind: "url", url: url2 }),
2115
+ (error) => ({ kind: "failed", error })
1651
2116
  );
1652
2117
  async function accountCheckoutUrl() {
1653
- if (!customer.beforeCheckout) return null;
2118
+ if (!resolveAccountCheckout) return null;
1654
2119
  try {
1655
- return await customer.beforeCheckout();
2120
+ const url2 = await resolveAccountCheckout();
2121
+ return url2 && `${url2}${url2.includes("?") ? "&" : "?"}logged_in=true`;
1656
2122
  } catch (error) {
1657
2123
  reportError(error, "account checkout");
1658
2124
  return null;
@@ -1660,15 +2126,12 @@ ${fragment}`, {
1660
2126
  }
1661
2127
  async function checkoutUrl(cartId) {
1662
2128
  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`;
2129
+ const accountUrl = await accountCheckoutUrl();
2130
+ if (accountUrl) return accountUrl;
2131
+ const answer = await cartAnswer;
2132
+ if (answer.kind === "failed") throw answer.error;
2133
+ if (!answer.url) throw new TetherError("No checkoutUrl (cart empty or expired)");
2134
+ return answer.url;
1672
2135
  }
1673
2136
  function detachOpenerIfAllowed(tab) {
1674
2137
  try {
@@ -1683,12 +2146,12 @@ ${fragment}`, {
1683
2146
  const tab = target === "_blank" ? window.open("", "_blank") : null;
1684
2147
  if (tab) detachOpenerIfAllowed(tab);
1685
2148
  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;
2149
+ const url2 = await checkoutUrl(cartId);
2150
+ if (!isHttpsUrl(url2)) throw new TetherError("The checkout URL is not https: not opened");
2151
+ emit("checkout", { url: url2, cart: currentCart() });
2152
+ if (tab && !tab.closed) tab.location.href = url2;
2153
+ else window.location.href = url2;
2154
+ return url2;
1692
2155
  } catch (error) {
1693
2156
  tab?.close();
1694
2157
  reportError(error, "checkout failed");
@@ -1696,8 +2159,27 @@ ${fragment}`, {
1696
2159
  }
1697
2160
  }
1698
2161
 
1699
- // src/cart.js
2162
+ // src/cart.ts
1700
2163
  var currentLines = () => currentCart()?.lines.nodes ?? [];
2164
+ async function reloadAfterUnclearFailure() {
2165
+ const id = savedCartId();
2166
+ if (!id) return;
2167
+ try {
2168
+ const cart = await fetchCart(id);
2169
+ if (!cart) forgetSavedCart();
2170
+ commit(cart, "load");
2171
+ } catch (error) {
2172
+ reportError(error, "cart reload after a failed change");
2173
+ }
2174
+ }
2175
+ function runChange(task) {
2176
+ return enqueue(
2177
+ () => task().catch(async (error) => {
2178
+ if (isUnclearFailure(error)) await reloadAfterUnclearFailure();
2179
+ throw error;
2180
+ })
2181
+ );
2182
+ }
1701
2183
  function changeQuantity(lineId, delta) {
1702
2184
  const line = currentLines().find((candidate) => candidate.id === lineId);
1703
2185
  if (!line) return Promise.resolve(currentCart());
@@ -1706,7 +2188,7 @@ ${fragment}`, {
1706
2188
  pendingQuantities.set(lineId, job);
1707
2189
  announce("pending");
1708
2190
  const isLatest = () => pendingQuantities.get(lineId) === job;
1709
- const sent = enqueue(async () => {
2191
+ const sent = runChange(async () => {
1710
2192
  if (!isLatest()) return currentCart();
1711
2193
  const now = currentLines().find((candidate) => candidate.id === lineId);
1712
2194
  if (now?.quantity === target) return currentCart();
@@ -1753,84 +2235,68 @@ ${fragment}`, {
1753
2235
  }
1754
2236
  var Cart = {
1755
2237
  init,
1756
- /** @returns {string | null} The saved cart's id. */
2238
+ /** The saved cart's id. */
1757
2239
  id: savedCartId,
1758
- /** @returns {CartStatus} */
1759
2240
  get status() {
1760
2241
  return cartStatus();
1761
2242
  },
1762
- /** @returns {ShopifyCart | null} */
1763
2243
  get state() {
1764
2244
  return currentCart();
1765
2245
  },
1766
- /** @returns {readonly ShopifyCartLine[]} */
2246
+ /** The cart's lines, a copy: changing it changes nothing in the cart. */
1767
2247
  get lines() {
1768
- return currentLines();
2248
+ return [...currentLines()];
1769
2249
  },
1770
- /** @returns {number} */
1771
2250
  get count() {
1772
2251
  return currentCart()?.totalQuantity ?? 0;
1773
2252
  },
1774
- /** @returns {boolean} True once the saved cart, or that there is none, is known. */
2253
+ /** True once the saved cart, or that there is none, is known. */
1775
2254
  get loaded() {
1776
2255
  return cartStatus().kind !== "unknown";
1777
2256
  },
1778
- /** @returns {unknown} The failure of the last load or change, until a change succeeds. */
2257
+ /**
2258
+ * The failure of the last load or change, until a change succeeds. Requests fail with an `Error`; anything else
2259
+ * thrown is wrapped, so the public type holds.
2260
+ */
1779
2261
  get lastError() {
1780
2262
  const current = cartStatus();
1781
- return current.kind === "error" ? current.error : null;
2263
+ if (current.kind !== "error") return null;
2264
+ return current.error instanceof Error ? current.error : new Error(String(current.error));
1782
2265
  },
1783
- set lastError(error) {
1784
- setFailure(error);
1785
- },
1786
- /** @returns {readonly CartWarning[]} Shopify's notes on the last change. */
2266
+ /** Shopify's notes on the last change, a copy. */
1787
2267
  get warnings() {
1788
- return currentWarnings();
2268
+ return [...currentWarnings()];
1789
2269
  },
1790
- /** @returns {ReadonlyMap<string, { quantity: number }>} Line id → the quantity quick clicks asked for. */
2270
+ /** Line id → the quantity quick clicks asked for. */
1791
2271
  get pendingQty() {
1792
2272
  return pendingQuantities;
1793
2273
  },
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
2274
  /**
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.
2275
+ * Sets where a logged-in customer checks out, or clears it with `null`: the account part's resolver answers the
2276
+ * customer's checkout URL for the cart (`null` uses the cart's own).
1813
2277
  */
1814
- setCustomer(link) {
1815
- if (link.accessToken !== void 0) customer.accessToken = link.accessToken;
1816
- if (link.beforeCheckout !== void 0) customer.beforeCheckout = link.beforeCheckout;
2278
+ setCheckoutResolver,
2279
+ /**
2280
+ * Forgets the saved cart on this device, as if there were none: the page shows an empty cart and
2281
+ * `tether:cart-updated` fires with reason `load`. The next add starts a new cart.
2282
+ */
2283
+ forgetSaved() {
2284
+ forgetSavedCart();
2285
+ settleLoad({ kind: "loaded", cart: null });
1817
2286
  },
1818
- /** Keeps a cart and shows it, as a change does; tests seed the cart with it. */
1819
- _commit: commit,
1820
2287
  render,
1821
2288
  subscribe,
1822
2289
  /**
1823
2290
  * `add(variantId, 2)`, `add({ variantId, quantity, sellingPlanId, attributes })`, or `add([line, line])` for
1824
2291
  * bundles in one request. Creates the cart when there is none.
1825
2292
  *
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>}
2293
+ * @throws {TetherError} (rejected) When the add fails, or Shopify answers it without a cart.
1830
2294
  */
1831
2295
  add(input, quantity = 1, options = {}) {
1832
- const lines = (Array.isArray(input) ? input : [input]).map((line) => toLineInput(line, quantity, options));
1833
- return enqueue(async () => {
2296
+ const lines = (Array.isArray(input) ? input : [input]).map(
2297
+ (line) => toLineInput(line, quantity, options)
2298
+ );
2299
+ return runChange(async () => {
1834
2300
  const cart = await mutateCart({
1835
2301
  name: "cartLinesAdd",
1836
2302
  args: "$lines: [CartLineInput!]!",
@@ -1838,6 +2304,7 @@ ${fragment}`, {
1838
2304
  variables: { lines },
1839
2305
  createInput: { lines }
1840
2306
  });
2307
+ if (!cart) throw new TetherError("Shopify answered the add without a cart");
1841
2308
  commit(cart, "add");
1842
2309
  emit("added", { cart, lines });
1843
2310
  if (cfg.openCartOnAdd) openCart();
@@ -1847,43 +2314,29 @@ ${fragment}`, {
1847
2314
  /**
1848
2315
  * `updateLine(lineId, 3)` or `updateLine(lineId, { quantity, sellingPlanId, attributes })`.
1849
2316
  *
1850
- * @param {string} lineId
1851
- * @param {number | LinePatch} patch A quantity of 0 or less removes the line.
1852
- * @returns {Promise<ShopifyCart | null>}
2317
+ * @param patch A quantity of 0 or less removes the line.
1853
2318
  */
1854
2319
  updateLine(lineId, patch) {
1855
2320
  const quantity = typeof patch === "number" ? patch : patch.quantity;
1856
2321
  if (quantity !== void 0 && quantity <= 0) return Cart.removeLine(lineId);
1857
2322
  const line = toLineUpdate(lineId, patch);
1858
- return enqueue(async () => commit(await updateLines([line]), "update"));
2323
+ return runChange(async () => commit(await updateLines([line]), "update"));
1859
2324
  },
1860
2325
  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
- */
2326
+ /** The quantity to show: the one asked for while an update is on its way. */
1865
2327
  quantityOf: pendingQuantityOf,
1866
- /**
1867
- * @param {string | string[]} lineIds
1868
- * @returns {Promise<ShopifyCart | null>}
1869
- */
1870
2328
  removeLine(lineIds) {
1871
2329
  const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
1872
- return enqueue(async () => commit(await removeLines(ids), "remove"));
2330
+ return runChange(async () => commit(await removeLines(ids), "remove"));
1873
2331
  },
1874
- /** @returns {Promise<ShopifyCart | null>} */
1875
2332
  clear() {
1876
2333
  const ids = currentLines().map((line) => line.id);
1877
2334
  if (!ids.length) return Promise.resolve(currentCart());
1878
2335
  return Cart.removeLine(ids);
1879
2336
  },
1880
- /**
1881
- * @param {string | null | undefined} note
1882
- * @returns {Promise<ShopifyCart | null>}
1883
- */
1884
2337
  setNote(note) {
1885
2338
  const variables = { note: note ?? "" };
1886
- return enqueue(
2339
+ return runChange(
1887
2340
  async () => commit(
1888
2341
  await mutateCart({
1889
2342
  name: "cartNoteUpdate",
@@ -1899,11 +2352,10 @@ ${fragment}`, {
1899
2352
  /**
1900
2353
  * Cart attributes, merged with the cart's (as it is when the change runs, so quick calls don't drop each other).
1901
2354
  *
1902
- * @param {Attributes} attributes The value `''` removes a key.
1903
- * @returns {Promise<ShopifyCart | null>}
2355
+ * @param attributes The value `''` removes a key.
1904
2356
  */
1905
2357
  setAttributes(attributes) {
1906
- return enqueue(async () => {
2358
+ return runChange(async () => {
1907
2359
  const variables = { attributes: mergedAttributes(attributes) };
1908
2360
  const cart = await mutateCart({
1909
2361
  name: "cartAttributesUpdate",
@@ -1915,14 +2367,9 @@ ${fragment}`, {
1915
2367
  return commit(cart, "attributes");
1916
2368
  });
1917
2369
  },
1918
- /**
1919
- * Replaces the discount codes; `[]` removes them all.
1920
- *
1921
- * @param {string[] | string | ((current: string[]) => string[])} codes
1922
- * @returns {Promise<ShopifyCart | null>}
1923
- */
2370
+ /** Replaces the discount codes; `[]` removes them all. */
1924
2371
  setDiscountCodes(codes) {
1925
- return enqueue(async () => {
2372
+ return runChange(async () => {
1926
2373
  const variables = { discountCodes: discountCodesFrom(codes) };
1927
2374
  const cart = await mutateCart({
1928
2375
  name: "cartDiscountCodesUpdate",
@@ -1937,8 +2384,6 @@ ${fragment}`, {
1937
2384
  /**
1938
2385
  * Adds a code. A code Shopify doesn't accept is removed again, so it doesn't warn on every later change.
1939
2386
  *
1940
- * @param {string} code
1941
- * @returns {Promise<ShopifyCart | null>}
1942
2387
  * @throws {TetherError} `INVALID_DISCOUNT` (rejected) when Shopify doesn't accept the code.
1943
2388
  */
1944
2389
  async applyDiscount(code) {
@@ -1953,10 +2398,6 @@ ${fragment}`, {
1953
2398
  }
1954
2399
  throw new TetherError(`Discount code not valid: ${code}`, { code: "INVALID_DISCOUNT" });
1955
2400
  },
1956
- /**
1957
- * @param {string} code
1958
- * @returns {Promise<ShopifyCart | null>}
1959
- */
1960
2401
  removeDiscount(code) {
1961
2402
  const unwanted = code.toLowerCase();
1962
2403
  return Cart.setDiscountCodes((current) => current.filter((existing) => existing.toLowerCase() !== unwanted));
@@ -1964,12 +2405,10 @@ ${fragment}`, {
1964
2405
  /**
1965
2406
  * Sets who is buying; a country changes the cart's currency and prices to that market's.
1966
2407
  *
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>}
2408
+ * @param options `create: false` forgets an expired cart instead of creating an empty one.
1970
2409
  */
1971
2410
  setBuyerIdentity(buyerIdentity, { create = true } = {}) {
1972
- return enqueue(async () => {
2411
+ return runChange(async () => {
1973
2412
  const cart = await mutateCart({
1974
2413
  name: "cartBuyerIdentityUpdate",
1975
2414
  args: "$buyerIdentity: CartBuyerIdentityInput!",
@@ -1991,22 +2430,8 @@ ${fragment}`, {
1991
2430
  toggle: toggleCart
1992
2431
  };
1993
2432
 
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
- });
2433
+ // src/product-state.ts
2434
+ var PERCENT2 = 100;
2010
2435
  var timesQuantity = (money, quantity) => fromMinorUnits(toMinorUnits(money) * quantity, money.currencyCode);
2011
2436
  var selectionOf = (variant) => Object.fromEntries(variant.selectedOptions.map((option) => [option.name, option.value]));
2012
2437
  function findVariant(product, selected) {
@@ -2021,14 +2446,18 @@ ${fragment}`, {
2021
2446
  )
2022
2447
  );
2023
2448
  }
2024
- function normalisePurchase(product, variant, wanted) {
2449
+ var purchasePlanId = (purchase) => purchase.kind === "subscription" ? purchase.planId : purchase.offeredPlanId;
2450
+ function linePurchase(purchase, variant) {
2451
+ if (purchase.kind === "subscription" && allocationFor(variant, purchase.planId)) return purchase;
2452
+ return { kind: "one-time" };
2453
+ }
2454
+ function normalisePurchase(product, variant, kind, preferredPlanId) {
2025
2455
  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 };
2456
+ const known = plans.find((plan) => plan.id === preferredPlanId) ?? plans[0];
2457
+ if (!known) return { kind: "one-time", offeredPlanId: null };
2028
2458
  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 };
2459
+ if (product.requiresSellingPlan || kind === "subscription") return { kind: "subscription", planId: onVariant.id };
2460
+ return { kind: "one-time", offeredPlanId: onVariant.id };
2032
2461
  }
2033
2462
  function initialChoice(product, { variant, sellingPlan, defaultPlan, defaultPurchase }) {
2034
2463
  const variants = product.variants.nodes;
@@ -2039,13 +2468,14 @@ ${fragment}`, {
2039
2468
  const kind = fromUrl || defaultPurchase === "subscription" ? "subscription" : "one-time";
2040
2469
  return {
2041
2470
  selected: start ? selectionOf(start) : {},
2042
- purchase: normalisePurchase(product, start, { kind, planId: offered?.id ?? null })
2471
+ purchase: normalisePurchase(product, start, kind, offered?.id ?? null)
2043
2472
  };
2044
2473
  }
2045
2474
  function showConditions(variant, lowStock, variantCount, sale, requiresSellingPlan) {
2046
2475
  const stock = variant.quantityAvailable;
2047
2476
  const low = stock != null && stock > 0 && stock <= lowStock;
2048
2477
  const onSale = sale.compareAtPrice !== null;
2478
+ const onPlan = sale.line.kind === "subscription";
2049
2479
  return {
2050
2480
  "in-stock": variant.availableForSale,
2051
2481
  "out-of-stock": !variant.availableForSale,
@@ -2060,23 +2490,24 @@ ${fragment}`, {
2060
2490
  "subscription-unavailable": !sale.subscriptionAvailable,
2061
2491
  "subscription-only": requiresSellingPlan,
2062
2492
  "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
2493
+ "subscription-selected": onPlan,
2494
+ "one-time-selected": !onPlan,
2495
+ "has-savings": onPlan ? sale.savings.percentage > 0 : onSale
2066
2496
  };
2067
2497
  }
2068
2498
  function productView(product, choice, lowStock) {
2069
2499
  const exact = findVariant(product, choice.selected);
2070
2500
  const variant = exact ?? product.variants.nodes[0];
2071
2501
  if (!variant) return null;
2072
- const purchase = normalisePurchase(product, variant, choice.purchase);
2073
- const { planId } = purchase;
2502
+ const purchase = normalisePurchase(product, variant, choice.purchase.kind, purchasePlanId(choice.purchase));
2503
+ const planId = purchasePlanId(purchase);
2074
2504
  const plans = allPlans(product);
2075
- const allocation = purchase.kind === "subscription" ? allocationFor(variant, planId) : null;
2505
+ const line = linePurchase(purchase, variant);
2506
+ const allocation = line.kind === "subscription" ? allocationFor(variant, line.planId) : null;
2076
2507
  const price = allocation ? planPrice(variant, planId) ?? variant.price : variant.price;
2077
2508
  const compareAt = allocation?.priceAdjustments[0]?.compareAtPrice ?? variant.compareAtPrice;
2078
2509
  const sale = {
2079
- isSubscription: allocation !== null,
2510
+ line,
2080
2511
  subscriptionAvailable: plans.some((plan) => allocationFor(variant, plan.id)),
2081
2512
  compareAtPrice: compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount) ? compareAt : null,
2082
2513
  savings: planSavings(variant, planId)
@@ -2094,15 +2525,15 @@ ${fragment}`, {
2094
2525
  }
2095
2526
  function variantBindTexts(view, format) {
2096
2527
  const { variant, plan, price, compareAtPrice, quantity } = view;
2097
- const planId = view.purchase.planId;
2528
+ const planId = purchasePlanId(view.purchase);
2098
2529
  const forQuantity = (money) => timesQuantity(money, Math.max(1, quantity));
2099
2530
  const checkoutCharge = () => {
2100
- const charge = view.isSubscription ? planCheckoutCharge(variant, planId) ?? price : price;
2531
+ const charge = view.line.kind === "subscription" ? planCheckoutCharge(variant, planId) ?? price : price;
2101
2532
  return format.money(forQuantity(charge));
2102
2533
  };
2103
2534
  const discountPercentage = () => {
2104
2535
  if (!compareAtPrice) return "0%";
2105
- return `${Math.round((1 - parseFloat(price.amount) / parseFloat(compareAtPrice.amount)) * PERCENT)}%`;
2536
+ return `${Math.round((1 - parseFloat(price.amount) / parseFloat(compareAtPrice.amount)) * PERCENT2)}%`;
2106
2537
  };
2107
2538
  const discountAmount = () => {
2108
2539
  if (!compareAtPrice) return "";
@@ -2151,7 +2582,7 @@ ${fragment}`, {
2151
2582
  return new Map(Object.entries(texts));
2152
2583
  }
2153
2584
 
2154
- // src/product-render.js
2585
+ // src/product-render.ts
2155
2586
  var NTH_IMAGE_BIND = /^image:(\d+)$/;
2156
2587
  var METAFIELD_BIND = "metafield:";
2157
2588
  var MULTI_LINE_METAFIELD = /^(rich_text|multi_line_text)_field$/;
@@ -2172,7 +2603,7 @@ ${fragment}`, {
2172
2603
  const dot = ref.indexOf(".");
2173
2604
  const namespace = ref.slice(0, dot);
2174
2605
  const key = ref.slice(dot + 1);
2175
- const metafield = (product.metafields ?? []).find((field) => field?.namespace === namespace && field.key === key);
2606
+ const metafield = (product.metafields ?? []).find((field2) => field2?.namespace === namespace && field2.key === key);
2176
2607
  if (!metafield) {
2177
2608
  setText(el, "");
2178
2609
  return;
@@ -2205,9 +2636,9 @@ ${fragment}`, {
2205
2636
  });
2206
2637
  }
2207
2638
  function renderProductBind(el, key, product) {
2208
- const text = PRODUCT_TEXTS.get(key);
2209
- if (text) {
2210
- setText(el, text(product));
2639
+ const text2 = PRODUCT_TEXTS.get(key);
2640
+ if (text2) {
2641
+ setText(el, text2(product));
2211
2642
  return;
2212
2643
  }
2213
2644
  switch (key) {
@@ -2218,10 +2649,12 @@ ${fragment}`, {
2218
2649
  setImage(el, product.featuredImage?.url, product.featuredImage?.altText || product.title);
2219
2650
  break;
2220
2651
  case "url":
2221
- case "link":
2222
- el.setAttribute("href", productUrl2(product.handle, product.id));
2652
+ case "link": {
2653
+ const url2 = productUrl2(product.handle, product.id);
2654
+ if (url2) el.setAttribute("href", url2);
2223
2655
  if (key === "url" && !el.firstElementChild && !el.textContent.trim()) setText(el, product.title);
2224
2656
  break;
2657
+ }
2225
2658
  case "images":
2226
2659
  renderGallery(el, product);
2227
2660
  break;
@@ -2252,14 +2685,11 @@ ${fragment}`, {
2252
2685
  }
2253
2686
  repeat(container, option.optionValues, (node, value) => {
2254
2687
  node.setAttribute(attr("option-value"), value.name);
2255
- const text = qsa(node, sel("bind", "option-value"))[0];
2256
- if (text) setText(text, value.name);
2688
+ const text2 = qsa(node, sel("bind", "option-value"))[0];
2689
+ if (text2) setText(text2, value.name);
2257
2690
  else if (!node.firstElementChild) setText(node, value.name);
2258
2691
  const swatchEl = qsa(node, sel("bind", "swatch"))[0];
2259
- const swatch = (
2260
- /** @type {Swatch} */
2261
- value.swatch
2262
- );
2692
+ const { swatch } = value;
2263
2693
  if (!swatchEl || !swatch) return;
2264
2694
  if (swatch.color) swatchEl.style.backgroundColor = swatch.color;
2265
2695
  if (swatch.image?.previewImage) setImage(swatchEl, swatch.image.previewImage.url, value.name);
@@ -2306,16 +2736,7 @@ ${fragment}`, {
2306
2736
  renderPlanLists(scope);
2307
2737
  }
2308
2738
 
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
- });
2739
+ // src/status.ts
2319
2740
  var URL_SOURCE = "url:";
2320
2741
  function decodePathSegment(segment) {
2321
2742
  try {
@@ -2325,17 +2746,17 @@ ${fragment}`, {
2325
2746
  throw error;
2326
2747
  }
2327
2748
  }
2328
- function resolveProductKey(el, location = window.location, name = "product") {
2749
+ function resolveProductKey(el, location2 = window.location, name = "product") {
2329
2750
  const raw = (get(el, name) ?? "").trim();
2330
2751
  if (!raw.toLowerCase().startsWith(URL_SOURCE)) return { key: raw || null, fromUrl: false, raw };
2331
2752
  const source = raw.slice(URL_SOURCE.length).trim();
2332
2753
  let key;
2333
2754
  if (source.toLowerCase() === "path") {
2334
- const segments = location.pathname.split("/").filter(Boolean);
2755
+ const segments = location2.pathname.split("/").filter(Boolean);
2335
2756
  const segment = segments[segments.length - 1];
2336
2757
  key = segment === void 0 ? null : decodePathSegment(segment);
2337
2758
  } else {
2338
- key = new URLSearchParams(location.search).get(source || "handle");
2759
+ key = new URLSearchParams(location2.search).get(source || "handle");
2339
2760
  }
2340
2761
  return { key: key?.trim() || null, fromUrl: true, raw };
2341
2762
  }
@@ -2367,7 +2788,7 @@ ${fragment}`, {
2367
2788
  });
2368
2789
  }
2369
2790
 
2370
- // src/variant-render.js
2791
+ // src/variant-render.ts
2371
2792
  var FORMAT = { money: formatMoney2, percent: formatPercent };
2372
2793
  var isInput2 = (el) => el.tagName === "INPUT";
2373
2794
  function addButtonLabel(button) {
@@ -2400,7 +2821,7 @@ ${fragment}`, {
2400
2821
  });
2401
2822
  }
2402
2823
  function renderPurchaseStates(scope, view) {
2403
- const { variant, purchase, isSubscription } = view;
2824
+ const { variant, purchase, line } = view;
2404
2825
  scope.$(sel("purchase")).forEach((el) => {
2405
2826
  if (isSelect(el)) {
2406
2827
  el.value = purchase.kind;
@@ -2413,17 +2834,17 @@ ${fragment}`, {
2413
2834
  });
2414
2835
  scope.$(sel("plan-value")).forEach((node) => {
2415
2836
  const planId = get(node, "plan-value") ?? "";
2416
- setSelected(node, isSubscription && planId === purchase.planId);
2837
+ setSelected(node, line.kind === "subscription" && planId === line.planId);
2417
2838
  node.classList.toggle("is-unavailable", !allocationFor(variant, planId));
2418
2839
  const plan = scope.plans.find((candidate) => candidate.id === planId);
2419
2840
  const texts = planBindTexts(variant, planId, plan, FORMAT);
2420
2841
  qsa(node, sel("plan-bind")).forEach((el) => {
2421
- const text = texts.get(get(el, "plan-bind") ?? "");
2422
- if (text) setText(el, text());
2842
+ const text2 = texts.get(get(el, "plan-bind") ?? "");
2843
+ if (text2) setText(el, text2());
2423
2844
  });
2424
2845
  });
2425
2846
  selectsIn(scope, sel("plan")).forEach((select) => {
2426
- select.value = isSubscription ? purchase.planId ?? "" : "";
2847
+ select.value = line.kind === "subscription" ? line.planId : "";
2427
2848
  });
2428
2849
  }
2429
2850
  function renderConditions2(scope, view) {
@@ -2447,10 +2868,10 @@ ${fragment}`, {
2447
2868
  const subscribeText = get(button, "subscribe-text");
2448
2869
  if (!soldOutText && !subscribeText) return;
2449
2870
  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);
2871
+ let text2 = original;
2872
+ if (view.line.kind === "subscription" && subscribeText) text2 = subscribeText;
2873
+ if (disabled && soldOutText) text2 = soldOutText;
2874
+ if (button.getAttribute("aria-busy") !== "true") setText(label, text2);
2454
2875
  });
2455
2876
  }
2456
2877
  function writeVariant(scope, view) {
@@ -2463,32 +2884,29 @@ ${fragment}`, {
2463
2884
  setImage(el, variant.image?.url || product.featuredImage?.url, variant.image?.altText || product.title);
2464
2885
  return;
2465
2886
  }
2466
- const text = texts.get(key);
2467
- if (text) setText(el, text());
2887
+ const text2 = texts.get(key);
2888
+ if (text2) setText(el, text2());
2468
2889
  });
2469
2890
  renderOptionStates(scope, view);
2470
2891
  renderPurchaseStates(scope, view);
2471
2892
  renderConditions2(scope, view);
2472
- scope.root.classList.toggle("is-subscription", view.isSubscription);
2473
- scope.root.setAttribute(attr("purchase-mode"), view.isSubscription ? "subscription" : "one-time");
2893
+ scope.root.classList.toggle("is-subscription", view.line.kind === "subscription");
2894
+ scope.root.setAttribute(attr("purchase-mode"), view.line.kind);
2474
2895
  renderAddButtons(scope, view);
2475
2896
  }
2476
2897
 
2477
- // src/product-events.js
2898
+ // src/product-events.ts
2478
2899
  var productSelector = () => sel("product");
2479
2900
  var isField = (el) => el.tagName === "INPUT" || el.tagName === "SELECT" || el.tagName === "TEXTAREA";
2480
2901
  var isElement = (target) => target instanceof window.Element;
2481
2902
  function closestOwn(scope, target, selector) {
2482
- const el = (
2483
- /** @type {HTMLElement | null} */
2484
- target.closest(selector)
2485
- );
2903
+ const el = target.closest(selector);
2486
2904
  return el?.closest(productSelector()) === scope.root ? el : null;
2487
2905
  }
2488
2906
  function onAddClick(scope, button) {
2489
2907
  const { label, original } = addButtonLabel(button);
2490
2908
  const addingText = get(button, "adding-text");
2491
- void busy(button, async () => {
2909
+ busy(button, async () => {
2492
2910
  if (addingText) setText(label, addingText);
2493
2911
  try {
2494
2912
  await scope.addToCart();
@@ -2500,6 +2918,8 @@ ${fragment}`, {
2500
2918
  }
2501
2919
  }).then(() => {
2502
2920
  scope.renderVariant();
2921
+ }).catch((error) => {
2922
+ reportError(error, "add to cart render");
2503
2923
  });
2504
2924
  }
2505
2925
  function onClick(scope, event, target) {
@@ -2519,7 +2939,7 @@ ${fragment}`, {
2519
2939
  const purchase = closestOwn(scope, target, `${sel("purchase")}:not(select)`);
2520
2940
  if (purchase) {
2521
2941
  if (purchase.tagName !== "INPUT") event.preventDefault();
2522
- scope.setPurchase(get(purchase, "purchase"));
2942
+ scope.setPurchase(purchaseKind(get(purchase, "purchase")));
2523
2943
  return;
2524
2944
  }
2525
2945
  const plus = closestOwn(scope, target, sel("qty-plus"));
@@ -2535,6 +2955,7 @@ ${fragment}`, {
2535
2955
  onAddClick(scope, add);
2536
2956
  }
2537
2957
  }
2958
+ var purchaseKind = (value) => value === "subscription" ? "subscription" : "one-time";
2538
2959
  function onChange(scope, target) {
2539
2960
  const value = isField(target) ? target.value : "";
2540
2961
  if (target.matches(`select${sel("option")}`)) {
@@ -2545,7 +2966,7 @@ ${fragment}`, {
2545
2966
  if (value) scope.selectPlan(value);
2546
2967
  else scope.setPurchase("one-time");
2547
2968
  } else if (target.matches(`select${sel("purchase")}`)) {
2548
- scope.setPurchase(value);
2969
+ scope.setPurchase(purchaseKind(value));
2549
2970
  } else if (target.matches(sel("qty"))) {
2550
2971
  scope.setQuantity(value);
2551
2972
  }
@@ -2560,7 +2981,7 @@ ${fragment}`, {
2560
2981
  });
2561
2982
  }
2562
2983
 
2563
- // src/product.js
2984
+ // src/product.ts
2564
2985
  var scopes = /* @__PURE__ */ new WeakMap();
2565
2986
  var productSelector2 = () => sel("product");
2566
2987
  var knownOptionNames = /* @__PURE__ */ new Map();
@@ -2585,11 +3006,15 @@ ${fragment}`, {
2585
3006
  return isField2(el) && el.value || null;
2586
3007
  }
2587
3008
  var ProductScope = class {
2588
- /**
2589
- * @param {HTMLElement} root
2590
- * @param {Product} product
2591
- */
2592
3009
  constructor(root, product) {
3010
+ __publicField(this, "root");
3011
+ __publicField(this, "product");
3012
+ __publicField(this, "plans");
3013
+ __publicField(this, "selected");
3014
+ /** How the visitor buys; `setPurchase` and `selectPlan` change it. */
3015
+ __publicField(this, "purchase");
3016
+ __publicField(this, "variant");
3017
+ __publicField(this, "quantity");
2593
3018
  this.root = root;
2594
3019
  this.product = product;
2595
3020
  this.plans = allPlans(product);
@@ -2601,7 +3026,7 @@ ${fragment}`, {
2601
3026
  defaultPurchase: get(root, "default-purchase") || cfg.defaultPurchase
2602
3027
  });
2603
3028
  this.selected = choice.selected;
2604
- this.purchaseChoice = choice.purchase;
3029
+ this.purchase = choice.purchase;
2605
3030
  this.variant = null;
2606
3031
  this.quantity = has(root, "quantity") ? Math.max(0, parseInt(get(root, "quantity") ?? "", 10) || 0) : 1;
2607
3032
  scopes.set(root, this);
@@ -2614,11 +3039,7 @@ ${fragment}`, {
2614
3039
  notifyLayoutChanged("product");
2615
3040
  emit("product-ready", { product, scope: this }, root);
2616
3041
  }
2617
- /**
2618
- * New data for the same product (a country switch). Keeps the visitor's choice.
2619
- *
2620
- * @param {Product} product
2621
- */
3042
+ /** New data for the same product (a country switch). Keeps the visitor's choice. */
2622
3043
  update(product) {
2623
3044
  const variantId = this.variant?.id;
2624
3045
  this.product = product;
@@ -2632,17 +3053,14 @@ ${fragment}`, {
2632
3053
  notifyLayoutChanged("product");
2633
3054
  emit("product-updated", { product, scope: this }, this.root);
2634
3055
  }
2635
- /**
2636
- * @param {string} selector
2637
- * @returns {HTMLElement[]} Matches in this scope, including the root itself (a card link with tether-bind="link").
2638
- */
3056
+ /** @returns Matches in this scope, including the root itself (a card link with tether-bind="link"). */
2639
3057
  $(selector) {
2640
3058
  const inner = scoped(this.root, selector, productSelector2());
2641
3059
  return this.root.matches(selector) ? [this.root, ...inner] : inner;
2642
3060
  }
2643
3061
  /**
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.
3062
+ * @param authored An option name in the markup.
3063
+ * @returns The product's option of that name, or at that position in the build's language.
2646
3064
  */
2647
3065
  optName(authored) {
2648
3066
  if (!authored) return "";
@@ -2651,50 +3069,31 @@ ${fragment}`, {
2651
3069
  const index = (knownOptionNames.get(this.product.handle) ?? []).indexOf(authored);
2652
3070
  return options[index]?.name ?? authored;
2653
3071
  }
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
3072
  get plan() {
2673
- return this.plans.find((plan) => plan.id === this.planId) ?? null;
3073
+ const planId = purchasePlanId(this.purchase);
3074
+ return this.plans.find((plan) => plan.id === planId) ?? null;
2674
3075
  }
2675
3076
  /** True when the current variant can be bought on a plan. */
2676
3077
  get subscriptionAvailable() {
2677
3078
  return this.plans.some((plan) => allocationFor(this.variant, plan.id));
2678
3079
  }
3080
+ /** True when the line is bought on a plan: subscribing, and the variant is on the plan. */
2679
3081
  get isSubscription() {
2680
- return this.purchase === "subscription" && allocationFor(this.variant, this.planId) !== null;
3082
+ return linePurchase(this.purchase, this.variant).kind === "subscription";
2681
3083
  }
2682
- /** @returns {CartLineInput | null} What would be added to the cart right now. */
3084
+ /** @returns What would be added to the cart right now. */
2683
3085
  getLine() {
2684
3086
  if (!this.variant) return null;
2685
3087
  const line = { merchandiseId: this.variant.id, quantity: this.quantity };
2686
- if (this.isSubscription && this.planId) line.sellingPlanId = this.planId;
3088
+ const bought = linePurchase(this.purchase, this.variant);
3089
+ if (bought.kind === "subscription") line.sellingPlanId = bought.planId;
2687
3090
  const attributes = this.collectAttributes();
2688
3091
  if (attributes.length) line.attributes = attributes;
2689
3092
  return line;
2690
3093
  }
2691
- /** @returns {{ key: string, value: string }[]} `tether-line-attributes` and the filled `tether-line-attribute` fields. */
3094
+ /** @returns `tether-line-attributes` and the filled `tether-line-attribute` fields. */
2692
3095
  collectAttributes() {
2693
- const json = (
2694
- /** @type {Parameters<typeof toAttributes>[0]} */
2695
- parseJSONAttr(this.root, "line-attributes")
2696
- );
2697
- const fixed = toAttributes(json);
3096
+ const fixed = attributesFromJson(parseJSONAttr(this.root, "line-attributes"));
2698
3097
  const fields = this.$(sel("line-attribute")).flatMap((el) => {
2699
3098
  const value = lineAttributeValue(el);
2700
3099
  return value === null ? [] : [{ key: get(el, "line-attribute") ?? "", value }];
@@ -2704,9 +3103,6 @@ ${fragment}`, {
2704
3103
  /**
2705
3104
  * Selects an option value. A combination no variant has moves to the closest variant with the value: the most other
2706
3105
  * choices kept, then one for sale.
2707
- *
2708
- * @param {string} name
2709
- * @param {string} value
2710
3106
  */
2711
3107
  select(name, value) {
2712
3108
  if (this.selected[name] === value) return;
@@ -2732,29 +3128,26 @@ ${fragment}`, {
2732
3128
  this.selected = next;
2733
3129
  this.renderVariant();
2734
3130
  }
2735
- /** @param {string} id A variant gid or numeric id. */
3131
+ /** @param id A variant gid or numeric id. */
2736
3132
  selectVariant(id) {
2737
3133
  const variant = this.product.variants.nodes.find((candidate) => candidate.id === GID.variant(id));
2738
3134
  if (!variant) return;
2739
3135
  this.selected = selectionOf(variant);
2740
3136
  this.renderVariant();
2741
3137
  }
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;
3138
+ setPurchase(kind) {
3139
+ const next = normalisePurchase(this.product, this.variant, kind, purchasePlanId(this.purchase));
3140
+ if (next.kind === this.purchase.kind) return;
3141
+ this.purchase = next;
2748
3142
  this.renderVariant();
2749
3143
  }
2750
- /** @param {string | null} ref A plan id, numeric id, name or option value. */
3144
+ /** @param ref A plan id, numeric id, name or option value. */
2751
3145
  selectPlan(ref) {
2752
3146
  const plan = findPlan(this.product, ref);
2753
3147
  if (!plan) return;
2754
- this.purchaseChoice = { kind: "subscription", planId: plan.id };
3148
+ this.purchase = { kind: "subscription", planId: plan.id };
2755
3149
  this.renderVariant();
2756
3150
  }
2757
- /** @param {number | string} quantity */
2758
3151
  setQuantity(quantity) {
2759
3152
  const minimum = has(this.root, "quantity") ? 0 : 1;
2760
3153
  this.quantity = Math.max(minimum, parseInt(String(quantity), 10) || 0);
@@ -2763,7 +3156,7 @@ ${fragment}`, {
2763
3156
  });
2764
3157
  this.renderVariant();
2765
3158
  }
2766
- /** @returns {Promise<unknown>} The cart after adding; `null` when the variant can't be added. */
3159
+ /** @returns The cart after adding; `null` when the variant can't be added. */
2767
3160
  async addToCart() {
2768
3161
  if (!this.variant?.availableForSale) return null;
2769
3162
  const line = this.getLine();
@@ -2789,7 +3182,7 @@ ${fragment}`, {
2789
3182
  renderVariant() {
2790
3183
  const view = productView(
2791
3184
  this.product,
2792
- { selected: this.selected, purchase: this.purchaseChoice, quantity: this.quantity },
3185
+ { selected: this.selected, purchase: this.purchase, quantity: this.quantity },
2793
3186
  cfg.lowStock
2794
3187
  );
2795
3188
  if (!view) {
@@ -2798,7 +3191,7 @@ ${fragment}`, {
2798
3191
  }
2799
3192
  this.variant = view.variant;
2800
3193
  this.selected = view.selected;
2801
- this.purchaseChoice = view.purchase;
3194
+ this.purchase = view.purchase;
2802
3195
  writeVariant(this, view);
2803
3196
  emit(
2804
3197
  "variant-changed",
@@ -2807,32 +3200,28 @@ ${fragment}`, {
2807
3200
  variant: view.variant,
2808
3201
  selected: { ...view.selected },
2809
3202
  purchase: view.purchase.kind,
2810
- sellingPlanId: view.isSubscription ? view.purchase.planId : null
3203
+ sellingPlanId: view.line.kind === "subscription" ? view.line.planId : null
2811
3204
  },
2812
3205
  this.root
2813
3206
  );
2814
3207
  }
2815
3208
  };
2816
3209
 
2817
- // src/collection.js
2818
- var collection_exports = {};
2819
- __export(collection_exports, {
2820
- ListScope: () => ListScope,
2821
- listSelector: () => listSelector
2822
- });
3210
+ // src/collection.ts
2823
3211
  var URL_QUERY_SOURCE = "url:";
2824
3212
  var listSelector = () => `${sel("collection")},${sel("products")},${sel("recommendations")}`;
2825
3213
  var isElement2 = (target) => target instanceof window.Element;
2826
3214
  var toLimit = (raw) => parseInt(raw ?? "", 10) || void 0;
2827
3215
  var toIntent = (raw) => raw === "COMPLEMENTARY" ? "COMPLEMENTARY" : "RELATED";
2828
3216
  var ListScope = class {
2829
- /** @param {HTMLElement} root */
2830
3217
  constructor(root) {
3218
+ __publicField(this, "root");
3219
+ __publicField(this, "products", []);
3220
+ __publicField(this, "pageInfo", null);
3221
+ __publicField(this, "collection", null);
3222
+ /** Counts loads, so only the latest one renders. */
3223
+ __publicField(this, "loadToken", 0);
2831
3224
  this.root = root;
2832
- this.products = [];
2833
- this.pageInfo = null;
2834
- this.collection = null;
2835
- this.loadToken = 0;
2836
3225
  setScopeStatus(root, "loading");
2837
3226
  root.addEventListener("click", (event) => {
2838
3227
  if (!isElement2(event.target)) return;
@@ -2842,31 +3231,29 @@ ${fragment}`, {
2842
3231
  void busy(more, () => this.load(true));
2843
3232
  });
2844
3233
  }
2845
- /** @param {string} selector */
2846
3234
  $(selector) {
2847
3235
  return scoped(this.root, selector, listSelector());
2848
3236
  }
2849
- /** @returns {ProductQuery} The collection or search query of the list's attributes. */
3237
+ /** The collection or search query of the list's attributes. */
2850
3238
  options() {
2851
3239
  const { root } = this;
2852
- const query = { reverse: get(root, "reverse") === "true" };
3240
+ const query2 = { reverse: get(root, "reverse") === "true" };
2853
3241
  const first = toLimit(get(root, "limit"));
2854
- if (first) query.first = first;
3242
+ if (first) query2.first = first;
2855
3243
  const sortKey = get(root, "sort");
2856
- if (sortKey) query.sortKey = sortKey;
3244
+ if (sortKey) query2.sortKey = sortKey;
2857
3245
  if (has(root, "collection")) {
2858
- query.collection = get(root, "collection") ?? "";
2859
- return query;
3246
+ query2.collection = get(root, "collection") ?? "";
3247
+ return query2;
2860
3248
  }
2861
3249
  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;
3250
+ query2.query = raw.startsWith(URL_QUERY_SOURCE) ? new URL(window.location.href).searchParams.get(raw.slice(URL_QUERY_SOURCE.length)) || "" : raw;
3251
+ return query2;
2864
3252
  }
2865
3253
  /**
2866
3254
  * The product of a recommendations list: a handle, `url:…` (one dynamic product page), or empty inside a product
2867
3255
  * scope for that product.
2868
3256
  *
2869
- * @returns {Promise<Product[]>}
2870
3257
  * @throws {TetherError} `NOT_FOUND` when there is no product to recommend for.
2871
3258
  */
2872
3259
  async loadRecommendations() {
@@ -2876,7 +3263,7 @@ ${fragment}`, {
2876
3263
  if (!key) throw new TetherError("No product for recommendations", { code: "NOT_FOUND" });
2877
3264
  return getRecommendations(key, toIntent(get(this.root, "intent")));
2878
3265
  }
2879
- /** @param {boolean} [more] Appends the next page instead of replacing the products. */
3266
+ /** @param more Appends the next page instead of replacing the products. */
2880
3267
  async load(more = false) {
2881
3268
  this.loadToken += 1;
2882
3269
  const token = this.loadToken;
@@ -2942,21 +3329,7 @@ ${fragment}`, {
2942
3329
  }
2943
3330
  };
2944
3331
 
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
- });
3332
+ // src/mounting.ts
2960
3333
  var HYDRATE_SELECTOR = 'script[type="application/json"][data-tether-hydrate]';
2961
3334
  var mountedProducts = /* @__PURE__ */ new WeakSet();
2962
3335
  var mountedLists = /* @__PURE__ */ new WeakSet();
@@ -2972,10 +3345,7 @@ ${fragment}`, {
2972
3345
  });
2973
3346
  }
2974
3347
  function within(root, selector) {
2975
- const self = root.matches?.(selector) ? [
2976
- /** @type {HTMLElement} */
2977
- root
2978
- ] : [];
3348
+ const self = root.matches?.(selector) ? [root] : [];
2979
3349
  return [...self, ...qsa(root, selector)];
2980
3350
  }
2981
3351
  function applyPageTitle(el, product) {
@@ -3056,12 +3426,8 @@ ${fragment}`, {
3056
3426
  function flushRefresh() {
3057
3427
  const keys = [...refreshQueue];
3058
3428
  refreshQueue = /* @__PURE__ */ new Set();
3059
- refreshProducts(keys).forEach((promise, index) => {
3060
- const key = (
3061
- /** @type {string} */
3062
- keys[index]
3063
- );
3064
- promise.then((fresh) => {
3429
+ keys.forEach((key) => {
3430
+ refreshProduct(key).then((fresh) => {
3065
3431
  applyRefresh(key, fresh);
3066
3432
  }).catch((error) => {
3067
3433
  reportError(error, `refresh ${key}`);
@@ -3086,11 +3452,19 @@ ${fragment}`, {
3086
3452
  document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
3087
3453
  });
3088
3454
  }
3089
- var isUsable = (product) => Boolean(product?.id && product.handle);
3090
- var sameCode = (a, b) => (a ?? "").toUpperCase() === (b ?? "").toUpperCase();
3455
+ function toHydratePayload(json) {
3456
+ if (!isRecord(json)) return { country: void 0, language: void 0, products: [] };
3457
+ const { products } = json;
3458
+ const entries = Array.isArray(products) ? products : [];
3459
+ return {
3460
+ country: toCountry(json["country"]) ?? void 0,
3461
+ language: toLanguage(json["language"]) ?? void 0,
3462
+ products: entries.filter(isProductShape)
3463
+ };
3464
+ }
3091
3465
  var readScripts = /* @__PURE__ */ new WeakSet();
3092
3466
  function hydrateFromPage(root = document) {
3093
- qsa(root.ownerDocument ?? root, HYDRATE_SELECTOR).forEach((script) => {
3467
+ within(root, HYDRATE_SELECTOR).forEach((script) => {
3094
3468
  if (readScripts.has(script)) return;
3095
3469
  readScripts.add(script);
3096
3470
  let json;
@@ -3100,14 +3474,10 @@ ${fragment}`, {
3100
3474
  warn("bad data-tether-hydrate", error);
3101
3475
  return;
3102
3476
  }
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);
3477
+ const { country, language, products } = toHydratePayload(json);
3478
+ rememberOptionNames(products);
3479
+ if (country === cfg.country && language === cfg.language) {
3480
+ hydrateProducts(products);
3111
3481
  return;
3112
3482
  }
3113
3483
  log("hydration skipped: other market");
@@ -3118,93 +3488,12 @@ ${fragment}`, {
3118
3488
  });
3119
3489
  }
3120
3490
 
3121
- // src/lazy-iife.js
3122
- var { cfg: cfg2 } = config_exports;
3123
- 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
3138
- };
3139
- var MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
3140
- var coreScript = typeof document === "undefined" ? "" : document.currentScript?.getAttribute("src") ?? "";
3141
- var coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : "";
3142
- var parts = /* @__PURE__ */ new Map();
3143
- 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;
3150
- }
3151
- function loadScript(name, url) {
3152
- return new Promise((resolve, reject) => {
3153
- const script = document.createElement("script");
3154
- script.src = url;
3155
- script.async = true;
3156
- 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`));
3164
- };
3165
- script.onerror = () => {
3166
- parts.delete(name);
3167
- reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`));
3168
- };
3169
- document.head.appendChild(script);
3170
- });
3171
- }
3172
- function loadPart(name) {
3173
- let loading = parts.get(name);
3174
- 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)));
3180
- }
3181
- const suffix = MINIFIED_SCRIPT.test(coreScriptUrl) || !coreScriptUrl ? ".min" : "";
3182
- loading = loadScript(name, `${folder.replace(/\/?$/, "/")}tether.${name}${suffix}.js`);
3183
- parts.set(name, loading);
3184
- }
3185
- return (
3186
- /** @type {Promise<Parts[Name]>} */
3187
- loading
3188
- );
3189
- }
3190
- coreModules["lazy.js"] = { loadPart, coreModules };
3191
-
3192
- // src/markets.js
3491
+ // src/markets.ts
3193
3492
  var storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
3194
- function localStorageIfAllowed() {
3195
- if (typeof window === "undefined") return null;
3493
+ var pageStorage = { getItem: (key) => getStored(key) };
3494
+ function parseJsonOrNull(text2) {
3196
3495
  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
- );
3496
+ return JSON.parse(text2);
3208
3497
  } catch (error) {
3209
3498
  if (error instanceof SyntaxError) return null;
3210
3499
  throw error;
@@ -3214,22 +3503,19 @@ ${fragment}`, {
3214
3503
  if (typeof value !== "object" || value === null) return null;
3215
3504
  const country = toCountry(Reflect.get(value, "country"));
3216
3505
  if (!country) return null;
3217
- const locale = (
3218
- /** @type {unknown} */
3219
- Reflect.get(value, "locale")
3220
- );
3506
+ const locale = Reflect.get(value, "locale");
3221
3507
  return {
3222
3508
  country,
3223
3509
  language: toLanguage(Reflect.get(value, "language")) ?? void 0,
3224
3510
  locale: typeof locale === "string" && locale ? locale : void 0
3225
3511
  };
3226
3512
  }
3227
- function readSavedMarket(storage = localStorageIfAllowed()) {
3228
- const text = storage?.getItem(storageKey());
3229
- return text ? toMarket(parseJsonOrNull(text)) : null;
3513
+ function readSavedMarket(storage = pageStorage) {
3514
+ const text2 = storage.getItem(storageKey());
3515
+ return text2 ? toMarket(parseJsonOrNull(text2)) : null;
3230
3516
  }
3231
3517
 
3232
- // src/page-market.js
3518
+ // src/page-market.ts
3233
3519
  var configuredMarket = { country: void 0, language: void 0, locale: void 0 };
3234
3520
  var lastPageHadOwnMarket = false;
3235
3521
  function pageMarket() {
@@ -3266,21 +3552,169 @@ ${fragment}`, {
3266
3552
  if (market.locale && !overrides.locale) cfg.locale = market.locale;
3267
3553
  log("market", market);
3268
3554
  }
3269
- var sameCode2 = (a, b) => (a ?? "").toUpperCase() === (b ?? "").toUpperCase();
3555
+ var sameCode = (a, b) => (a ?? "").toUpperCase() === (b ?? "").toUpperCase();
3270
3556
  function marketForArrival() {
3271
3557
  const page = pageMarket();
3272
3558
  const leftOwnMarket = page === null && lastPageHadOwnMarket;
3273
3559
  lastPageHadOwnMarket = page !== null;
3274
3560
  const target = page ?? (leftOwnMarket ? readSavedMarket() ?? configuredMarket : null);
3275
3561
  if (!target?.country) return null;
3276
- if (sameCode2(target.country, cfg.country) && sameCode2(target.language, cfg.language)) return null;
3562
+ if (sameCode(target.country, cfg.country) && sameCode(target.language, cfg.language)) return null;
3277
3563
  return { country: target.country, language: target.language, locale: target.locale };
3278
3564
  }
3279
3565
  function setCountry(code, options) {
3280
3566
  return loadPart("markets").then((part) => part.setCountry(code, options));
3281
3567
  }
3282
3568
 
3283
- // src/account-api.js
3569
+ // src/part-integrity.ts
3570
+ var PART_INTEGRITY = { "tether.diagnose.js": "sha384-My35ltXsPvspuBHm0xCTu7wl6C4ORwigmKQMCaTKI6VP9ELMCCLYCETpAYW3Fus+", "tether.markets.js": "sha384-m9u+yVeYzqljJYufGt6C6E2xCgqgBSJstvmjU5a2lXZLK3q2saH19EYgP/EC5ox3", "tether.search.js": "sha384-ZrByfRsWNOHbbWOocY4WOD2RWe4QI2qQRZ4Eb+fM5v/UHIQ8mlWxbSwrxJt8yLbc", "tether.extras.js": "sha384-sE3c2fclN/3LKtuYUtC4CjVk76Zu0Mv/jR3p8YYLisAvyDvqJxpx4nj8PbJ3KzXH", "tether.account.js": "sha384-UF36poTg1vnXE7Pg+FYFGMJ4qjKhx9mbOug9bqTYhzgqRppOKS9M/1uY5D/cz9KF", "tether.account-pages.js": "sha384-jJYvZswrRw+pGDYrk0FswWcYqGpZkbR6ooxJGFu0lTYkUlVOcsCXuLLLU6M5kO54", "tether.analytics.js": "sha384-10v5pBMzgfF7BuLGQBg3cdbgMk701Uto5rGjhByiRD2jI9+/WF8DFcSWplIcdRNg" };
3571
+
3572
+ // src/part-registry.ts
3573
+ function isTetherInternals(value) {
3574
+ return isRecord(value) && isRecord(value["__core"]) && isRecord(value["__parts"]);
3575
+ }
3576
+
3577
+ // src/lazy-iife.ts
3578
+ var coreModules = {
3579
+ get "config.ts"() {
3580
+ return [cfg, isConfigured, log, MARKET_SOURCES, parseMarkets, reportError, warn];
3581
+ },
3582
+ get "client.ts"() {
3583
+ return [endpoint, gql, orNull, shaped, TetherError];
3584
+ },
3585
+ get "dom.ts"() {
3586
+ return [
3587
+ ANALYTICS_EVENTS,
3588
+ attr,
3589
+ attributesFromJson,
3590
+ emit,
3591
+ formatDate,
3592
+ formatMoney2,
3593
+ get,
3594
+ has,
3595
+ hasTemplate,
3596
+ isOwnEvent,
3597
+ parseJSONAttr,
3598
+ productUrl2,
3599
+ qsa,
3600
+ repeat,
3601
+ sel,
3602
+ setImage,
3603
+ setText,
3604
+ show
3605
+ ];
3606
+ },
3607
+ get "loader.ts"() {
3608
+ return [clearProductCache, getProduct];
3609
+ },
3610
+ get "selling-plans.ts"() {
3611
+ return [allPlans];
3612
+ },
3613
+ get "status.ts"() {
3614
+ return [boundary, resolveProductKey];
3615
+ },
3616
+ get "cart.ts"() {
3617
+ return [Cart];
3618
+ },
3619
+ get "product.ts"() {
3620
+ return [scopes];
3621
+ },
3622
+ get "collection.ts"() {
3623
+ return [listSelector];
3624
+ },
3625
+ get "mounting.ts"() {
3626
+ return [liveLists, liveProducts, mountedProducts, mountProduct, pruneLive, within];
3627
+ },
3628
+ get "market-codes.ts"() {
3629
+ return [toCountry, toLanguage];
3630
+ },
3631
+ get "page-market.ts"() {
3632
+ return [setCountry];
3633
+ },
3634
+ get "shopify-values.ts"() {
3635
+ return [isHttpsUrl];
3636
+ },
3637
+ // Parts that load other parts (account → account-pages) use this same loader.
3638
+ get "lazy.ts"() {
3639
+ return [loadPart];
3640
+ }
3641
+ };
3642
+ var MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
3643
+ var coreScript = typeof document === "undefined" ? "" : document.currentScript?.getAttribute("src") ?? "";
3644
+ var coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : "";
3645
+ var parts = /* @__PURE__ */ new Map();
3646
+ function partsFolder() {
3647
+ if (cfg.partsUrl) return new URL(cfg.partsUrl, document.baseURI).href;
3648
+ if (!coreScriptUrl) return null;
3649
+ return coreScriptUrl.replace(/[^/]*$/, "");
3650
+ }
3651
+ function applyIntegrity(script, integrity) {
3652
+ script.integrity = integrity;
3653
+ if (new URL(script.src).origin !== window.location.origin) script.crossOrigin = "anonymous";
3654
+ }
3655
+ function loadScript(name, folder, file, integrity) {
3656
+ return new Promise((resolve, reject) => {
3657
+ const url2 = `${folder.replace(/\/?$/, "/")}${file}`;
3658
+ const script = document.createElement("script");
3659
+ script.src = url2;
3660
+ script.async = true;
3661
+ applyIntegrity(script, integrity);
3662
+ script.onload = () => {
3663
+ const tether = Reflect.get(window, "Tether");
3664
+ const part = isTetherInternals(tether) ? tether.__parts[name] : void 0;
3665
+ if (part) {
3666
+ resolve(part);
3667
+ return;
3668
+ }
3669
+ parts.delete(name);
3670
+ const problem = isTetherInternals(tether) ? "did not register" : "can't register: window.Tether is no longer this Tether script's, another script replaced it";
3671
+ reject(new Error(`${url2} ${problem}`));
3672
+ };
3673
+ script.onerror = () => {
3674
+ parts.delete(name);
3675
+ reject(
3676
+ new Error(
3677
+ `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.`
3678
+ )
3679
+ );
3680
+ };
3681
+ document.head.appendChild(script);
3682
+ });
3683
+ }
3684
+ function loadPart(name) {
3685
+ let loading = parts.get(name);
3686
+ if (!loading) {
3687
+ const folder = partsFolder();
3688
+ if (folder === null) {
3689
+ return Promise.reject(
3690
+ new Error(`The Tether script's own location is unknown, so its ${name} part can't be found: set partsUrl.`)
3691
+ );
3692
+ }
3693
+ const suffix = MINIFIED_SCRIPT.test(coreScriptUrl) || !coreScriptUrl ? ".min" : "";
3694
+ const file = `tether.${name}${suffix}.js`;
3695
+ const integrity = PART_INTEGRITY[file];
3696
+ if (integrity === void 0) {
3697
+ return Promise.reject(new Error(`This Tether script was built without ${file}, so it can't load that part.`));
3698
+ }
3699
+ loading = loadScript(name, folder, file, integrity);
3700
+ parts.set(name, loading);
3701
+ }
3702
+ return loading;
3703
+ }
3704
+
3705
+ // src/account-marks.ts
3706
+ var hintKey = () => `${cfg.prefix}:account-hint:${cfg.domain}`;
3707
+ function removeFromBothAreas(key) {
3708
+ removeStored(key, "sessionStorage");
3709
+ removeStored(key, "localStorage");
3710
+ }
3711
+ function forgetBrowserModeTokens() {
3712
+ removeFromBothAreas(`${cfg.prefix}:account:${cfg.domain}`);
3713
+ removeFromBothAreas(`${cfg.prefix}:account-login:${cfg.domain}`);
3714
+ }
3715
+ var isLoginOutcome = (value) => value === "in" || value === "expired" || value === "error";
3716
+
3717
+ // src/account-api.ts
3284
3718
  var ACCOUNT_ATTRIBUTES = [
3285
3719
  "account-login",
3286
3720
  "account-bind",
@@ -3293,20 +3727,12 @@ ${fragment}`, {
3293
3727
  "address-form"
3294
3728
  ];
3295
3729
  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
- }
3730
+ function hasLoginHint() {
3731
+ return Boolean(getStored(hintKey()));
3306
3732
  }
3307
3733
  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();
3734
+ if (!cfg.account) return;
3735
+ const needed = within(root, accountSelector()).length > 0 || isLoginOutcome(new URLSearchParams(window.location.search).get("account")) || hasLoginHint();
3310
3736
  if (!needed) return;
3311
3737
  loadPart("account").then((part) => part.mountAccount(root)).catch((error) => {
3312
3738
  reportError(error, "account");
@@ -3314,22 +3740,14 @@ ${fragment}`, {
3314
3740
  }
3315
3741
  function fromPart(name) {
3316
3742
  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
- );
3743
+ const call = part[name];
3323
3744
  return call(...args);
3324
3745
  });
3325
3746
  }
3326
3747
  var accountApi = {
3327
3748
  login: fromPart("login"),
3328
3749
  logout: fromPart("logout"),
3329
- handleCallback: fromPart("handleCallback"),
3330
- getToken: fromPart("getToken"),
3331
3750
  isLoggedIn: fromPart("isLoggedIn"),
3332
- query: fromPart("query"),
3333
3751
  customer: fromPart("customer"),
3334
3752
  orders: fromPart("orders"),
3335
3753
  order: fromPart("order"),
@@ -3344,7 +3762,7 @@ ${fragment}`, {
3344
3762
  mountAccount: fromPart("mountAccount")
3345
3763
  };
3346
3764
 
3347
- // src/controls/cart-controls.js
3765
+ // src/controls/cart-controls.ts
3348
3766
  var lineKeyAttribute = () => `data-${cfg.prefix}-key`;
3349
3767
  var isElement3 = (target) => target instanceof window.Element;
3350
3768
  function lineOf(target) {
@@ -3441,7 +3859,7 @@ ${fragment}`, {
3441
3859
  document.addEventListener("keydown", onKeydown);
3442
3860
  }
3443
3861
 
3444
- // src/controls/discount.js
3862
+ // src/controls/discount.ts
3445
3863
  var isElement4 = (target) => target instanceof window.Element;
3446
3864
  function codeInputOf(form) {
3447
3865
  const input = form.querySelector(sel("discount-input")) ?? form.querySelector("input");
@@ -3499,7 +3917,7 @@ ${fragment}`, {
3499
3917
  document.addEventListener("click", onClick3);
3500
3918
  }
3501
3919
 
3502
- // src/controls/extras.js
3920
+ // src/controls/extras.ts
3503
3921
  var addGroup = (button) => loadPart("extras").then((part) => part.addGroup(button));
3504
3922
  var reportExtrasError = (error) => {
3505
3923
  reportError(error, "extras");
@@ -3518,6 +3936,7 @@ ${fragment}`, {
3518
3936
  }).catch(reportExtrasError);
3519
3937
  }
3520
3938
  function onVariantChanged(event) {
3939
+ if (!isOwnEvent(event, "variant-changed")) return;
3521
3940
  const { target } = event;
3522
3941
  if (!(target instanceof window.Element)) return;
3523
3942
  if (!cfg.variantUrl && !target.hasAttribute(attr("variant-url"))) return;
@@ -3528,14 +3947,10 @@ ${fragment}`, {
3528
3947
  function bindExtrasControls() {
3529
3948
  document.addEventListener("click", onClick4);
3530
3949
  window.addEventListener("storage", onStorage);
3531
- document.addEventListener(
3532
- `${cfg.prefix}:variant-changed`,
3533
- /** @type {EventListener} */
3534
- onVariantChanged
3535
- );
3950
+ document.addEventListener(`${cfg.prefix}:variant-changed`, onVariantChanged);
3536
3951
  }
3537
3952
 
3538
- // src/controls/navigation.js
3953
+ // src/controls/navigation.ts
3539
3954
  var mountedBody = null;
3540
3955
  function rememberDocumentMount() {
3541
3956
  mountedBody = document.body;
@@ -3556,7 +3971,9 @@ ${fragment}`, {
3556
3971
  }
3557
3972
  function bindAstroRouter(hooks) {
3558
3973
  document.addEventListener("astro:page-load", () => {
3559
- void onPageLoad(hooks);
3974
+ onPageLoad(hooks).catch((error) => {
3975
+ reportError(error, "page mount");
3976
+ });
3560
3977
  });
3561
3978
  document.addEventListener("astro:before-swap", () => {
3562
3979
  mountedBody = null;
@@ -3583,11 +4000,13 @@ ${fragment}`, {
3583
4000
  window.addEventListener("online", onOnline);
3584
4001
  }
3585
4002
 
3586
- // src/index.js
3587
- var VERSION = "0.6.8";
3588
- var ANALYTICS_EVENTS = ["product-ready", "list-ready", "cart-updated", "cart-toggled", "checkout", "search"];
4003
+ // src/index.ts
4004
+ var VERSION = "0.7.1";
3589
4005
  var OBSERVE_DEBOUNCE_MS = 50;
3590
- var pageGlobals = () => window;
4006
+ function pageConfig() {
4007
+ const config = pageGlobal("TETHER_CONFIG");
4008
+ return typeof config === "object" && config !== null ? Object.fromEntries(Object.entries(config)) : {};
4009
+ }
3591
4010
  var initPromise = null;
3592
4011
  var observer = null;
3593
4012
  var analyticsQueue = null;
@@ -3601,10 +4020,7 @@ ${fragment}`, {
3601
4020
  const list = new ListScope(el);
3602
4021
  liveLists.add(list);
3603
4022
  el.addEventListener(`${cfg.prefix}:list-rendered`, (event) => {
3604
- mount(
3605
- /** @type {CustomEvent<{ container: ParentNode }>} */
3606
- event.detail.container
3607
- );
4023
+ if (isOwnEvent(event, "list-rendered")) mount(event.detail.container);
3608
4024
  });
3609
4025
  void trackMarket(list.load());
3610
4026
  });
@@ -3625,21 +4041,39 @@ ${fragment}`, {
3625
4041
  });
3626
4042
  }
3627
4043
  }
3628
- function mount(root = document) {
3629
- if (!isConfigured()) return 0;
3630
- if (root === document) rememberDocumentMount();
3631
- pruneLive();
3632
- hydrateFromPage(root.ownerDocument ?? document);
4044
+ function mountWithin(root) {
3633
4045
  let count = mountLists(root);
3634
4046
  within(root, sel("product")).forEach((el) => {
3635
4047
  if (mountProduct(el)) count++;
3636
4048
  });
3637
4049
  mountParts(root);
4050
+ return count;
4051
+ }
4052
+ function mount(root = document) {
4053
+ if (!isConfigured()) return 0;
4054
+ if (root === document) rememberDocumentMount();
4055
+ pruneLive();
4056
+ hydrateFromPage(root.ownerDocument ?? document);
4057
+ const count = mountWithin(root);
3638
4058
  settleMarket();
3639
4059
  Cart.render();
3640
4060
  log(`mounted ${count} scope(s)`, root);
3641
4061
  return count;
3642
4062
  }
4063
+ function mountAdded(added) {
4064
+ const roots = added.filter(
4065
+ (root) => root.isConnected && !added.some((other) => other !== root && other.contains(root))
4066
+ );
4067
+ if (!roots.length || !isConfigured()) return;
4068
+ pruneLive();
4069
+ roots.forEach((root) => {
4070
+ hydrateFromPage(root);
4071
+ });
4072
+ const count = roots.reduce((sum, root) => sum + mountWithin(root), 0);
4073
+ settleMarket();
4074
+ Cart.render();
4075
+ log(`mounted ${count} scope(s) in ${roots.length} added element(s)`);
4076
+ }
3643
4077
  function unmount() {
3644
4078
  Cart.close();
3645
4079
  }
@@ -3661,13 +4095,13 @@ ${fragment}`, {
3661
4095
  ANALYTICS_EVENTS.forEach((name) => {
3662
4096
  document.addEventListener(`${cfg.prefix}:${name}`, (event) => queue.push(event));
3663
4097
  });
3664
- document.addEventListener("astro:page-load", () => queue.push(new CustomEvent(`${cfg.prefix}:page`)));
4098
+ document.addEventListener("astro:page-load", (event) => queue.push(event));
3665
4099
  loadPart("analytics").then((part) => part.startAnalytics(queue)).catch((error) => {
3666
4100
  reportError(error, "analytics");
3667
4101
  });
3668
4102
  }
3669
4103
  async function cartInPageMarket(cart) {
3670
- const country = cfg.country?.toUpperCase();
4104
+ const country = cfg.country;
3671
4105
  const cartCountry = cart?.buyerIdentity.countryCode;
3672
4106
  if (!cart || !country || !cartCountry || cartCountry === country) return cart;
3673
4107
  try {
@@ -3679,12 +4113,13 @@ ${fragment}`, {
3679
4113
  }
3680
4114
  function init2(overrides = {}) {
3681
4115
  if (initPromise && !Object.keys(overrides).length) return initPromise;
3682
- configure({ ...pageGlobals().TETHER_CONFIG, ...overrides });
4116
+ configure({ ...pageConfig(), ...overrides });
3683
4117
  if (!isConfigured()) {
3684
4118
  warn("TETHER_CONFIG is missing domain or token. Tether is idle.");
3685
4119
  initPromise = Promise.resolve(null);
3686
4120
  return initPromise;
3687
4121
  }
4122
+ forgetBrowserModeTokens();
3688
4123
  bindControls();
3689
4124
  if (cfg.observe) startObserver();
3690
4125
  if (cfg.analytics && !analyticsQueue) startAnalyticsQueue();
@@ -3706,10 +4141,9 @@ ${fragment}`, {
3706
4141
  });
3707
4142
  clearTimeout(timer);
3708
4143
  timer = setTimeout(() => {
3709
- queued.forEach((node) => {
3710
- if (node.isConnected) mount(node);
3711
- });
4144
+ const added = [...queued];
3712
4145
  queued.clear();
4146
+ mountAdded(added);
3713
4147
  }, OBSERVE_DEBOUNCE_MS);
3714
4148
  });
3715
4149
  observer.observe(document.body, { childList: true, subtree: true });
@@ -3718,32 +4152,28 @@ ${fragment}`, {
3718
4152
  var getLocalization = () => loadPart("markets").then((part) => part.getLocalization());
3719
4153
  var diagnose = (options) => loadPart("diagnose").then((part) => part.diagnose({ ...options, version: VERSION }));
3720
4154
  function on(name, listener) {
4155
+ const type = `${cfg.prefix}:${name}`;
3721
4156
  const handler = (event) => {
3722
- const custom = (
3723
- /** @type {CustomEvent} Tether's events are CustomEvents. */
3724
- event
3725
- );
3726
- listener(custom.detail, custom);
4157
+ if (isOwnEvent(event, name)) listener(event.detail, event);
3727
4158
  };
3728
- document.addEventListener(`${cfg.prefix}:${name}`, handler);
4159
+ document.addEventListener(type, handler);
3729
4160
  return () => {
3730
- document.removeEventListener(`${cfg.prefix}:${name}`, handler);
4161
+ document.removeEventListener(type, handler);
3731
4162
  };
3732
4163
  }
3733
- var api = {
4164
+ var publicApi = {
3734
4165
  VERSION,
3735
4166
  init: init2,
3736
4167
  mount,
3737
4168
  unmount,
3738
- /** @returns {TetherConfig} */
4169
+ /** The live config, read-only. */
3739
4170
  get config() {
3740
- return cfg;
4171
+ return configView;
3741
4172
  },
3742
- /** @returns {Promise<TetherApi | null>} */
3743
4173
  get ready() {
3744
4174
  return initPromise ?? Promise.resolve(null);
3745
4175
  },
3746
- query: gql,
4176
+ query,
3747
4177
  withContext,
3748
4178
  cart: Cart,
3749
4179
  getProduct,
@@ -3764,19 +4194,15 @@ ${fragment}`, {
3764
4194
  detectMarket,
3765
4195
  getLocalization,
3766
4196
  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
4197
  on
3774
4198
  };
4199
+ var internals = { __core: coreModules, __parts: {} };
4200
+ var api = Object.assign(publicApi, internals);
3775
4201
  var index_default = api;
3776
4202
  function autoStart() {
3777
- const globals = pageGlobals();
3778
- globals.Tether = api;
3779
- if (!globals.TETHER_CONFIG || globals.TETHER_CONFIG.autoMount === false || globals.TETHER_INTEGRATION) return;
4203
+ Reflect.set(window, "Tether", api);
4204
+ const config = pageGlobal("TETHER_CONFIG");
4205
+ if (!config || field(config, "autoMount") === false || pageGlobal("TETHER_INTEGRATION")) return;
3780
4206
  const start = () => {
3781
4207
  init2().then(() => mount(document)).catch((error) => {
3782
4208
  reportError(error, "init");