@occluze/tether 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2608 +1,2 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
6
-
7
- // src/config.js
8
- var DEFAULTS = {
9
- // Shopify Storefront API
10
- domain: "",
11
- // 'your-store.myshopify.com'
12
- token: "",
13
- // public Storefront API access token
14
- apiVersion: "2026-07",
15
- country: void 0,
16
- // ISO code, e.g. 'NL' -> @inContext(country: NL)
17
- language: void 0,
18
- // e.g. 'NL' -> @inContext(language: NL)
19
- locale: void 0,
20
- // Intl locale for money/dates, e.g. 'nl-NL'
21
- markets: null,
22
- // { detect: true, locales: { de: { country: 'DE', language: 'DE' } }, cookie, remember } (markets.js)
23
- // Markup
24
- prefix: "tether",
25
- // attribute prefix: tether-product, tether-bind, ...
26
- productPath: "/product",
27
- // Webflow CMS template path for products
28
- collectionPath: "/collection",
29
- // Fetching
30
- metafields: [],
31
- // ['custom.material', { namespace, key }]
32
- imagesFirst: 25,
33
- variantsFirst: 100,
34
- sellingPlanGroupsFirst: 10,
35
- sellingPlansFirst: 25,
36
- collectionFirst: 24,
37
- retries: 2,
38
- // Cart
39
- storageKey: null,
40
- // defaults to `tether:cart:<domain>`
41
- cartDrawerOpenClass: "is-open",
42
- openCartOnAdd: true,
43
- checkoutTarget: "_self",
44
- // '_self' or '_blank'
45
- // Subscriptions
46
- defaultPurchase: "one-time",
47
- // 'one-time' | 'subscription'
48
- // Mounting
49
- autoMount: true,
50
- // mount document on DOMContentLoaded
51
- observe: false,
52
- // auto-mount nodes added later (CMS load-more, filters)
53
- // Formatting
54
- formatMoney: null,
55
- // (money, locale) => string
56
- // Hooks (all optional)
57
- onLayoutChange: null,
58
- // (reason) => void. Default refreshes ScrollTrigger if present
59
- onCartOpen: null,
60
- // (drawers) => void, e.g. lock scroll
61
- onCartClose: null,
62
- // (drawers) => void
63
- onLineEnter: null,
64
- // (node) => void, animate a new cart line in
65
- onLineLeave: null,
66
- // (node, done) => void, animate out, then call done()
67
- onValueChange: null,
68
- // (el, previousText) => void, e.g. text scramble
69
- onError: null,
70
- // (error, context) => void
71
- debug: false
72
- };
73
- var cfg = { ...DEFAULTS };
74
- function configure(overrides = {}) {
75
- Object.keys(cfg).forEach((k) => delete cfg[k]);
76
- Object.assign(cfg, DEFAULTS, overrides);
77
- if (cfg.domain) cfg.domain = String(cfg.domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
78
- if (!cfg.storageKey) cfg.storageKey = `${cfg.prefix}:cart:${cfg.domain}`;
79
- if (cfg.apiVersion === "unstable") warn('apiVersion "unstable" changes without notice. Pin a dated version (e.g. 2026-07).');
80
- return cfg;
81
- }
82
- var isConfigured = () => Boolean(cfg.domain && cfg.token);
83
- function log(...a) {
84
- if (cfg.debug) console.log(`[${cfg.prefix}]`, ...a);
85
- }
86
- function warn(...a) {
87
- console.warn(`[${cfg.prefix}]`, ...a);
88
- }
89
- function reportError(err, context) {
90
- warn(context, err && err.message ? err.message : err);
91
- if (typeof cfg.onError === "function") {
92
- try {
93
- cfg.onError(err, context);
94
- } catch {
95
- }
96
- }
97
- }
98
-
99
- // src/client.js
100
- var TetherError = class extends Error {
101
- constructor(message, { status, errors, userErrors, code } = {}) {
102
- super(message);
103
- this.name = "TetherError";
104
- this.code = code || (status ? `HTTP_${status}` : "ERROR");
105
- this.status = status;
106
- this.errors = errors;
107
- this.userErrors = userErrors;
108
- }
109
- };
110
- function endpoint() {
111
- return `https://${cfg.domain}/api/${cfg.apiVersion}/graphql.json`;
112
- }
113
- function withContext(query, ctx = cfg) {
114
- const args = [];
115
- if (ctx.country) args.push(`country: ${String(ctx.country).toUpperCase()}`);
116
- if (ctx.language) args.push(`language: ${String(ctx.language).toUpperCase().replace("-", "_")}`);
117
- if (!args.length || /@inContext/.test(query)) return query;
118
- return query.replace(
119
- /^(\s*(?:query|mutation)\s+\w+\s*(?:\([^)]*\))?)/,
120
- `$1 @inContext(${args.join(", ")})`
121
- );
122
- }
123
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
124
- async function gql(query, variables = {}, { context = true } = {}) {
125
- var _a;
126
- if (!cfg.domain || !cfg.token) {
127
- throw new TetherError("Tether is not configured: domain and token are required", { code: "NOT_CONFIGURED" });
128
- }
129
- const body = JSON.stringify({ query: context ? withContext(query) : query, variables });
130
- let attempt = 0;
131
- for (; ; ) {
132
- let res;
133
- try {
134
- res = await fetch(endpoint(), {
135
- method: "POST",
136
- headers: {
137
- "Content-Type": "application/json",
138
- "X-Shopify-Storefront-Access-Token": cfg.token
139
- },
140
- body
141
- });
142
- } catch (err) {
143
- if (attempt++ < cfg.retries) {
144
- await sleep(300 * 2 ** attempt);
145
- continue;
146
- }
147
- throw new TetherError(`Network error: ${err.message}`, { code: "NETWORK" });
148
- }
149
- if (res.status === 430) throw new TetherError("Shopify security rejection (430)", { status: 430 });
150
- const retryable = res.status === 429 || res.status >= 500;
151
- if (retryable && attempt++ < cfg.retries) {
152
- log("retry", res.status, attempt);
153
- await sleep(300 * 2 ** attempt);
154
- continue;
155
- }
156
- let json;
157
- try {
158
- json = await res.json();
159
- } catch {
160
- throw new TetherError(`Invalid response (${res.status})`, { status: res.status });
161
- }
162
- if ((_a = json.errors) == null ? void 0 : _a.length) {
163
- const inventoryDenied = json.errors.some((e) => /quantityAvailable/.test(e.message || ""));
164
- if (inventoryDenied && /\bquantityAvailable\b/.test(body)) {
165
- log("no inventory permission, retrying without quantityAvailable");
166
- return gql(query.replace(/\bquantityAvailable\b/g, ""), variables, { context });
167
- }
168
- const throttled = json.errors.some((e) => {
169
- var _a2;
170
- return ((_a2 = e.extensions) == null ? void 0 : _a2.code) === "THROTTLED";
171
- });
172
- if (throttled && attempt++ < cfg.retries) {
173
- await sleep(500 * 2 ** attempt);
174
- continue;
175
- }
176
- throw new TetherError(json.errors.map((e) => e.message).join(" | "), {
177
- status: res.status,
178
- errors: json.errors
179
- });
180
- }
181
- if (!res.ok) throw new TetherError(`HTTP ${res.status}`, { status: res.status });
182
- return json.data;
183
- }
184
- }
185
- function unwrap(payload, name) {
186
- if (!payload) throw new TetherError(`${name}: empty response`);
187
- const errs = payload.userErrors || [];
188
- if (errs.length) {
189
- throw new TetherError(errs.map((e) => e.message).join(" | "), { userErrors: errs });
190
- }
191
- return payload;
192
- }
193
-
194
- // src/fragments.js
195
- var MONEY = "amount currencyCode";
196
- var IMAGE = "url altText width height";
197
- function metafieldIdentifiers() {
198
- return (cfg.metafields || []).map((m) => {
199
- if (typeof m === "string") {
200
- const i = m.indexOf(".");
201
- return { namespace: m.slice(0, i), key: m.slice(i + 1) };
202
- }
203
- return m;
204
- }).filter((m) => m.namespace && m.key);
205
- }
206
- function metafieldsField() {
207
- const ids = metafieldIdentifiers();
208
- if (!ids.length) return "";
209
- const list = ids.map((m) => `{namespace: ${JSON.stringify(m.namespace)}, key: ${JSON.stringify(m.key)}}`).join(", ");
210
- return `metafields(identifiers: [${list}]) {
211
- namespace key type value
212
- reference { ... on MediaImage { image { ${IMAGE} } } }
213
- }`;
214
- }
215
- var SELLING_PLAN_FIELDS = `
216
- id name description recurringDeliveries
217
- options { name value }
218
- priceAdjustments {
219
- orderCount
220
- adjustmentValue {
221
- __typename
222
- ... on SellingPlanPercentagePriceAdjustment { adjustmentPercentage }
223
- ... on SellingPlanFixedAmountPriceAdjustment { adjustmentAmount { ${MONEY} } }
224
- ... on SellingPlanFixedPriceAdjustment { price { ${MONEY} } }
225
- }
226
- }`;
227
- var ALLOCATION_FIELDS = `
228
- sellingPlan { id }
229
- checkoutChargeAmount { ${MONEY} }
230
- remainingBalanceChargeAmount { ${MONEY} }
231
- priceAdjustments {
232
- price { ${MONEY} }
233
- compareAtPrice { ${MONEY} }
234
- perDeliveryPrice { ${MONEY} }
235
- unitPrice { ${MONEY} }
236
- }`;
237
- function productFragment() {
238
- return `
239
- fragment TetherProduct on Product {
240
- id handle title description descriptionHtml productType vendor tags
241
- availableForSale publishedAt onlineStoreUrl requiresSellingPlan
242
- seo { title description }
243
- featuredImage { ${IMAGE} }
244
- images(first: ${cfg.imagesFirst}) { nodes { ${IMAGE} } }
245
- options { name optionValues { name swatch { color image { previewImage { ${IMAGE} } } } } }
246
- priceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
247
- compareAtPriceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
248
- ${metafieldsField()}
249
- sellingPlanGroups(first: ${cfg.sellingPlanGroupsFirst}) {
250
- nodes {
251
- name appName
252
- options { name values }
253
- sellingPlans(first: ${cfg.sellingPlansFirst}) { nodes { ${SELLING_PLAN_FIELDS} } }
254
- }
255
- }
256
- variants(first: ${cfg.variantsFirst}) {
257
- nodes {
258
- id title availableForSale currentlyNotInStock quantityAvailable sku barcode
259
- selectedOptions { name value }
260
- image { ${IMAGE} }
261
- price { ${MONEY} }
262
- compareAtPrice { ${MONEY} }
263
- unitPrice { ${MONEY} }
264
- sellingPlanAllocations(first: ${cfg.sellingPlansFirst}) { nodes { ${ALLOCATION_FIELDS} } }
265
- }
266
- }
267
- }`;
268
- }
269
- function cartFragment() {
270
- return `
271
- fragment TetherCart on Cart {
272
- id checkoutUrl totalQuantity note createdAt updatedAt
273
- attributes { key value }
274
- buyerIdentity { countryCode email }
275
- discountCodes { code applicable }
276
- cost {
277
- subtotalAmount { ${MONEY} }
278
- totalAmount { ${MONEY} }
279
- checkoutChargeAmount { ${MONEY} }
280
- }
281
- lines(first: 100) {
282
- nodes {
283
- id quantity
284
- attributes { key value }
285
- cost {
286
- totalAmount { ${MONEY} }
287
- subtotalAmount { ${MONEY} }
288
- amountPerQuantity { ${MONEY} }
289
- compareAtAmountPerQuantity { ${MONEY} }
290
- }
291
- discountAllocations { discountedAmount { ${MONEY} } }
292
- sellingPlanAllocation {
293
- sellingPlan { id name description recurringDeliveries options { name value } }
294
- checkoutChargeAmount { ${MONEY} }
295
- priceAdjustments { price { ${MONEY} } compareAtPrice { ${MONEY} } perDeliveryPrice { ${MONEY} } }
296
- }
297
- merchandise {
298
- ... on ProductVariant {
299
- id title sku availableForSale requiresShipping
300
- image { ${IMAGE} }
301
- price { ${MONEY} }
302
- compareAtPrice { ${MONEY} }
303
- selectedOptions { name value }
304
- product { id title handle vendor featuredImage { ${IMAGE} } }
305
- }
306
- }
307
- ... on ComponentizableCartLine {
308
- lineComponents {
309
- id quantity
310
- merchandise { ... on ProductVariant { id title product { title handle } } }
311
- }
312
- }
313
- }
314
- }
315
- }`;
316
- }
317
- function collectionFields() {
318
- return `
319
- id handle title description descriptionHtml
320
- image { ${IMAGE} }
321
- seo { title description }`;
322
- }
323
-
324
- // src/loader.js
325
- var productCache = /* @__PURE__ */ new Map();
326
- var pending = [];
327
- var resolvers = /* @__PURE__ */ new Map();
328
- var scheduled = false;
329
- var generation = 0;
330
- var isGid = (v) => typeof v === "string" && v.startsWith("gid://");
331
- function normalizeProductKey(raw) {
332
- const v = String(raw != null ? raw : "").trim();
333
- if (!v) return null;
334
- if (isGid(v)) return v;
335
- if (/^\d+$/.test(v)) return `gid://shopify/Product/${v}`;
336
- const m = v.match(/\/products\/([^/?#]+)/);
337
- return m ? m[1] : v;
338
- }
339
- function primeProduct(product) {
340
- if (!product) return;
341
- const p = Promise.resolve(product);
342
- productCache.set(product.handle, p);
343
- productCache.set(product.id, p);
344
- }
345
- function getProduct(rawKey) {
346
- const key = normalizeProductKey(rawKey);
347
- if (!key) return Promise.reject(new TetherError("empty product key", { code: "NOT_FOUND" }));
348
- if (productCache.has(key)) return productCache.get(key);
349
- const p = new Promise((resolve, reject) => {
350
- resolvers.set(key, { resolve, reject });
351
- pending.push(key);
352
- if (!scheduled) {
353
- scheduled = true;
354
- setTimeout(flush, 0);
355
- }
356
- });
357
- productCache.set(key, p);
358
- return p;
359
- }
360
- async function flush() {
361
- scheduled = false;
362
- const gen = generation;
363
- const keys = pending.splice(0);
364
- if (!keys.length) return;
365
- const ids = keys.filter(isGid);
366
- const handles = keys.filter((k) => !isGid(k));
367
- const defs = [];
368
- const parts = [];
369
- const vars = {};
370
- if (ids.length) {
371
- defs.push("$ids: [ID!]!");
372
- vars.ids = ids;
373
- parts.push("byId: nodes(ids: $ids) { ... on Product { ...TetherProduct } }");
374
- }
375
- handles.forEach((h, i) => {
376
- defs.push(`$h${i}: String!`);
377
- vars[`h${i}`] = h;
378
- parts.push(`h${i}: product(handle: $h${i}) { ...TetherProduct }`);
379
- });
380
- const query = `query TetherProducts(${defs.join(", ")}) {
381
- ${parts.join("\n ")}
382
- }
383
- ${productFragment()}`;
384
- log("fetch products", keys);
385
- const settle = (k, fn) => {
386
- const r = resolvers.get(k);
387
- resolvers.delete(k);
388
- if (r) fn(r);
389
- };
390
- try {
391
- const data = await gql(query, vars);
392
- const got = /* @__PURE__ */ new Map();
393
- (data.byId || []).forEach((p) => {
394
- if (p) got.set(p.id, p);
395
- });
396
- handles.forEach((h, i) => {
397
- if (data[`h${i}`]) got.set(h, data[`h${i}`]);
398
- });
399
- keys.forEach((k) => settle(k, (r) => {
400
- if (got.has(k)) {
401
- if (gen === generation) primeProduct(got.get(k));
402
- r.resolve(got.get(k));
403
- } else {
404
- if (gen === generation) productCache.delete(k);
405
- r.reject(new TetherError(`Product not found: ${k}`, { code: "NOT_FOUND" }));
406
- }
407
- }));
408
- } catch (err) {
409
- keys.forEach((k) => settle(k, (r) => {
410
- if (gen === generation) productCache.delete(k);
411
- r.reject(err);
412
- }));
413
- }
414
- }
415
- function clearProductCache() {
416
- productCache.clear();
417
- generation++;
418
- }
419
- async function getProducts({
420
- collection,
421
- query,
422
- first = cfg.collectionFirst,
423
- after = null,
424
- sortKey,
425
- reverse = false,
426
- filters
427
- } = {}) {
428
- const fragment = productFragment();
429
- if (collection) {
430
- const data2 = await gql(`query TetherCollection($handle: String!, $first: Int!, $after: String, $sortKey: ProductCollectionSortKeys, $reverse: Boolean, $filters: [ProductFilter!]) {
431
- collection(handle: $handle) {
432
- ${collectionFields()}
433
- products(first: $first, after: $after, sortKey: $sortKey, reverse: $reverse, filters: $filters) {
434
- nodes { ...TetherProduct }
435
- pageInfo { hasNextPage endCursor }
436
- }
437
- }
438
- }
439
- ${fragment}`, {
440
- handle: collection,
441
- first,
442
- after,
443
- sortKey: sortKey || void 0,
444
- reverse,
445
- filters: filters || void 0
446
- });
447
- if (!data2.collection) throw new TetherError(`Collection not found: ${collection}`, { code: "NOT_FOUND" });
448
- const { products, ...rest } = data2.collection;
449
- products.nodes.forEach(primeProduct);
450
- return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
451
- }
452
- const data = await gql(`query TetherSearch($first: Int!, $after: String, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
453
- products(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
454
- nodes { ...TetherProduct }
455
- pageInfo { hasNextPage endCursor }
456
- }
457
- }
458
- ${fragment}`, { first, after, query: query || void 0, sortKey: sortKey || void 0, reverse });
459
- data.products.nodes.forEach(primeProduct);
460
- return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
461
- }
462
- async function getRecommendations(productId, intent = "RELATED") {
463
- const data = await gql(`query TetherRecs($id: ID!, $intent: ProductRecommendationIntent) {
464
- productRecommendations(productId: $id, intent: $intent) { ...TetherProduct }
465
- }
466
- ${productFragment()}`, { id: productId, intent });
467
- const list = data.productRecommendations || [];
468
- list.forEach(primeProduct);
469
- return list;
470
- }
471
-
472
- // src/selling-plans.js
473
- var selling_plans_exports = {};
474
- __export(selling_plans_exports, {
475
- GID: () => GID,
476
- allPlans: () => allPlans,
477
- allocationFor: () => allocationFor,
478
- findPlan: () => findPlan,
479
- hasPlans: () => hasPlans,
480
- planAdjustment: () => planAdjustment,
481
- planCheckoutCharge: () => planCheckoutCharge,
482
- planPrice: () => planPrice,
483
- planSavings: () => planSavings,
484
- plansForVariant: () => plansForVariant,
485
- toGid: () => toGid
486
- });
487
- var GID = {
488
- product: (v) => toGid(v, "Product"),
489
- variant: (v) => toGid(v, "ProductVariant"),
490
- sellingPlan: (v) => toGid(v, "SellingPlan")
491
- };
492
- function toGid(value, type) {
493
- const v = String(value != null ? value : "").trim();
494
- if (!v) return null;
495
- if (v.startsWith("gid://")) return v;
496
- if (/^\d+$/.test(v)) return `gid://shopify/${type}/${v}`;
497
- return v;
498
- }
499
- function allPlans(product) {
500
- var _a;
501
- const groups = ((_a = product == null ? void 0 : product.sellingPlanGroups) == null ? void 0 : _a.nodes) || [];
502
- return groups.flatMap((g) => {
503
- var _a2;
504
- return (((_a2 = g.sellingPlans) == null ? void 0 : _a2.nodes) || []).map((p) => ({ ...p, group: g }));
505
- });
506
- }
507
- function hasPlans(product) {
508
- return allPlans(product).length > 0;
509
- }
510
- function allocationFor(variant, planId) {
511
- var _a;
512
- if (!variant || !planId) return null;
513
- return (((_a = variant.sellingPlanAllocations) == null ? void 0 : _a.nodes) || []).find((a) => {
514
- var _a2;
515
- return ((_a2 = a.sellingPlan) == null ? void 0 : _a2.id) === planId;
516
- }) || null;
517
- }
518
- function plansForVariant(product, variant) {
519
- return allPlans(product).filter((p) => allocationFor(variant, p.id));
520
- }
521
- function planPrice(variant, planId) {
522
- var _a;
523
- const a = allocationFor(variant, planId);
524
- const adj = (_a = a == null ? void 0 : a.priceAdjustments) == null ? void 0 : _a[0];
525
- return (adj == null ? void 0 : adj.perDeliveryPrice) || (adj == null ? void 0 : adj.price) || (variant == null ? void 0 : variant.price) || null;
526
- }
527
- function planCheckoutCharge(variant, planId) {
528
- var _a, _b;
529
- const a = allocationFor(variant, planId);
530
- return (a == null ? void 0 : a.checkoutChargeAmount) || ((_b = (_a = a == null ? void 0 : a.priceAdjustments) == null ? void 0 : _a[0]) == null ? void 0 : _b.price) || (variant == null ? void 0 : variant.price) || null;
531
- }
532
- function planSavings(variant, planId) {
533
- var _a, _b;
534
- const base = parseFloat((_a = variant == null ? void 0 : variant.price) == null ? void 0 : _a.amount);
535
- const sub = parseFloat((_b = planPrice(variant, planId)) == null ? void 0 : _b.amount);
536
- if (!Number.isFinite(base) || !Number.isFinite(sub) || base <= 0 || sub >= base) {
537
- return { amount: null, percentage: 0 };
538
- }
539
- return {
540
- amount: { amount: (base - sub).toFixed(2), currencyCode: variant.price.currencyCode },
541
- percentage: Math.round((1 - sub / base) * 100)
542
- };
543
- }
544
- function planAdjustment(plan) {
545
- var _a, _b;
546
- const v = (_b = (_a = plan == null ? void 0 : plan.priceAdjustments) == null ? void 0 : _a[0]) == null ? void 0 : _b.adjustmentValue;
547
- if (!v) return null;
548
- switch (v.__typename) {
549
- case "SellingPlanPercentagePriceAdjustment":
550
- return { type: "percentage", value: v.adjustmentPercentage };
551
- case "SellingPlanFixedAmountPriceAdjustment":
552
- return { type: "amount", value: v.adjustmentAmount };
553
- case "SellingPlanFixedPriceAdjustment":
554
- return { type: "price", value: v.price };
555
- default:
556
- return null;
557
- }
558
- }
559
- function findPlan(product, ref) {
560
- if (!ref) return null;
561
- const plans = allPlans(product);
562
- const gid = GID.sellingPlan(ref);
563
- const needle = String(ref).trim().toLowerCase();
564
- return plans.find((p) => p.id === gid) || plans.find((p) => p.name.toLowerCase() === needle) || plans.find((p) => (p.options || []).some((o) => String(o.value).toLowerCase() === needle)) || null;
565
- }
566
-
567
- // src/dom.js
568
- var attr = (name) => `${cfg.prefix}-${name}`;
569
- var sel = (name, value) => value === void 0 ? `[${attr(name)}]` : `[${attr(name)}="${value}"]`;
570
- var get = (el, name) => el.getAttribute(attr(name));
571
- var has = (el, name) => el.hasAttribute(attr(name));
572
- var dataKey = () => `data-${cfg.prefix}-key`;
573
- function productUrl(handle, id) {
574
- const path = cfg.productPath || "/product";
575
- if (!/\{(handle|id)\}/.test(path)) return `${path.replace(/\/$/, "")}/${handle}`;
576
- const numeric = id ? String(id).split("/").pop() : "";
577
- return path.replace(/\{handle\}/g, encodeURIComponent(handle || "")).replace(/\{id\}/g, encodeURIComponent(numeric));
578
- }
579
- var qsa = (root, s) => root ? [...root.querySelectorAll(s)] : [];
580
- var scoped = (root, s, scopeSelector) => qsa(root, s).filter((el) => el.closest(scopeSelector) === root);
581
- function emit(name, detail, target = document) {
582
- target.dispatchEvent(new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true }));
583
- }
584
- function formatMoney(money) {
585
- if (!money) return "";
586
- if (typeof cfg.formatMoney === "function") return cfg.formatMoney(money, cfg.locale);
587
- const amount = parseFloat(money.amount);
588
- if (Number.isNaN(amount)) return "";
589
- try {
590
- return new Intl.NumberFormat(cfg.locale || void 0, {
591
- style: "currency",
592
- currency: money.currencyCode
593
- }).format(amount);
594
- } catch {
595
- return `${money.currencyCode} ${amount.toFixed(2)}`;
596
- }
597
- }
598
- function formatDate(iso) {
599
- if (!iso) return "";
600
- try {
601
- return new Date(iso).toLocaleDateString(cfg.locale || void 0);
602
- } catch {
603
- return "";
604
- }
605
- }
606
- function cdnUrl(url, el) {
607
- if (!url || !el) return url;
608
- const w = el.getAttribute("max-width");
609
- const h = el.getAttribute("max-height");
610
- const crop = el.getAttribute("crop");
611
- if (!w && !h) return url;
612
- try {
613
- const u = new URL(url);
614
- if (w) u.searchParams.set("width", w);
615
- if (h) u.searchParams.set("height", h);
616
- if (crop) u.searchParams.set("crop", crop);
617
- return u.toString();
618
- } catch {
619
- return url;
620
- }
621
- }
622
- function setImage(el, url, alt) {
623
- if (!url) return;
624
- const src = cdnUrl(url, el);
625
- if (el.tagName === "IMG") {
626
- el.removeAttribute("srcset");
627
- el.removeAttribute("sizes");
628
- el.setAttribute("src", src);
629
- if (alt != null) el.setAttribute("alt", alt);
630
- if (!el.getAttribute("loading")) el.setAttribute("loading", "lazy");
631
- } else {
632
- el.style.backgroundImage = `url("${src}")`;
633
- }
634
- }
635
- function setText(el, v) {
636
- const text = v == null ? "" : String(v);
637
- if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") el.value = text;
638
- else el.textContent = text;
639
- }
640
- var readText = (el) => el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el.value : el.textContent;
641
- function setTextTracked(el, v) {
642
- const before = readText(el);
643
- setText(el, v);
644
- const after = readText(el);
645
- if (before !== after && el.dataset.tetherSeen && typeof cfg.onValueChange === "function") {
646
- try {
647
- cfg.onValueChange(el, before);
648
- } catch {
649
- }
650
- }
651
- el.dataset.tetherSeen = "1";
652
- }
653
- function show(el, visible) {
654
- const k = "tetherPrevDisplay";
655
- if (visible) {
656
- if (el.dataset[k] !== void 0) {
657
- el.style.display = el.dataset[k];
658
- delete el.dataset[k];
659
- } else if (el.style.display === "none") {
660
- el.style.display = "";
661
- }
662
- } else {
663
- if (el.dataset[k] === void 0) el.dataset[k] = el.style.display || "";
664
- el.style.display = "none";
665
- }
666
- }
667
- function setSelected(el, selected) {
668
- var _a;
669
- el.classList.toggle("is-selected", selected);
670
- el.setAttribute("aria-pressed", selected ? "true" : "false");
671
- if (el.tagName === "INPUT" && (el.type === "radio" || el.type === "checkbox")) el.checked = selected;
672
- const input = (_a = el.querySelector) == null ? void 0 : _a.call(el, 'input[type="radio"],input[type="checkbox"]');
673
- if (input) input.checked = selected;
674
- }
675
- var templates = /* @__PURE__ */ new WeakMap();
676
- var hasTemplate = (container) => Boolean(templates.get(container));
677
- function template(container) {
678
- var _a, _b;
679
- if (!templates.has(container)) {
680
- const tpl = (_b = (_a = container.firstElementChild) == null ? void 0 : _a.cloneNode(true)) != null ? _b : null;
681
- templates.set(container, tpl);
682
- container.innerHTML = "";
683
- }
684
- return templates.get(container);
685
- }
686
- function repeat(container, items, render) {
687
- const tpl = template(container);
688
- if (!tpl) {
689
- warn("repeater without a template child", container);
690
- return;
691
- }
692
- container.innerHTML = "";
693
- items.forEach((item, i) => {
694
- const node = tpl.cloneNode(true);
695
- render(node, item, i);
696
- container.appendChild(node);
697
- });
698
- }
699
- function repeatKeyed(container, items, keyOf, render, { onEnter, onLeave } = {}) {
700
- const tpl = template(container);
701
- if (!tpl) {
702
- warn("repeater without a template child", container);
703
- return;
704
- }
705
- const KEY = dataKey();
706
- const LEAVING = `data-${cfg.prefix}-leaving`;
707
- const live = () => [...container.children].filter((n) => n.hasAttribute(KEY) && !n.hasAttribute(LEAVING));
708
- const existing = new Map(live().map((n) => [n.getAttribute(KEY), n]));
709
- const wanted = [];
710
- const fresh = /* @__PURE__ */ new Set();
711
- items.forEach((item, i) => {
712
- const k = String(keyOf(item, i));
713
- let node = existing.get(k);
714
- if (node) existing.delete(k);
715
- else {
716
- node = tpl.cloneNode(true);
717
- node.setAttribute(KEY, k);
718
- fresh.add(node);
719
- }
720
- render(node, item, i, fresh.has(node));
721
- wanted.push(node);
722
- });
723
- const staying = wanted.filter((n) => !fresh.has(n));
724
- const current = live().filter((n) => staying.includes(n));
725
- const orderOk = current.length === staying.length && current.every((n, i) => n === staying[i]);
726
- if (!orderOk) {
727
- existing.forEach((n) => n.remove());
728
- wanted.forEach((n) => container.appendChild(n));
729
- return;
730
- }
731
- wanted.forEach((node, i) => {
732
- if (!fresh.has(node)) return;
733
- const prev = wanted[i - 1];
734
- if (prev && prev.parentNode === container) prev.after(node);
735
- else container.prepend(node);
736
- if (onEnter) {
737
- try {
738
- onEnter(node);
739
- } catch {
740
- }
741
- }
742
- });
743
- existing.forEach((node) => {
744
- node.setAttribute(LEAVING, "");
745
- node.removeAttribute(KEY);
746
- node.style.pointerEvents = "none";
747
- node.inert = true;
748
- const done = () => node.remove();
749
- if (onLeave) {
750
- try {
751
- onLeave(node, done);
752
- } catch {
753
- done();
754
- }
755
- } else done();
756
- });
757
- }
758
- var layoutTimer = null;
759
- function notifyLayoutChanged(reason) {
760
- clearTimeout(layoutTimer);
761
- layoutTimer = setTimeout(() => {
762
- var _a;
763
- if (typeof cfg.onLayoutChange === "function") cfg.onLayoutChange(reason);
764
- else if ((_a = window.ScrollTrigger) == null ? void 0 : _a.refresh) window.ScrollTrigger.refresh();
765
- emit("layout-changed", { reason });
766
- }, 120);
767
- }
768
- function watchImages(root, reason) {
769
- const imgs = qsa(root, "img").filter((i) => !i.complete);
770
- if (!imgs.length) return;
771
- let left = imgs.length;
772
- const done = () => {
773
- if (--left === 0) notifyLayoutChanged(`${reason}:images`);
774
- };
775
- imgs.forEach((img) => {
776
- img.addEventListener("load", done, { once: true });
777
- img.addEventListener("error", done, { once: true });
778
- });
779
- }
780
- function busy(el, fn) {
781
- if (el.getAttribute("aria-busy") === "true") return Promise.resolve();
782
- el.setAttribute("aria-busy", "true");
783
- return Promise.resolve().then(fn).finally(() => el.removeAttribute("aria-busy"));
784
- }
785
- function parseJSONAttr(el, name) {
786
- const raw = el && get(el, name);
787
- if (!raw) return null;
788
- try {
789
- return JSON.parse(raw);
790
- } catch {
791
- warn(`invalid JSON in ${attr(name)}`, el);
792
- return null;
793
- }
794
- }
795
- function toAttributes(obj) {
796
- if (!obj) return [];
797
- if (Array.isArray(obj)) return obj.map((a) => {
798
- var _a;
799
- return { key: String(a.key), value: String((_a = a.value) != null ? _a : "") };
800
- });
801
- return Object.entries(obj).map(([key, value]) => ({ key, value: String(value != null ? value : "") }));
802
- }
803
-
804
- // src/cart.js
805
- function toLineInput(input, quantity = 1, opts = {}) {
806
- var _a;
807
- const src = typeof input === "object" && input !== null ? input : { variantId: input, quantity, ...opts };
808
- const merchandiseId = GID.variant(src.merchandiseId || src.variantId || src.id);
809
- if (!merchandiseId) throw new TetherError("add(): variantId is required");
810
- const line = { merchandiseId, quantity: Math.max(1, parseInt((_a = src.quantity) != null ? _a : 1, 10) || 1) };
811
- const plan = src.sellingPlanId || src.sellingPlan;
812
- if (plan) line.sellingPlanId = GID.sellingPlan(plan);
813
- const attrs = toAttributes(src.attributes);
814
- if (attrs.length) line.attributes = attrs;
815
- return line;
816
- }
817
- function mutation(name, args, inner) {
818
- return `mutation Tether_${name}(${args}) {
819
- ${name}(${inner}) { cart { ...TetherCart } userErrors { field message } }
820
- }
821
- ${cartFragment()}`;
822
- }
823
- var isMissingCart = (err) => /does not exist|not found|invalid id/i.test((err == null ? void 0 : err.message) || "");
824
- var Cart = {
825
- state: null,
826
- _chain: Promise.resolve(),
827
- id() {
828
- try {
829
- return localStorage.getItem(cfg.storageKey);
830
- } catch {
831
- return null;
832
- }
833
- },
834
- _setId(id) {
835
- try {
836
- if (id) localStorage.setItem(cfg.storageKey, id);
837
- else localStorage.removeItem(cfg.storageKey);
838
- } catch {
839
- }
840
- },
841
- get lines() {
842
- var _a, _b;
843
- return ((_b = (_a = this.state) == null ? void 0 : _a.lines) == null ? void 0 : _b.nodes) || [];
844
- },
845
- get count() {
846
- var _a;
847
- return ((_a = this.state) == null ? void 0 : _a.totalQuantity) || 0;
848
- },
849
- lastError: null,
850
- _queue(fn) {
851
- const run = this._chain.then(fn, fn).then((value) => {
852
- if (this.lastError) {
853
- this.lastError = null;
854
- this.render();
855
- }
856
- return value;
857
- }, (err) => {
858
- this.lastError = err;
859
- this.render();
860
- emit("cart-error", { error: err });
861
- throw err;
862
- });
863
- this._chain = run.catch(() => {
864
- });
865
- return run;
866
- },
867
- _commit(cart, reason) {
868
- this.state = cart;
869
- if (cart == null ? void 0 : cart.id) this._setId(cart.id);
870
- this.render();
871
- emit("cart-updated", { cart, reason });
872
- return cart;
873
- },
874
- async init() {
875
- const id = this.id();
876
- if (!id) {
877
- this.state = null;
878
- this.render();
879
- return null;
880
- }
881
- try {
882
- const data = await gql(`query TetherCart($id: ID!) { cart(id: $id) { ...TetherCart } }
883
- ${cartFragment()}`, { id });
884
- if (!data.cart) {
885
- this._setId(null);
886
- this.state = null;
887
- } else this.state = data.cart;
888
- } catch (err) {
889
- reportError(err, "cart load failed");
890
- this.lastError = err;
891
- if (isMissingCart(err)) this._setId(null);
892
- this.state = null;
893
- }
894
- this.render();
895
- return this.state;
896
- },
897
- async _create(input = {}) {
898
- const buyerIdentity = cfg.country ? { countryCode: String(cfg.country).toUpperCase() } : void 0;
899
- const data = await gql(
900
- mutation("cartCreate", "$input: CartInput!", "input: $input"),
901
- { input: { buyerIdentity, ...input } }
902
- );
903
- return unwrap(data.cartCreate, "cartCreate").cart;
904
- },
905
- /** Runs a cart mutation; creates a fresh cart if ours expired. */
906
- async _mutate(name, args, inner, variables, createInput) {
907
- const cartId = this.id();
908
- if (!cartId) return createInput ? this._create(createInput) : null;
909
- try {
910
- const data = await gql(mutation(name, `$cartId: ID!, ${args}`, `cartId: $cartId, ${inner}`), { ...variables, cartId });
911
- return unwrap(data[name], name).cart;
912
- } catch (err) {
913
- if (createInput && isMissingCart(err)) {
914
- log("cart expired, creating a new one");
915
- this._setId(null);
916
- return this._create(createInput);
917
- }
918
- throw err;
919
- }
920
- },
921
- /**
922
- * add('gid://shopify/ProductVariant/1', 2)
923
- * add({ variantId, quantity, sellingPlanId, attributes })
924
- * add([line, line]) // bundles / build-a-box in one request
925
- */
926
- add(input, quantity = 1, opts = {}) {
927
- const lines = (Array.isArray(input) ? input : [input]).map((l) => toLineInput(l, quantity, opts));
928
- return this._queue(async () => {
929
- const cart = await this._mutate("cartLinesAdd", "$lines: [CartLineInput!]!", "lines: $lines", { lines }, { lines });
930
- this._commit(cart, "add");
931
- emit("added", { cart, lines });
932
- if (cfg.openCartOnAdd) this.open();
933
- return cart;
934
- });
935
- },
936
- /**
937
- * updateLine(lineId, 3)
938
- * updateLine(lineId, { quantity, sellingPlanId, attributes })
939
- * sellingPlanId: null switches a line back to one-time purchase.
940
- */
941
- updateLine(lineId, patch) {
942
- const p = typeof patch === "number" ? { quantity: patch } : { ...patch };
943
- if (p.quantity !== void 0 && p.quantity <= 0) return this.removeLine(lineId);
944
- const line = { id: lineId };
945
- if (p.quantity !== void 0) line.quantity = p.quantity;
946
- if ("sellingPlanId" in p) line.sellingPlanId = p.sellingPlanId ? GID.sellingPlan(p.sellingPlanId) : null;
947
- if (p.attributes) line.attributes = toAttributes(p.attributes);
948
- if (p.merchandiseId || p.variantId) line.merchandiseId = GID.variant(p.merchandiseId || p.variantId);
949
- return this._queue(async () => this._commit(
950
- await this._mutate("cartLinesUpdate", "$lines: [CartLineUpdateInput!]!", "lines: $lines", { lines: [line] }),
951
- "update"
952
- ));
953
- },
954
- removeLine(lineIds) {
955
- const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
956
- return this._queue(async () => this._commit(
957
- await this._mutate("cartLinesRemove", "$lineIds: [ID!]!", "lineIds: $lineIds", { lineIds: ids }),
958
- "remove"
959
- ));
960
- },
961
- clear() {
962
- const ids = this.lines.map((l) => l.id);
963
- if (!ids.length) return Promise.resolve(this.state);
964
- return this.removeLine(ids);
965
- },
966
- setNote(note) {
967
- return this._queue(async () => this._commit(
968
- await this._mutate("cartNoteUpdate", "$note: String!", "note: $note", { note: note != null ? note : "" }, { note: note != null ? note : "" }),
969
- "note"
970
- ));
971
- },
972
- /** Cart-level attributes, merged with existing ones. Value '' removes a key. */
973
- setAttributes(obj) {
974
- var _a;
975
- const merged = new Map((((_a = this.state) == null ? void 0 : _a.attributes) || []).map((a) => [a.key, a.value]));
976
- toAttributes(obj).forEach((a) => merged.set(a.key, a.value));
977
- const attributes = [...merged].filter(([, v]) => v !== "").map(([key, value]) => ({ key, value }));
978
- return this._queue(async () => this._commit(
979
- await this._mutate("cartAttributesUpdate", "$attributes: [AttributeInput!]!", "attributes: $attributes", { attributes }, { attributes }),
980
- "attributes"
981
- ));
982
- },
983
- /** Replaces the discount codes. Pass [] to remove all. */
984
- setDiscountCodes(codes) {
985
- const discountCodes = (Array.isArray(codes) ? codes : [codes]).map((c) => String(c).trim()).filter(Boolean);
986
- return this._queue(async () => this._commit(
987
- await this._mutate("cartDiscountCodesUpdate", "$discountCodes: [String!]!", "discountCodes: $discountCodes", { discountCodes }, { discountCodes }),
988
- "discount"
989
- ));
990
- },
991
- applyDiscount(code) {
992
- var _a;
993
- const current = (((_a = this.state) == null ? void 0 : _a.discountCodes) || []).map((d) => d.code);
994
- return this.setDiscountCodes([.../* @__PURE__ */ new Set([...current, code])]);
995
- },
996
- removeDiscount(code) {
997
- var _a;
998
- return this.setDiscountCodes((((_a = this.state) == null ? void 0 : _a.discountCodes) || []).map((d) => d.code).filter((c) => c !== code));
999
- },
1000
- /** { countryCode, email, phone } — changes currency/prices for that market. */
1001
- setBuyerIdentity(buyerIdentity, { create = true } = {}) {
1002
- return this._queue(async () => {
1003
- const cart = await this._mutate("cartBuyerIdentityUpdate", "$buyerIdentity: CartBuyerIdentityInput!", "buyerIdentity: $buyerIdentity", { buyerIdentity }, create ? { buyerIdentity } : null).catch((err) => {
1004
- if (create || !isMissingCart(err)) throw err;
1005
- this._setId(null);
1006
- return null;
1007
- });
1008
- return this._commit(cart, "buyer-identity");
1009
- });
1010
- },
1011
- /**
1012
- * Goes to Shopify checkout. With checkoutTarget '_blank' the tab is opened
1013
- * inside the click (Safari blocks window.open after an await) and gets its
1014
- * URL once we have it.
1015
- */
1016
- async checkout(target = cfg.checkoutTarget) {
1017
- var _a;
1018
- const id = this.id();
1019
- if (!id) return null;
1020
- const tab = target === "_blank" ? window.open("", "_blank") : null;
1021
- if (tab) {
1022
- try {
1023
- tab.opener = null;
1024
- } catch {
1025
- }
1026
- }
1027
- try {
1028
- const data = await gql("query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }", { id });
1029
- const url = (_a = data.cart) == null ? void 0 : _a.checkoutUrl;
1030
- if (!url) throw new TetherError("No checkoutUrl (cart empty or expired)");
1031
- emit("checkout", { url, cart: this.state });
1032
- if (tab && !tab.closed) tab.location.href = url;
1033
- else window.location.href = url;
1034
- return url;
1035
- } catch (err) {
1036
- if (tab) tab.close();
1037
- reportError(err, "checkout failed");
1038
- return null;
1039
- }
1040
- },
1041
- // -------------------------------------------------------------- drawer
1042
- drawers() {
1043
- return qsa(document, sel("cart-drawer"));
1044
- },
1045
- isOpen() {
1046
- return this.drawers().some((d) => d.classList.contains(cfg.cartDrawerOpenClass));
1047
- },
1048
- open() {
1049
- if (this.isOpen()) return;
1050
- const ds = this.drawers().filter((d) => get(d, "cart-drawer") !== "manual");
1051
- ds.forEach((d) => {
1052
- d.classList.add(cfg.cartDrawerOpenClass);
1053
- d.setAttribute("aria-hidden", "false");
1054
- });
1055
- if (typeof cfg.onCartOpen === "function") cfg.onCartOpen(ds);
1056
- emit("cart-toggled", { open: true });
1057
- },
1058
- close() {
1059
- if (!this.isOpen()) return;
1060
- const ds = this.drawers();
1061
- ds.forEach((d) => {
1062
- d.classList.remove(cfg.cartDrawerOpenClass);
1063
- d.setAttribute("aria-hidden", "true");
1064
- });
1065
- if (typeof cfg.onCartClose === "function") cfg.onCartClose(ds);
1066
- emit("cart-toggled", { open: false });
1067
- },
1068
- toggle() {
1069
- if (this.isOpen()) this.close();
1070
- else this.open();
1071
- },
1072
- // -------------------------------------------------------------- render
1073
- render() {
1074
- const cart = this.state;
1075
- const count = cart ? cart.totalQuantity : 0;
1076
- const money = (k) => {
1077
- var _a;
1078
- return cart ? formatMoney((_a = cart.cost) == null ? void 0 : _a[k]) : "";
1079
- };
1080
- qsa(document, sel("cart-count")).forEach((el) => setTextTracked(el, count));
1081
- qsa(document, sel("cart-subtotal")).forEach((el) => setTextTracked(el, money("subtotalAmount")));
1082
- qsa(document, sel("cart-total")).forEach((el) => setTextTracked(el, money("totalAmount")));
1083
- qsa(document, sel("cart-checkout-charge")).forEach((el) => setTextTracked(el, money("checkoutChargeAmount")));
1084
- const hasSub = this.lines.some((l) => l.sellingPlanAllocation);
1085
- const conds = {
1086
- "cart-empty": count === 0,
1087
- "cart-filled": count > 0,
1088
- "cart-has-subscription": hasSub,
1089
- "cart-no-subscription": !hasSub,
1090
- "cart-has-discount": ((cart == null ? void 0 : cart.discountCodes) || []).some((d) => d.applicable),
1091
- "cart-error": Boolean(this.lastError)
1092
- };
1093
- qsa(document, sel("show")).forEach((el) => {
1094
- const k = get(el, "show");
1095
- if (k in conds && !el.closest(sel("product"))) show(el, conds[k]);
1096
- });
1097
- qsa(document, `textarea${sel("cart-note")},input${sel("cart-note")}`).forEach((el) => {
1098
- if (document.activeElement !== el) el.value = (cart == null ? void 0 : cart.note) || "";
1099
- });
1100
- qsa(document, sel("cart-discounts")).forEach((container) => {
1101
- repeat(container, (cart == null ? void 0 : cart.discountCodes) || [], (node, d) => {
1102
- node.setAttribute(attr("discount-code"), d.code);
1103
- const t = node.querySelector(sel("discount", "code")) || (node.firstElementChild ? null : node);
1104
- if (t) setText(t, d.code);
1105
- node.classList.toggle("is-inapplicable", !d.applicable);
1106
- });
1107
- });
1108
- qsa(document, sel("cart-lines")).forEach((container) => {
1109
- repeatKeyed(container, this.lines, (l) => l.id, (node, line) => renderLine(node, line), {
1110
- onEnter: cfg.onLineEnter,
1111
- onLeave: cfg.onLineLeave
1112
- });
1113
- });
1114
- if (cart) notifyLayoutChanged("cart");
1115
- }
1116
- };
1117
- function lineValue(line, key) {
1118
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1119
- const m = line.merchandise || {};
1120
- const plan = (_a = line.sellingPlanAllocation) == null ? void 0 : _a.sellingPlan;
1121
- switch (key) {
1122
- case "title":
1123
- return (_b = m.product) == null ? void 0 : _b.title;
1124
- case "product-title":
1125
- return (_c = m.product) == null ? void 0 : _c.title;
1126
- case "variant-title":
1127
- return m.title === "Default Title" ? "" : m.title;
1128
- case "options":
1129
- return (m.selectedOptions || []).filter((o) => o.value !== "Default Title").map((o) => `${o.name}: ${o.value}`).join(", ");
1130
- case "vendor":
1131
- return (_d = m.product) == null ? void 0 : _d.vendor;
1132
- case "sku":
1133
- return m.sku;
1134
- case "price":
1135
- return formatMoney(((_e = line.cost) == null ? void 0 : _e.amountPerQuantity) || m.price);
1136
- case "compare-at-price":
1137
- return formatMoney(((_f = line.cost) == null ? void 0 : _f.compareAtAmountPerQuantity) || m.compareAtPrice);
1138
- case "line-total":
1139
- return formatMoney((_g = line.cost) == null ? void 0 : _g.totalAmount);
1140
- case "line-subtotal":
1141
- return formatMoney((_h = line.cost) == null ? void 0 : _h.subtotalAmount);
1142
- case "quantity":
1143
- return line.quantity;
1144
- case "plan":
1145
- return (plan == null ? void 0 : plan.name) || "";
1146
- case "plan-description":
1147
- return (plan == null ? void 0 : plan.description) || "";
1148
- case "components":
1149
- return (line.lineComponents || []).map((c) => {
1150
- var _a2, _b2;
1151
- return `${c.quantity} \xD7 ${((_b2 = (_a2 = c.merchandise) == null ? void 0 : _a2.product) == null ? void 0 : _b2.title) || ""}`;
1152
- }).join(", ");
1153
- default:
1154
- if (key.startsWith("attribute:")) {
1155
- const k = key.slice("attribute:".length);
1156
- return ((_i = (line.attributes || []).find((a) => a.key === k)) == null ? void 0 : _i.value) || "";
1157
- }
1158
- return void 0;
1159
- }
1160
- }
1161
- function renderLine(node, line) {
1162
- var _a, _b, _c, _d;
1163
- const m = line.merchandise || {};
1164
- const isSub = Boolean(line.sellingPlanAllocation);
1165
- node.setAttribute(attr("line-id"), line.id);
1166
- node.classList.toggle("is-subscription", isSub);
1167
- qsa(node, sel("line")).forEach((el) => {
1168
- var _a2, _b2, _c2, _d2, _e, _f;
1169
- const key = get(el, "line");
1170
- if (key === "image") return setImage(el, ((_a2 = m.image) == null ? void 0 : _a2.url) || ((_c2 = (_b2 = m.product) == null ? void 0 : _b2.featuredImage) == null ? void 0 : _c2.url), (_d2 = m.product) == null ? void 0 : _d2.title);
1171
- if (key === "url") return el.setAttribute("href", productUrl((_e = m.product) == null ? void 0 : _e.handle, (_f = m.product) == null ? void 0 : _f.id));
1172
- const v = lineValue(line, key);
1173
- if (v !== void 0) setTextTracked(el, v);
1174
- return void 0;
1175
- });
1176
- const lineConds = {
1177
- subscription: isSub,
1178
- "one-time": !isSub,
1179
- "on-sale": parseFloat((_b = (_a = line.cost) == null ? void 0 : _a.compareAtAmountPerQuantity) == null ? void 0 : _b.amount) > parseFloat((_d = (_c = line.cost) == null ? void 0 : _c.amountPerQuantity) == null ? void 0 : _d.amount),
1180
- "has-components": (line.lineComponents || []).length > 0
1181
- };
1182
- qsa(node, sel("line-show")).forEach((el) => {
1183
- const k = get(el, "line-show");
1184
- if (k in lineConds) show(el, lineConds[k]);
1185
- });
1186
- qsa(node, `select${sel("line-plan")}`).forEach((select) => {
1187
- var _a2;
1188
- const handle = (_a2 = m.product) == null ? void 0 : _a2.handle;
1189
- if (!handle) return;
1190
- getProduct(handle).then((product) => {
1191
- var _a3, _b2;
1192
- const plans = allPlans(product);
1193
- const allowOneTime = !product.requiresSellingPlan && get(select, "line-plan") !== "subscription-only";
1194
- const want = [
1195
- ...allowOneTime ? [{ value: "", label: select.getAttribute(attr("one-time-label")) || "One-time purchase" }] : [],
1196
- ...plans.map((p) => ({ value: p.id, label: p.name }))
1197
- ];
1198
- const sig = want.map((w) => w.value).join("|");
1199
- if (select.dataset.tetherSig !== sig) {
1200
- select.innerHTML = "";
1201
- want.forEach((w) => {
1202
- const o = document.createElement("option");
1203
- o.value = w.value;
1204
- o.textContent = w.label;
1205
- select.appendChild(o);
1206
- });
1207
- select.dataset.tetherSig = sig;
1208
- }
1209
- select.value = ((_b2 = (_a3 = line.sellingPlanAllocation) == null ? void 0 : _a3.sellingPlan) == null ? void 0 : _b2.id) || "";
1210
- show(select, plans.length > 0);
1211
- }).catch((err) => reportError(err, "line plan options"));
1212
- });
1213
- }
1214
-
1215
- // src/status.js
1216
- function resolveProductKey(el, location = window.location) {
1217
- const raw = (get(el, "product") || "").trim();
1218
- if (!raw.toLowerCase().startsWith("url:")) return { key: raw || null, fromUrl: false, raw };
1219
- const what = raw.slice(4).trim();
1220
- let key = null;
1221
- if (what.toLowerCase() === "path") {
1222
- const parts = location.pathname.split("/").filter(Boolean);
1223
- key = parts.length ? safeDecode(parts[parts.length - 1]) : null;
1224
- } else {
1225
- key = new URLSearchParams(location.search).get(what || "handle");
1226
- }
1227
- key = key && key.trim() ? key.trim() : null;
1228
- return { key, fromUrl: true, raw };
1229
- }
1230
- function safeDecode(s) {
1231
- try {
1232
- return decodeURIComponent(s);
1233
- } catch {
1234
- return s;
1235
- }
1236
- }
1237
- var STATUS_KEYS = ["loading", "ready", "error", "not-found"];
1238
- var isNotFound = (err) => (err == null ? void 0 : err.code) === "NOT_FOUND";
1239
- var boundary = () => ["product", "collection", "products", "recommendations"].map((n) => sel(n)).join(",");
1240
- var ownedBy = (el, root) => {
1241
- var _a;
1242
- return el === root || ((_a = el.parentElement) == null ? void 0 : _a.closest(boundary())) === root;
1243
- };
1244
- function setScopeStatus(root, status, err) {
1245
- const notFound = status === "not-found";
1246
- root.setAttribute(attr("state"), notFound ? "error" : status);
1247
- if (status === "error" || notFound) root.setAttribute(attr("error"), notFound ? "not-found" : (err == null ? void 0 : err.code) || "error");
1248
- else root.removeAttribute(attr("error"));
1249
- scoped(root, sel("show"), boundary()).forEach((el) => {
1250
- const k = get(el, "show");
1251
- if (!STATUS_KEYS.includes(k)) return;
1252
- const visible = k === status;
1253
- show(el, visible);
1254
- if (visible && typeof getComputedStyle === "function" && getComputedStyle(el).display === "none") {
1255
- el.style.display = el.getAttribute(attr("display")) || "block";
1256
- }
1257
- });
1258
- if (status === "error" || notFound) {
1259
- scoped(root, sel("add-to-cart"), boundary()).forEach((btn) => {
1260
- btn.classList.add("is-disabled");
1261
- if ("disabled" in btn) btn.disabled = true;
1262
- });
1263
- }
1264
- }
1265
-
1266
- // src/product.js
1267
- var scopes = /* @__PURE__ */ new WeakMap();
1268
- var productSel = () => sel("product");
1269
- function metafieldValue(product, ref) {
1270
- const i = ref.indexOf(".");
1271
- const ns = ref.slice(0, i);
1272
- const key = ref.slice(i + 1);
1273
- const mf = (product.metafields || []).find((m) => m && m.namespace === ns && m.key === key);
1274
- return mf || null;
1275
- }
1276
- var ProductScope = class {
1277
- constructor(root, product) {
1278
- var _a;
1279
- this.root = root;
1280
- this.product = product;
1281
- this.selected = {};
1282
- this.variant = null;
1283
- this.quantity = Math.max(0, parseInt((_a = get(root, "quantity")) != null ? _a : "1", 10) || (has(root, "quantity") ? 0 : 1));
1284
- this.plans = allPlans(product);
1285
- this.purchase = "one-time";
1286
- this.planId = null;
1287
- scopes.set(root, this);
1288
- root.setAttribute(attr("state"), "ready");
1289
- root.removeAttribute(attr("error"));
1290
- this.initSelection();
1291
- this.initPurchase();
1292
- this.renderProduct();
1293
- this.bind();
1294
- this.renderVariant();
1295
- watchImages(root, "product");
1296
- notifyLayoutChanged("product");
1297
- emit("product-ready", { product, scope: this }, root);
1298
- }
1299
- /** New data for the same product (e.g. after a country switch). Keeps the selection. */
1300
- update(product) {
1301
- var _a, _b;
1302
- const variantId = (_a = this.variant) == null ? void 0 : _a.id;
1303
- this.product = product;
1304
- this.plans = allPlans(product);
1305
- if (this.planId && !this.plans.some((p) => p.id === this.planId)) this.planId = ((_b = this.plans[0]) == null ? void 0 : _b.id) || null;
1306
- if (!this.plans.length && this.purchase === "subscription") this.purchase = "one-time";
1307
- const v = product.variants.nodes.find((x) => x.id === variantId);
1308
- if (v) {
1309
- this.selected = {};
1310
- v.selectedOptions.forEach((o) => {
1311
- this.selected[o.name] = o.value;
1312
- });
1313
- }
1314
- this.root.setAttribute(attr("state"), "ready");
1315
- this.root.removeAttribute(attr("error"));
1316
- this.renderProduct();
1317
- this.renderVariant();
1318
- notifyLayoutChanged("product");
1319
- emit("product-updated", { product, scope: this }, this.root);
1320
- }
1321
- /** Matching elements in this scope, including the root itself (e.g. a card link with tether-bind="link"). */
1322
- $(s) {
1323
- const inner = scoped(this.root, s, productSel());
1324
- return this.root.matches(s) ? [this.root, ...inner] : inner;
1325
- }
1326
- // ------------------------------------------------------------ state
1327
- initSelection() {
1328
- const vs = this.product.variants.nodes;
1329
- const first = vs.find((v) => v.availableForSale) || vs[0];
1330
- if (first) first.selectedOptions.forEach((o) => {
1331
- this.selected[o.name] = o.value;
1332
- });
1333
- const param = new URLSearchParams(window.location.search).get("variant") || get(this.root, "variant");
1334
- if (param) {
1335
- const gid = GID.variant(param);
1336
- const match = vs.find((v) => v.id === gid);
1337
- if (match) {
1338
- this.selected = {};
1339
- match.selectedOptions.forEach((o) => {
1340
- this.selected[o.name] = o.value;
1341
- });
1342
- }
1343
- }
1344
- }
1345
- initPurchase() {
1346
- if (!this.plans.length) return;
1347
- const params = new URLSearchParams(window.location.search);
1348
- const fromUrl = findPlan(this.product, params.get("selling_plan"));
1349
- const fromAttr = findPlan(this.product, get(this.root, "default-plan"));
1350
- this.planId = (fromUrl || fromAttr || this.plans[0]).id;
1351
- const wanted = fromUrl ? "subscription" : get(this.root, "default-purchase") || cfg.defaultPurchase;
1352
- this.purchase = this.product.requiresSellingPlan || wanted === "subscription" ? "subscription" : "one-time";
1353
- }
1354
- findVariant() {
1355
- return this.product.variants.nodes.find((v) => v.selectedOptions.every((o) => this.selected[o.name] === o.value)) || null;
1356
- }
1357
- isValueAvailable(name, value) {
1358
- return this.product.variants.nodes.some((v) => v.availableForSale && v.selectedOptions.every((o) => o.name === name ? o.value === value : this.selected[o.name] === o.value));
1359
- }
1360
- get plan() {
1361
- return this.plans.find((p) => p.id === this.planId) || null;
1362
- }
1363
- /** True when the current variant can be bought on the selected plan. */
1364
- get subscriptionAvailable() {
1365
- return Boolean(this.variant && this.plans.some((p) => allocationFor(this.variant, p.id)));
1366
- }
1367
- get isSubscription() {
1368
- return this.purchase === "subscription" && Boolean(allocationFor(this.variant, this.planId));
1369
- }
1370
- /** What would be added to the cart right now. */
1371
- getLine() {
1372
- if (!this.variant) return null;
1373
- const line = { merchandiseId: this.variant.id, quantity: this.quantity };
1374
- if (this.isSubscription) line.sellingPlanId = this.planId;
1375
- const attrs = this.collectAttributes();
1376
- if (attrs.length) line.attributes = attrs;
1377
- return line;
1378
- }
1379
- collectAttributes() {
1380
- const fixed = toAttributes(parseJSONAttr(this.root, "line-attributes"));
1381
- const inputs = this.$(sel("line-attribute")).map((el) => {
1382
- const key = get(el, "line-attribute");
1383
- let value;
1384
- if (el.type === "checkbox") value = el.checked ? el.value && el.value !== "on" ? el.value : "Yes" : "";
1385
- else if (el.type === "radio") value = el.checked ? el.value : null;
1386
- else value = el.value;
1387
- return value == null || value === "" ? null : { key, value: String(value) };
1388
- }).filter(Boolean);
1389
- return [...fixed, ...inputs];
1390
- }
1391
- // ------------------------------------------------------------ actions
1392
- select(name, value) {
1393
- if (this.selected[name] === value) return;
1394
- this.selected[name] = value;
1395
- this.renderVariant();
1396
- }
1397
- selectVariant(id) {
1398
- const v = this.product.variants.nodes.find((x) => x.id === GID.variant(id));
1399
- if (!v) return;
1400
- this.selected = {};
1401
- v.selectedOptions.forEach((o) => {
1402
- this.selected[o.name] = o.value;
1403
- });
1404
- this.renderVariant();
1405
- }
1406
- setPurchase(mode) {
1407
- const next = mode === "subscription" ? "subscription" : "one-time";
1408
- if (next === "one-time" && this.product.requiresSellingPlan) return;
1409
- if (this.purchase === next) return;
1410
- this.purchase = next;
1411
- this.renderVariant();
1412
- }
1413
- selectPlan(ref) {
1414
- const p = findPlan(this.product, ref);
1415
- if (!p) return;
1416
- this.planId = p.id;
1417
- this.purchase = "subscription";
1418
- this.renderVariant();
1419
- }
1420
- setQuantity(n) {
1421
- const min = has(this.root, "quantity") ? 0 : 1;
1422
- this.quantity = Math.max(min, parseInt(n, 10) || 0);
1423
- this.$(sel("qty")).forEach((i) => setText(i, this.quantity));
1424
- this.renderVariant();
1425
- }
1426
- async addToCart() {
1427
- var _a;
1428
- if (!((_a = this.variant) == null ? void 0 : _a.availableForSale)) return null;
1429
- const line = this.getLine();
1430
- if (!line || line.quantity < 1) return null;
1431
- const cart = await Cart.add(line);
1432
- emit("product-added", { product: this.product, variant: this.variant, line }, this.root);
1433
- return cart;
1434
- }
1435
- // ------------------------------------------------------------ events
1436
- bind() {
1437
- const own = (el) => el && el.closest(productSel()) === this.root;
1438
- this.root.addEventListener("click", (e) => {
1439
- const t = e.target;
1440
- if (!(t == null ? void 0 : t.closest)) return;
1441
- const opt = t.closest(sel("option-value"));
1442
- if (own(opt) && opt.closest(sel("option-list"))) {
1443
- e.preventDefault();
1444
- this.select(get(opt.closest(sel("option-list")), "option-list"), get(opt, "option-value"));
1445
- return;
1446
- }
1447
- const plan = t.closest(sel("plan-value"));
1448
- if (own(plan)) {
1449
- e.preventDefault();
1450
- this.selectPlan(get(plan, "plan-value"));
1451
- return;
1452
- }
1453
- const purchase = t.closest(`${sel("purchase")}:not(select)`);
1454
- if (own(purchase)) {
1455
- if (purchase.tagName !== "INPUT") e.preventDefault();
1456
- this.setPurchase(get(purchase, "purchase"));
1457
- return;
1458
- }
1459
- const plus = t.closest(sel("qty-plus"));
1460
- if (own(plus)) {
1461
- e.preventDefault();
1462
- this.setQuantity(this.quantity + 1);
1463
- return;
1464
- }
1465
- const minus = t.closest(sel("qty-minus"));
1466
- if (own(minus)) {
1467
- e.preventDefault();
1468
- this.setQuantity(this.quantity - 1);
1469
- return;
1470
- }
1471
- const add = t.closest(sel("add-to-cart"));
1472
- if (own(add)) {
1473
- e.preventDefault();
1474
- this.onAddClick(add);
1475
- }
1476
- });
1477
- this.root.addEventListener("change", (e) => {
1478
- const t = e.target;
1479
- if (!own(t)) return;
1480
- if (t.matches(`select${sel("option")}`)) this.select(get(t, "option"), t.value);
1481
- else if (t.matches(`select${sel("variant-select")}`)) this.selectVariant(t.value);
1482
- else if (t.matches(`select${sel("plan")}`)) {
1483
- if (t.value) this.selectPlan(t.value);
1484
- else this.setPurchase("one-time");
1485
- } else if (t.matches(`select${sel("purchase")}`)) this.setPurchase(t.value);
1486
- else if (t.matches(sel("qty"))) this.setQuantity(t.value);
1487
- });
1488
- }
1489
- onAddClick(btn) {
1490
- const label = btn.querySelector(sel("add-label")) || btn;
1491
- const busyText = get(btn, "adding-text");
1492
- const write = (txt) => setText(label, txt);
1493
- if (label.dataset.tetherLabel === void 0) label.dataset.tetherLabel = label.tagName === "INPUT" ? label.value : label.textContent;
1494
- busy(btn, async () => {
1495
- if (busyText) write(busyText);
1496
- try {
1497
- await this.addToCart();
1498
- } catch (err) {
1499
- reportError(err, "add to cart failed");
1500
- emit("error", { error: err, context: "add-to-cart" }, this.root);
1501
- } finally {
1502
- if (busyText) write(label.dataset.tetherLabel);
1503
- }
1504
- }).then(() => {
1505
- this.renderVariant();
1506
- });
1507
- }
1508
- // ------------------------------------------------------------ render
1509
- renderProduct() {
1510
- const p = this.product;
1511
- this.$(sel("bind")).forEach((el) => {
1512
- var _a, _b, _c;
1513
- const key = get(el, "bind");
1514
- switch (key) {
1515
- case "title":
1516
- return setText(el, p.title);
1517
- case "description":
1518
- el.innerHTML = p.descriptionHtml || "";
1519
- return void 0;
1520
- case "description-text":
1521
- return setText(el, p.description);
1522
- case "vendor":
1523
- return setText(el, p.vendor);
1524
- case "type":
1525
- return setText(el, p.productType);
1526
- case "handle":
1527
- return setText(el, p.handle);
1528
- case "published-at":
1529
- return setText(el, formatDate(p.publishedAt));
1530
- case "min-price":
1531
- return setText(el, formatMoney(p.priceRange.minVariantPrice));
1532
- case "max-price":
1533
- return setText(el, formatMoney(p.priceRange.maxVariantPrice));
1534
- case "featured-image":
1535
- return setImage(el, (_a = p.featuredImage) == null ? void 0 : _a.url, ((_b = p.featuredImage) == null ? void 0 : _b.altText) || p.title);
1536
- case "url":
1537
- case "link":
1538
- el.setAttribute("href", productUrl(p.handle, p.id));
1539
- if (key === "url" && !el.firstElementChild && !el.textContent.trim()) setText(el, p.title);
1540
- return void 0;
1541
- case "images":
1542
- return repeat(el, p.images.nodes, (node, img) => {
1543
- const target = node.matches(`img,${sel("bind", "image")}`) ? node : node.querySelector("img") || node;
1544
- setImage(target, img.url, img.altText || p.title);
1545
- });
1546
- case "tags": {
1547
- const exclude = (el.getAttribute("exclude") || "").split(",").map((s) => s.trim()).filter(Boolean);
1548
- return repeat(el, p.tags.filter((t) => !exclude.includes(t)), (node, tag) => setText(node, tag));
1549
- }
1550
- default:
1551
- if (key && /^image:\d+$/.test(key)) {
1552
- const img = p.images.nodes[parseInt(key.slice(6), 10) - 1] || p.featuredImage;
1553
- return setImage(el, img == null ? void 0 : img.url, (img == null ? void 0 : img.altText) || p.title);
1554
- }
1555
- if (key && key.startsWith("metafield:")) {
1556
- const mf = metafieldValue(p, key.slice("metafield:".length));
1557
- if (!mf) return setText(el, "");
1558
- if ((_c = mf.reference) == null ? void 0 : _c.image) return setImage(el, mf.reference.image.url, mf.reference.image.altText);
1559
- if (mf.type === "rich_text_field" || mf.type === "multi_line_text_field") {
1560
- el.textContent = mf.value;
1561
- el.style.whiteSpace = "pre-line";
1562
- return void 0;
1563
- }
1564
- return setText(el, mf.value);
1565
- }
1566
- return void 0;
1567
- }
1568
- });
1569
- this.$(sel("option-list")).forEach((container) => {
1570
- const name = get(container, "option-list");
1571
- const opt = p.options.find((o) => o.name === name);
1572
- if (!opt || opt.optionValues.length <= 1 && has(container, "hide-single")) return show(container, false);
1573
- return repeat(container, opt.optionValues, (node, v) => {
1574
- var _a, _b, _c;
1575
- node.setAttribute(attr("option-value"), v.name);
1576
- const t = node.querySelector(sel("bind", "option-value"));
1577
- if (t) setText(t, v.name);
1578
- else if (!node.firstElementChild) setText(node, v.name);
1579
- const sw = node.querySelector(sel("bind", "swatch"));
1580
- if (sw && ((_a = v.swatch) == null ? void 0 : _a.color)) sw.style.backgroundColor = v.swatch.color;
1581
- if (sw && ((_c = (_b = v.swatch) == null ? void 0 : _b.image) == null ? void 0 : _c.previewImage)) setImage(sw, v.swatch.image.previewImage.url, v.name);
1582
- });
1583
- });
1584
- this.$(`select${sel("option")}`).forEach((s) => {
1585
- const opt = p.options.find((o) => o.name === get(s, "option"));
1586
- if (!opt) return show(s, false);
1587
- s.innerHTML = "";
1588
- opt.optionValues.forEach((v) => s.appendChild(new Option(v.name, v.name)));
1589
- return void 0;
1590
- });
1591
- this.$(`select${sel("variant-select")}`).forEach((s) => {
1592
- s.innerHTML = "";
1593
- p.variants.nodes.forEach((v) => s.appendChild(new Option(v.title, v.id)));
1594
- });
1595
- this.$(sel("plan-list")).forEach((container) => {
1596
- const raw = (get(container, "plan-list") || "").trim();
1597
- const group = ["", "true", "all", "*"].includes(raw.toLowerCase()) ? null : raw;
1598
- const plans = this.plans.filter((pl) => !group || pl.group.name === group);
1599
- repeat(container, plans, (node, plan) => {
1600
- node.setAttribute(attr("plan-value"), plan.id);
1601
- if (!node.firstElementChild && !node.querySelector(sel("plan-bind"))) setText(node, plan.name);
1602
- });
1603
- });
1604
- this.$(`select${sel("plan")}`).forEach((s) => {
1605
- s.innerHTML = "";
1606
- if (!p.requiresSellingPlan && get(s, "plan") !== "subscription-only") {
1607
- s.appendChild(new Option(s.getAttribute(attr("one-time-label")) || "One-time purchase", ""));
1608
- }
1609
- this.plans.forEach((pl) => s.appendChild(new Option(pl.name, pl.id)));
1610
- });
1611
- }
1612
- renderVariant() {
1613
- var _a, _b, _c;
1614
- const p = this.product;
1615
- let v = this.findVariant();
1616
- if (!v) {
1617
- [v] = p.variants.nodes;
1618
- if (v) {
1619
- this.selected = {};
1620
- v.selectedOptions.forEach((o) => {
1621
- this.selected[o.name] = o.value;
1622
- });
1623
- }
1624
- }
1625
- this.variant = v;
1626
- if (!v) return;
1627
- if (this.plans.length && !allocationFor(v, this.planId)) {
1628
- const alt = this.plans.find((pl) => allocationFor(v, pl.id));
1629
- if (alt) this.planId = alt.id;
1630
- }
1631
- const sub = this.isSubscription;
1632
- const oneTimePrice = v.price;
1633
- const subPrice = this.planId ? planPrice(v, this.planId) : null;
1634
- const price = sub ? subPrice : oneTimePrice;
1635
- const compareAt = sub ? ((_c = (_b = (_a = allocationFor(v, this.planId)) == null ? void 0 : _a.priceAdjustments) == null ? void 0 : _b[0]) == null ? void 0 : _c.compareAtPrice) || v.compareAtPrice : v.compareAtPrice;
1636
- const onSale = compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount);
1637
- const discountPct = onSale ? Math.round((1 - parseFloat(price.amount) / parseFloat(compareAt.amount)) * 100) : 0;
1638
- const savings = this.planId ? planSavings(v, this.planId) : { amount: null, percentage: 0 };
1639
- const plan = this.plan;
1640
- const total = { amount: (parseFloat(price.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: price.currencyCode };
1641
- this.$(sel("bind")).forEach((el) => {
1642
- var _a2, _b2, _c2, _d, _e;
1643
- switch (get(el, "bind")) {
1644
- case "price":
1645
- return setText(el, formatMoney(price));
1646
- case "total-price":
1647
- return setText(el, formatMoney(total));
1648
- case "one-time-price":
1649
- return setText(el, formatMoney(oneTimePrice));
1650
- case "subscription-price":
1651
- return setText(el, formatMoney(subPrice));
1652
- case "subscription-savings":
1653
- return setText(el, savings.percentage ? `${savings.percentage}%` : "");
1654
- case "subscription-savings-amount":
1655
- return setText(el, formatMoney(savings.amount));
1656
- case "checkout-charge":
1657
- return setText(el, formatMoney(sub ? planCheckoutCharge(v, this.planId) : price));
1658
- case "plan-name":
1659
- return setText(el, (plan == null ? void 0 : plan.name) || "");
1660
- case "plan-description":
1661
- return setText(el, (plan == null ? void 0 : plan.description) || "");
1662
- case "plan-group":
1663
- return setText(el, ((_a2 = plan == null ? void 0 : plan.group) == null ? void 0 : _a2.name) || "");
1664
- case "compare-at-price":
1665
- return setText(el, onSale ? formatMoney(compareAt) : "");
1666
- case "unit-price":
1667
- return setText(el, formatMoney(v.unitPrice));
1668
- case "discount-percentage":
1669
- return setText(el, `${discountPct}%`);
1670
- case "discount-amount":
1671
- return setText(el, onSale ? formatMoney({
1672
- amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)),
1673
- currencyCode: price.currencyCode
1674
- }) : "");
1675
- case "variant-title":
1676
- return setText(el, v.title);
1677
- case "variant-id":
1678
- return setText(el, v.id);
1679
- case "sku":
1680
- return setText(el, v.sku);
1681
- case "barcode":
1682
- return setText(el, v.barcode);
1683
- case "stock":
1684
- return setText(el, (_b2 = v.quantityAvailable) != null ? _b2 : "");
1685
- case "quantity":
1686
- return setText(el, this.quantity);
1687
- case "variant-image":
1688
- return setImage(el, ((_c2 = v.image) == null ? void 0 : _c2.url) || ((_d = p.featuredImage) == null ? void 0 : _d.url), ((_e = v.image) == null ? void 0 : _e.altText) || p.title);
1689
- default:
1690
- return void 0;
1691
- }
1692
- });
1693
- this.$(sel("option-selected")).forEach((el) => setText(el, this.selected[get(el, "option-selected")] || ""));
1694
- this.$(sel("option-list")).forEach((container) => {
1695
- const name = get(container, "option-list");
1696
- qsa(container, sel("option-value")).forEach((node) => {
1697
- const value = get(node, "option-value");
1698
- setSelected(node, this.selected[name] === value);
1699
- const unavailable = !this.isValueAvailable(name, value);
1700
- node.classList.toggle("is-unavailable", unavailable);
1701
- node.setAttribute("data-unavailable", unavailable ? "true" : "false");
1702
- });
1703
- });
1704
- this.$(`select${sel("option")}`).forEach((s) => {
1705
- s.value = this.selected[get(s, "option")] || "";
1706
- });
1707
- this.$(`select${sel("variant-select")}`).forEach((s) => {
1708
- s.value = v.id;
1709
- });
1710
- this.$(sel("purchase")).forEach((el) => {
1711
- if (el.tagName === "SELECT") {
1712
- el.value = this.purchase;
1713
- return;
1714
- }
1715
- const mode = get(el, "purchase");
1716
- setSelected(el, mode === this.purchase);
1717
- const unavailable = mode === "subscription" ? !this.subscriptionAvailable : p.requiresSellingPlan;
1718
- el.classList.toggle("is-unavailable", unavailable);
1719
- });
1720
- this.$(sel("plan-value")).forEach((node) => {
1721
- const id = get(node, "plan-value");
1722
- const planObj = this.plans.find((pl) => pl.id === id);
1723
- const inVariant = Boolean(allocationFor(v, id));
1724
- setSelected(node, sub && id === this.planId);
1725
- node.classList.toggle("is-unavailable", !inVariant);
1726
- qsa(node, sel("plan-bind")).forEach((el) => {
1727
- const adj = planAdjustment(planObj);
1728
- const s = planSavings(v, id);
1729
- switch (get(el, "plan-bind")) {
1730
- case "name":
1731
- return setText(el, planObj == null ? void 0 : planObj.name);
1732
- case "description":
1733
- return setText(el, (planObj == null ? void 0 : planObj.description) || "");
1734
- case "price":
1735
- return setText(el, formatMoney(planPrice(v, id)));
1736
- case "savings":
1737
- return setText(el, s.percentage ? `${s.percentage}%` : "");
1738
- case "savings-amount":
1739
- return setText(el, formatMoney(s.amount));
1740
- case "adjustment":
1741
- if (!adj) return setText(el, "");
1742
- return setText(el, adj.type === "percentage" ? `${adj.value}%` : formatMoney(adj.value));
1743
- default:
1744
- return void 0;
1745
- }
1746
- });
1747
- });
1748
- this.$(`select${sel("plan")}`).forEach((s) => {
1749
- s.value = sub ? this.planId : "";
1750
- });
1751
- const conds = {
1752
- "in-stock": v.availableForSale,
1753
- "out-of-stock": !v.availableForSale,
1754
- "backorder": v.availableForSale && v.currentlyNotInStock,
1755
- "on-sale": Boolean(onSale),
1756
- "not-on-sale": !onSale,
1757
- "has-variants": p.variants.nodes.length > 1,
1758
- "single-variant": p.variants.nodes.length === 1,
1759
- "low-stock": v.quantityAvailable != null && v.quantityAvailable > 0 && v.quantityAvailable <= 5,
1760
- "subscription-available": this.subscriptionAvailable,
1761
- "subscription-unavailable": !this.subscriptionAvailable,
1762
- "subscription-only": Boolean(p.requiresSellingPlan),
1763
- "one-time-available": !p.requiresSellingPlan,
1764
- "subscription-selected": sub,
1765
- "one-time-selected": !sub,
1766
- "has-savings": sub ? savings.percentage > 0 : Boolean(onSale),
1767
- // scope state (see status.js)
1768
- "loading": false,
1769
- "ready": true,
1770
- "error": false,
1771
- "not-found": false
1772
- };
1773
- this.$(sel("show")).forEach((el) => {
1774
- const k = get(el, "show");
1775
- if (!(k in conds)) return;
1776
- if (STATUS_KEYS.includes(k) && !ownedBy(el, this.root)) return;
1777
- show(el, conds[k]);
1778
- });
1779
- this.root.classList.toggle("is-subscription", sub);
1780
- this.root.setAttribute(attr("purchase-mode"), sub ? "subscription" : "one-time");
1781
- this.$(sel("add-to-cart")).forEach((btn) => {
1782
- const disabled = !v.availableForSale;
1783
- btn.classList.toggle("is-disabled", disabled);
1784
- if ("disabled" in btn) btn.disabled = disabled;
1785
- const soldOut = get(btn, "soldout-text");
1786
- const subText = get(btn, "subscribe-text");
1787
- if (soldOut || subText) {
1788
- const label = btn.querySelector(sel("add-label")) || btn;
1789
- if (label.dataset.tetherLabel === void 0) label.dataset.tetherLabel = label.tagName === "INPUT" ? label.value : label.textContent;
1790
- let text = label.dataset.tetherLabel;
1791
- if (sub && subText) text = subText;
1792
- if (disabled && soldOut) text = soldOut;
1793
- if (btn.getAttribute("aria-busy") !== "true") setText(label, text);
1794
- }
1795
- });
1796
- emit("variant-changed", {
1797
- product: p,
1798
- variant: v,
1799
- selected: { ...this.selected },
1800
- purchase: this.purchase,
1801
- sellingPlanId: sub ? this.planId : null
1802
- }, this.root);
1803
- }
1804
- };
1805
-
1806
- // src/collection.js
1807
- var listSelector = () => `${sel("collection")},${sel("products")},${sel("recommendations")}`;
1808
- var ListScope = class {
1809
- constructor(root) {
1810
- this.root = root;
1811
- this.products = [];
1812
- this.pageInfo = null;
1813
- this.collection = null;
1814
- setScopeStatus(root, "loading");
1815
- root.addEventListener("click", (e) => {
1816
- var _a, _b;
1817
- const more = (_b = (_a = e.target).closest) == null ? void 0 : _b.call(_a, sel("load-more"));
1818
- if (more && more.closest(listSelector()) === root) {
1819
- e.preventDefault();
1820
- busy(more, () => this.load(true));
1821
- }
1822
- });
1823
- }
1824
- $(s) {
1825
- return scoped(this.root, s, listSelector());
1826
- }
1827
- options() {
1828
- const r = this.root;
1829
- const opts = {
1830
- first: parseInt(get(r, "limit") || "", 10) || void 0,
1831
- sortKey: get(r, "sort") || void 0,
1832
- reverse: get(r, "reverse") === "true"
1833
- };
1834
- if (has(r, "collection")) opts.collection = get(r, "collection");
1835
- else opts.query = get(r, "products") || "";
1836
- return opts;
1837
- }
1838
- async load(more = false) {
1839
- var _a;
1840
- const token = this.loadToken = (this.loadToken || 0) + 1;
1841
- const stale = () => token !== this.loadToken;
1842
- try {
1843
- if (has(this.root, "recommendations")) {
1844
- const key = get(this.root, "recommendations");
1845
- const base = await getProduct(key);
1846
- const recs = await getRecommendations(base.id, get(this.root, "intent") || "RELATED");
1847
- if (stale()) return;
1848
- const limit = parseInt(get(this.root, "limit") || "", 10);
1849
- this.products = limit ? recs.slice(0, limit) : recs;
1850
- } else {
1851
- const res = await getProducts({ ...this.options(), after: more ? (_a = this.pageInfo) == null ? void 0 : _a.endCursor : null });
1852
- if (stale()) return;
1853
- this.collection = res.collection;
1854
- this.pageInfo = res.pageInfo;
1855
- this.products = more ? [...this.products, ...res.products] : res.products;
1856
- }
1857
- this.render();
1858
- setScopeStatus(this.root, "ready");
1859
- emit("list-ready", { products: this.products, collection: this.collection }, this.root);
1860
- } catch (err) {
1861
- if (stale()) return;
1862
- if (!more) setScopeStatus(this.root, isNotFound(err) ? "not-found" : "error", err);
1863
- reportError(err, "list load failed");
1864
- emit("error", { error: err, context: "list" }, this.root);
1865
- }
1866
- }
1867
- render() {
1868
- const c = this.collection;
1869
- this.$(sel("collection-bind")).forEach((el) => {
1870
- var _a, _b;
1871
- switch (get(el, "collection-bind")) {
1872
- case "title":
1873
- return setText(el, (c == null ? void 0 : c.title) || "");
1874
- case "description":
1875
- el.innerHTML = (c == null ? void 0 : c.descriptionHtml) || "";
1876
- return void 0;
1877
- case "image":
1878
- return setImage(el, (_a = c == null ? void 0 : c.image) == null ? void 0 : _a.url, ((_b = c == null ? void 0 : c.image) == null ? void 0 : _b.altText) || (c == null ? void 0 : c.title));
1879
- case "count":
1880
- return setText(el, this.products.length);
1881
- default:
1882
- return void 0;
1883
- }
1884
- });
1885
- this.$(sel("list")).forEach((container) => {
1886
- repeat(container, this.products, (node, p) => {
1887
- node.setAttribute(attr("product"), p.handle);
1888
- node.removeAttribute(attr("state"));
1889
- });
1890
- emit("list-rendered", { container }, this.root);
1891
- });
1892
- const empty = this.products.length === 0;
1893
- this.$(sel("show")).forEach((el) => {
1894
- const k = get(el, "show");
1895
- if (k === "list-empty") el.style.display = empty ? "" : "none";
1896
- if (k === "list-filled") el.style.display = empty ? "none" : "";
1897
- });
1898
- this.$(sel("load-more")).forEach((el) => {
1899
- var _a;
1900
- el.style.display = ((_a = this.pageInfo) == null ? void 0 : _a.hasNextPage) ? "" : "none";
1901
- });
1902
- notifyLayoutChanged("list");
1903
- }
1904
- };
1905
-
1906
- // src/api-version.js
1907
- var parse = (v) => {
1908
- const m = /^(\d{4})-(01|04|07|10)$/.exec(String(v || ""));
1909
- return m ? { y: +m[1], m: +m[2] } : null;
1910
- };
1911
- var fmt = (y, m) => `${y}-${String(m).padStart(2, "0")}`;
1912
- function nextApiVersion(v, steps = 1) {
1913
- const p = parse(v);
1914
- if (!p) throw new Error(`not a quarterly Shopify API version: ${v}`);
1915
- const q = p.y * 4 + (p.m - 1) / 3 + steps;
1916
- return fmt(Math.floor(q / 4), q % 4 * 3 + 1);
1917
- }
1918
- function apiVersionStatus(version, now = /* @__PURE__ */ new Date()) {
1919
- if (version === "unstable") return { version, ok: false, note: '"unstable" changes without notice: pin a dated version such as 2026-07' };
1920
- const p = parse(version);
1921
- if (!p) return { version, ok: false, note: "not a quarterly version (YYYY-01, -04, -07 or -10)" };
1922
- const released = new Date(Date.UTC(p.y, p.m - 1, 1));
1923
- const supportedUntil = new Date(Date.UTC(p.y + 1, p.m - 1, 1));
1924
- const monthsLeft = (supportedUntil - now) / (30.44 * 24 * 3600 * 1e3);
1925
- let note = `supported until about ${supportedUntil.toISOString().slice(0, 7)}`;
1926
- if (released > now) note = "not released yet (release candidate at best)";
1927
- else if (monthsLeft <= 0) note = "no longer supported: Shopify falls back to the oldest supported version. Upgrade (see README)";
1928
- else if (monthsLeft < 3) note = `${note}: plan the upgrade to ${nextApiVersion(version)} or later (see README)`;
1929
- return { version, ok: released <= now && monthsLeft > 0, monthsLeft: Math.round(monthsLeft), next: nextApiVersion(version), note };
1930
- }
1931
-
1932
- // src/markets.js
1933
- var COUNTRY = /^[A-Z]{2}$/;
1934
- var REGIONAL_LANGUAGES = /* @__PURE__ */ new Set(["PT_BR", "PT_PT", "ZH_CN", "ZH_TW"]);
1935
- var storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
1936
- var defaultEnv = () => ({
1937
- document: typeof document !== "undefined" ? document : null,
1938
- location: typeof window !== "undefined" ? window.location : null,
1939
- storage: (() => {
1940
- try {
1941
- return window.localStorage;
1942
- } catch {
1943
- return null;
1944
- }
1945
- })(),
1946
- global: typeof window !== "undefined" ? window : {}
1947
- });
1948
- function toCountry(v) {
1949
- const c = String(v || "").trim().toUpperCase();
1950
- return COUNTRY.test(c) && c !== "XX" && c !== "T1" ? c : null;
1951
- }
1952
- function toLanguage(v) {
1953
- const raw = String(v || "").trim().replace("-", "_").toUpperCase();
1954
- if (!raw) return null;
1955
- if (REGIONAL_LANGUAGES.has(raw)) return raw;
1956
- const primary = raw.split("_")[0];
1957
- return /^[A-Z]{2,3}$/.test(primary) ? primary : null;
1958
- }
1959
- function readCookie(doc, name) {
1960
- if (!doc || !name) return null;
1961
- const hit = String(doc.cookie || "").split(";").map((c) => c.trim()).find((c) => c.startsWith(`${name}=`));
1962
- return hit ? decodeURIComponent(hit.slice(name.length + 1)) : null;
1963
- }
1964
- var lookup = (locales, key) => {
1965
- if (!locales || !key) return null;
1966
- const k = String(key).toLowerCase();
1967
- const found = Object.keys(locales).find((x) => x.toLowerCase() === k);
1968
- return found ? locales[found] : null;
1969
- };
1970
- var readers = {
1971
- saved(m, env) {
1972
- if (m.remember === false || !env.storage) return null;
1973
- try {
1974
- return JSON.parse(env.storage.getItem(storageKey()) || "null");
1975
- } catch {
1976
- return null;
1977
- }
1978
- },
1979
- server(m, env) {
1980
- const meta = (name) => {
1981
- var _a, _b, _c;
1982
- return (_c = (_b = (_a = env.document) == null ? void 0 : _a.querySelector) == null ? void 0 : _b.call(_a, `meta[name="${name}"]`)) == null ? void 0 : _c.getAttribute("content");
1983
- };
1984
- const country = env.global.TETHER_COUNTRY || meta("tether-country") || readCookie(env.document, m.cookie || "tether_country");
1985
- const language = env.global.TETHER_LANGUAGE || meta("tether-language");
1986
- return { country, language };
1987
- },
1988
- webflow(m, env) {
1989
- var _a, _b, _c;
1990
- const segment = (((_a = env.location) == null ? void 0 : _a.pathname) || "/").split("/").filter(Boolean)[0];
1991
- const fromPath = lookup(m.locales, segment);
1992
- if (fromPath) return fromPath;
1993
- const lang = (_c = (_b = env.document) == null ? void 0 : _b.documentElement) == null ? void 0 : _c.getAttribute("lang");
1994
- if (!lang) return null;
1995
- const mapped = lookup(m.locales, lang) || lookup(m.locales, lang.split("-")[0]);
1996
- if (mapped) return { locale: lang, ...mapped };
1997
- if (m.locales && m.strict) return null;
1998
- const region = lang.split("-")[1];
1999
- return { language: lang, country: region && region.length === 2 ? region : null, locale: lang };
2000
- }
2001
- };
2002
- function detectMarket(markets = cfg.markets, env = defaultEnv()) {
2003
- const m = markets || {};
2004
- if (m.detect === false) return {};
2005
- const list = !m.detect ? ["saved"] : m.detect === true ? ["saved", "server", "webflow"] : [].concat(m.detect);
2006
- markets = m;
2007
- const out = { sources: {} };
2008
- list.forEach((name) => {
2009
- var _a;
2010
- const r = (_a = readers[name]) == null ? void 0 : _a.call(readers, markets, env);
2011
- if (!r) return;
2012
- const vals = { country: toCountry(r.country), language: toLanguage(r.language), locale: r.locale || null };
2013
- Object.entries(vals).forEach(([k, v]) => {
2014
- if (v && !out[k]) {
2015
- out[k] = v;
2016
- out.sources[k] = name;
2017
- }
2018
- });
2019
- });
2020
- return out;
2021
- }
2022
- function rememberMarket(market) {
2023
- var _a;
2024
- if (((_a = cfg.markets) == null ? void 0 : _a.remember) === false) return;
2025
- try {
2026
- localStorage.setItem(storageKey(), JSON.stringify({ country: market.country, language: market.language || void 0 }));
2027
- } catch {
2028
- }
2029
- }
2030
- var localizationPromise = null;
2031
- function clearLocalization() {
2032
- localizationPromise = null;
2033
- }
2034
- function getLocalization() {
2035
- if (!localizationPromise) {
2036
- localizationPromise = gql(`query TetherLocalization {
2037
- localization {
2038
- country { isoCode name currency { isoCode symbol } }
2039
- language { isoCode name }
2040
- availableCountries { isoCode name currency { isoCode symbol } availableLanguages { isoCode name } }
2041
- }
2042
- }`).then((d) => d.localization).catch((err) => {
2043
- localizationPromise = null;
2044
- throw err;
2045
- });
2046
- }
2047
- return localizationPromise;
2048
- }
2049
-
2050
- // src/diagnose.js
2051
- var mask = (t) => t ? `${String(t).slice(0, 4)}\u2026${String(t).slice(-4)} (${String(t).length} chars)` : "(missing)";
2052
- function describe(el) {
2053
- const id = el.id ? `#${el.id}` : "";
2054
- const cls = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
2055
- return `<${el.tagName.toLowerCase()}${id}${cls}>`;
2056
- }
2057
- function checkMarkup(root = document) {
2058
- const problems = [];
2059
- const add = (level, message, el) => problems.push({ level, message, element: el ? describe(el) : void 0, el });
2060
- const inProduct = (el) => Boolean(el.closest(sel("product")));
2061
- const listSel = `${sel("collection")},${sel("products")},${sel("recommendations")}`;
2062
- qsa(root, sel("product")).forEach((el) => {
2063
- if (el.closest(listSel) && el.closest(sel("list"))) return;
2064
- const { key, fromUrl, raw } = resolveProductKey(el);
2065
- if (!raw) add("error", `${attr("product")} has no value. Bind it to the Shopify handle (CMS field), or use "url:handle".`, el);
2066
- else if (fromUrl && !key) add("warn", `${attr("product")}="${raw}" but this URL has no value for it (e.g. ?handle=my-product).`, el);
2067
- });
2068
- const productOnly = ["bind", "option-list", "option", "variant-select", "add-to-cart", "purchase", "plan-list", "plan", "qty", "line-attribute"];
2069
- productOnly.forEach((name) => {
2070
- qsa(root, sel(name)).forEach((el) => {
2071
- if (inProduct(el)) return;
2072
- if (name === "line-attribute" && el.closest(sel("group"))) return;
2073
- add("warn", `${attr(name)} outside any ${attr("product")}: it does nothing here.`, el);
2074
- });
2075
- });
2076
- qsa(root, sel("group-add")).forEach((el) => {
2077
- if (!el.closest(sel("group"))) add("warn", `${attr("group-add")} outside a ${attr("group")}.`, el);
2078
- });
2079
- [...qsa(root, sel("option-list")), ...qsa(root, sel("plan-list")), ...qsa(root, sel("cart-lines")), ...qsa(root, sel("list"))].forEach((el) => {
2080
- if (!el.firstElementChild && !hasTemplate(el)) {
2081
- add("warn", "Repeater without a template child: put one example element inside it.", el);
2082
- }
2083
- });
2084
- qsa(root, listSel).forEach((el) => {
2085
- if (!qsa(el, sel("list")).length) add("error", `List without a ${attr("list")} child: products have nowhere to render.`, el);
2086
- });
2087
- const known = /* @__PURE__ */ new Set([
2088
- "in-stock",
2089
- "out-of-stock",
2090
- "backorder",
2091
- "low-stock",
2092
- "on-sale",
2093
- "not-on-sale",
2094
- "has-variants",
2095
- "single-variant",
2096
- "subscription-available",
2097
- "subscription-unavailable",
2098
- "subscription-only",
2099
- "one-time-available",
2100
- "subscription-selected",
2101
- "one-time-selected",
2102
- "has-savings",
2103
- "loading",
2104
- "ready",
2105
- "error",
2106
- "not-found",
2107
- "cart-empty",
2108
- "cart-filled",
2109
- "cart-has-subscription",
2110
- "cart-no-subscription",
2111
- "cart-has-discount",
2112
- "cart-error",
2113
- "list-empty",
2114
- "list-filled"
2115
- ]);
2116
- qsa(root, sel("show")).forEach((el) => {
2117
- const k = get(el, "show");
2118
- if (!known.has(k)) add("warn", `Unknown ${attr("show")}="${k}".`, el);
2119
- });
2120
- qsa(root, sel("bind")).forEach((el) => {
2121
- const k = get(el, "bind") || "";
2122
- if (!k.startsWith("metafield:")) return;
2123
- const ref = k.slice(10);
2124
- const listed = (cfg.metafields || []).some((m) => (typeof m === "string" ? m : `${m.namespace}.${m.key}`) === ref);
2125
- if (!listed) add("error", `${attr("bind")}="${k}" but "${ref}" is not in config.metafields, so it stays empty.`, el);
2126
- });
2127
- if (qsa(root, sel("cart-open")).length && !qsa(root, sel("cart-drawer")).length) {
2128
- add("warn", `${attr("cart-open")} exists but there is no ${attr("cart-drawer")} on this page.`);
2129
- }
2130
- if (qsa(root, sel("cart-drawer")).length > 1) add("warn", `More than one ${attr("cart-drawer")}.`);
2131
- qsa(root, `[${attr("state")}="error"]`).forEach((el) => {
2132
- if (el.matches(boundary())) add("error", `Scope failed to load (${get(el, "error") || "error"}).`, el);
2133
- });
2134
- return problems;
2135
- }
2136
- async function diagnose({ root = document, silent = false, version, now = /* @__PURE__ */ new Date() } = {}) {
2137
- var _a, _b, _c, _d, _e, _f, _g;
2138
- const report = {
2139
- version,
2140
- config: {
2141
- domain: cfg.domain || "(missing)",
2142
- token: mask(cfg.token),
2143
- apiVersion: cfg.apiVersion,
2144
- endpoint: cfg.domain ? endpoint() : null,
2145
- country: cfg.country,
2146
- language: cfg.language,
2147
- locale: cfg.locale,
2148
- markets: cfg.markets || null,
2149
- prefix: cfg.prefix,
2150
- productPath: cfg.productPath,
2151
- metafields: cfg.metafields,
2152
- autoMount: cfg.autoMount,
2153
- observe: cfg.observe,
2154
- onError: typeof cfg.onError === "function"
2155
- },
2156
- apiVersion: apiVersionStatus(cfg.apiVersion, now),
2157
- market: ((_a = cfg.markets) == null ? void 0 : _a.detect) ? detectMarket(cfg.markets) : null,
2158
- reachable: false,
2159
- shop: null,
2160
- latencyMs: null,
2161
- products: [],
2162
- sellingPlans: [],
2163
- problems: []
2164
- };
2165
- if (!isConfigured()) {
2166
- report.problems.push({ level: "error", message: "TETHER_CONFIG needs domain and token." });
2167
- } else {
2168
- const t0 = Date.now();
2169
- try {
2170
- const data = await gql("query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }", {}, { context: false });
2171
- report.reachable = true;
2172
- report.shop = { name: (_b = data.shop) == null ? void 0 : _b.name, url: (_d = (_c = data.shop) == null ? void 0 : _c.primaryDomain) == null ? void 0 : _d.url, currency: (_f = (_e = data.shop) == null ? void 0 : _e.paymentSettings) == null ? void 0 : _f.currencyCode };
2173
- } catch (err) {
2174
- const hint = err.status === 401 || err.status === 403 ? "Token rejected: use the public Storefront token from the Headless channel, not an Admin token." : err.status === 404 ? "Store or API version not found: check domain (x.myshopify.com) and apiVersion." : "";
2175
- report.problems.push({ level: "error", message: `Storefront API not reachable: ${err.message}${hint ? `. ${hint}` : ""}` });
2176
- }
2177
- report.latencyMs = Date.now() - t0;
2178
- if (report.reachable) {
2179
- const keys = [...new Set(qsa(root, sel("product")).map((el) => resolveProductKey(el).key).filter(Boolean))].slice(0, 20);
2180
- const results = await Promise.allSettled(keys.map((k) => getProduct(k)));
2181
- results.forEach((r, i) => {
2182
- var _a2, _b2;
2183
- if (r.status === "fulfilled") {
2184
- const p = r.value;
2185
- const plans = allPlans(p);
2186
- report.products.push({ key: keys[i], handle: p.handle, title: p.title, plans: plans.length, requiresSellingPlan: p.requiresSellingPlan });
2187
- plans.forEach((pl) => {
2188
- var _a3;
2189
- return report.sellingPlans.push({ product: p.handle, group: (_a3 = pl.group) == null ? void 0 : _a3.name, plan: pl.name, id: pl.id });
2190
- });
2191
- } else {
2192
- report.products.push({ key: keys[i], error: (_a2 = r.reason) == null ? void 0 : _a2.message });
2193
- report.problems.push({ level: "error", message: `Product "${keys[i]}": ${(_b2 = r.reason) == null ? void 0 : _b2.message}. Is it published to the Headless channel?` });
2194
- }
2195
- });
2196
- }
2197
- }
2198
- if (!report.apiVersion.ok || ((_g = report.apiVersion.monthsLeft) != null ? _g : 99) < 3) {
2199
- report.problems.push({ level: report.apiVersion.ok ? "warn" : "error", message: `apiVersion ${cfg.apiVersion}: ${report.apiVersion.note}` });
2200
- }
2201
- if (typeof document !== "undefined") report.problems.push(...checkMarkup(root));
2202
- if (!silent) print(report);
2203
- return report;
2204
- }
2205
- function print(r) {
2206
- var _a, _b, _c;
2207
- const c = console;
2208
- const errors = r.problems.filter((p) => p.level === "error").length;
2209
- (_a = c.group) == null ? void 0 : _a.call(c, `[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : "no errors"}, ${r.problems.length - errors} warning(s)`);
2210
- c.log("version", r.version, "| API", r.apiVersion.version, `(${r.apiVersion.note})`);
2211
- c.log("config", r.config);
2212
- if (r.market) c.log("market detected", r.market, "(in use:", r.config.country, r.config.language, ")");
2213
- c.log(r.reachable ? `Storefront API reachable: ${(_b = r.shop) == null ? void 0 : _b.name} (${r.latencyMs} ms)` : "Storefront API NOT reachable");
2214
- if (r.products.length) (c.table || c.log)(r.products);
2215
- if (r.sellingPlans.length) (c.table || c.log)(r.sellingPlans);
2216
- else if (r.reachable) c.log("No selling plans on the products of this page.");
2217
- r.problems.forEach((p) => (p.level === "error" ? c.error : c.warn)(p.message, p.el || ""));
2218
- (_c = c.groupEnd) == null ? void 0 : _c.call(c);
2219
- }
2220
-
2221
- // src/index.js
2222
- var VERSION = "0.3.5";
2223
- var mountedProducts = /* @__PURE__ */ new WeakSet();
2224
- var mountedLists = /* @__PURE__ */ new WeakSet();
2225
- var liveProducts = /* @__PURE__ */ new Set();
2226
- var liveLists = /* @__PURE__ */ new Set();
2227
- var requestToken = /* @__PURE__ */ new WeakMap();
2228
- function pruneLive() {
2229
- liveProducts.forEach((el) => {
2230
- if (!el.isConnected) liveProducts.delete(el);
2231
- });
2232
- liveLists.forEach((l) => {
2233
- if (!l.root.isConnected) liveLists.delete(l);
2234
- });
2235
- }
2236
- var initPromise = null;
2237
- var observer = null;
2238
- function within(root, selector) {
2239
- var _a;
2240
- return [...((_a = root.matches) == null ? void 0 : _a.call(root, selector)) ? [root] : [], ...qsa(root, selector)];
2241
- }
2242
- function mount(root = document) {
2243
- if (!isConfigured()) return 0;
2244
- let count = 0;
2245
- pruneLive();
2246
- within(root, listSelector()).forEach((el) => {
2247
- if (mountedLists.has(el)) return;
2248
- mountedLists.add(el);
2249
- count++;
2250
- const list = new ListScope(el);
2251
- liveLists.add(list);
2252
- el.addEventListener(`${cfg.prefix}:list-rendered`, (e) => mount(e.detail.container));
2253
- list.load();
2254
- });
2255
- within(root, sel("product")).forEach((el) => {
2256
- if (mountProduct(el)) count++;
2257
- });
2258
- renderCountrySelects(root);
2259
- qsa(root, sel("account-link")).forEach((a) => {
2260
- a.setAttribute("href", `https://${cfg.domain}/account`);
2261
- });
2262
- Cart.render();
2263
- log(`mounted ${count} scope(s)`, root);
2264
- return count;
2265
- }
2266
- function mountProduct(el) {
2267
- if (mountedProducts.has(el)) return false;
2268
- const { key, fromUrl, raw } = resolveProductKey(el);
2269
- if (!key) {
2270
- if (fromUrl) {
2271
- mountedProducts.add(el);
2272
- setScopeStatus(el, "not-found");
2273
- emit("product-not-found", { key: null, source: raw }, el);
2274
- } else if (!el.closest(listSelector())) {
2275
- warn(`[${attr("product")}] without a value. Bind it to the Shopify handle field, or use "url:handle".`, el);
2276
- }
2277
- return false;
2278
- }
2279
- mountedProducts.add(el);
2280
- liveProducts.add(el);
2281
- const token = {};
2282
- requestToken.set(el, token);
2283
- const current = () => requestToken.get(el) === token;
2284
- if (!scopes.get(el)) setScopeStatus(el, "loading");
2285
- else el.setAttribute(attr("state"), "updating");
2286
- getProduct(key).then((p) => {
2287
- if (!current()) return;
2288
- const existing = scopes.get(el);
2289
- if (existing) existing.update(p);
2290
- else new ProductScope(el, p);
2291
- applyPageTitle(el, p);
2292
- }).catch((err) => {
2293
- if (!current()) return;
2294
- const notFound = isNotFound(err);
2295
- setScopeStatus(el, notFound ? "not-found" : "error", err);
2296
- if (notFound) {
2297
- emit("product-not-found", { key, source: raw }, el);
2298
- if (fromUrl) log(`product not found: ${key}`);
2299
- else reportError(err, `product ${key}`);
2300
- } else {
2301
- reportError(err, `product ${key}`);
2302
- emit("error", { error: err, context: "product", key }, el);
2303
- }
2304
- });
2305
- return true;
2306
- }
2307
- function renderCountrySelects(root = document) {
2308
- const selects = within(root, `select${sel("country-select")}`);
2309
- const binds = within(root, sel("country-bind"));
2310
- if (!selects.length && !binds.length) return Promise.resolve();
2311
- return getLocalization().then((loc) => {
2312
- var _a;
2313
- const current = (cfg.country || ((_a = loc.country) == null ? void 0 : _a.isoCode) || "").toUpperCase();
2314
- const countries = [...loc.availableCountries || []].sort((a, b) => a.name.localeCompare(b.name, cfg.locale || void 0));
2315
- const info = countries.find((c) => c.isoCode === current) || loc.country;
2316
- const fill = (tpl, c) => {
2317
- var _a2, _b;
2318
- return tpl.replace(/\{name\}/g, c.name).replace(/\{code\}/g, c.isoCode).replace(/\{currency\}/g, ((_a2 = c.currency) == null ? void 0 : _a2.isoCode) || "").replace(/\{symbol\}/g, ((_b = c.currency) == null ? void 0 : _b.symbol) || "");
2319
- };
2320
- selects.forEach((s) => {
2321
- const tpl = s.getAttribute(attr("country-label")) || "{name} ({currency})";
2322
- s.innerHTML = "";
2323
- countries.forEach((c) => s.appendChild(new Option(fill(tpl, c), c.isoCode)));
2324
- s.value = current;
2325
- if (s.value !== current) s.selectedIndex = -1;
2326
- s.disabled = countries.length < 2 && !s.hasAttribute(attr("always-enabled"));
2327
- });
2328
- binds.forEach((el) => {
2329
- var _a2, _b;
2330
- const k = get(el, "country-bind") || "name";
2331
- const v = { name: info == null ? void 0 : info.name, code: info == null ? void 0 : info.isoCode, currency: (_a2 = info == null ? void 0 : info.currency) == null ? void 0 : _a2.isoCode, symbol: (_b = info == null ? void 0 : info.currency) == null ? void 0 : _b.symbol }[k];
2332
- el.textContent = v || "";
2333
- });
2334
- }).catch((err) => reportError(err, "country select"));
2335
- }
2336
- async function setCountry(code, { language, remember = true } = {}) {
2337
- const country = toCountry(code);
2338
- if (!country) throw new TetherError(`Not a country code: ${code}`, { code: "INVALID_COUNTRY" });
2339
- const lang = language === void 0 ? cfg.language : toLanguage(language);
2340
- const changed = country !== String(cfg.country || "").toUpperCase() || lang !== cfg.language;
2341
- cfg.country = country;
2342
- cfg.language = lang || void 0;
2343
- if (remember) rememberMarket({ country, language: lang });
2344
- if (!changed) {
2345
- renderCountrySelects();
2346
- return Cart.state;
2347
- }
2348
- clearProductCache();
2349
- const jobs = [];
2350
- if (language !== void 0) clearLocalization();
2351
- if (Cart.id()) jobs.push(Cart.setBuyerIdentity({ countryCode: country }, { create: false }).catch((err) => reportError(err, "cart country")));
2352
- pruneLive();
2353
- liveLists.forEach((list) => jobs.push(list.load()));
2354
- liveProducts.forEach((el) => {
2355
- if (el.closest(listSelector())) return;
2356
- mountedProducts.delete(el);
2357
- mountProduct(el);
2358
- });
2359
- renderCountrySelects();
2360
- emit("country-changed", { country, language: cfg.language });
2361
- await Promise.all(jobs);
2362
- return Cart.state;
2363
- }
2364
- function applyPageTitle(el, p) {
2365
- const tpl = get(el, "page-title");
2366
- if (!tpl || typeof document === "undefined") return;
2367
- document.title = tpl.replace(/\{title\}/g, p.title).replace(/\{vendor\}/g, p.vendor || "").replace(/\{type\}/g, p.productType || "");
2368
- }
2369
- function unmount() {
2370
- Cart.close();
2371
- }
2372
- function getScope(el) {
2373
- var _a;
2374
- const root = (_a = el == null ? void 0 : el.closest) == null ? void 0 : _a.call(el, sel("product"));
2375
- return root ? scopes.get(root) || null : null;
2376
- }
2377
- var controlsBound = false;
2378
- function bindControls() {
2379
- if (controlsBound) return;
2380
- controlsBound = true;
2381
- document.addEventListener("click", (e) => {
2382
- var _a;
2383
- const t = e.target;
2384
- if (!(t == null ? void 0 : t.closest)) return;
2385
- if (t.closest(sel("cart-open"))) {
2386
- e.preventDefault();
2387
- Cart.toggle();
2388
- return;
2389
- }
2390
- if (t.closest(sel("cart-close"))) {
2391
- e.preventDefault();
2392
- Cart.close();
2393
- return;
2394
- }
2395
- const clear = t.closest(sel("cart-clear"));
2396
- if (clear) {
2397
- e.preventDefault();
2398
- busy(clear, () => Cart.clear().catch((err) => reportError(err, "cart clear")));
2399
- return;
2400
- }
2401
- const checkout = t.closest(sel("checkout"));
2402
- if (checkout) {
2403
- e.preventDefault();
2404
- busy(checkout, () => Cart.checkout());
2405
- return;
2406
- }
2407
- const line = t.closest(dataKeySel());
2408
- if (line && line.closest(sel("cart-lines"))) {
2409
- const id = line.getAttribute(`data-${cfg.prefix}-key`);
2410
- const current = Cart.lines.find((l) => l.id === id);
2411
- const run = (fn) => {
2412
- e.preventDefault();
2413
- fn().catch((err) => reportError(err, "cart line"));
2414
- };
2415
- if (t.closest(sel("line-remove"))) return run(() => Cart.removeLine(id));
2416
- if (current && t.closest(sel("line-plus"))) return run(() => Cart.updateLine(id, current.quantity + 1));
2417
- if (current && t.closest(sel("line-minus"))) return run(() => Cart.updateLine(id, current.quantity - 1));
2418
- }
2419
- const removeCode = t.closest(sel("discount-remove"));
2420
- if (removeCode) {
2421
- e.preventDefault();
2422
- const code = (_a = removeCode.closest(sel("discount-code"))) == null ? void 0 : _a.getAttribute(attr("discount-code"));
2423
- if (code) Cart.removeDiscount(code).catch((err) => reportError(err, "discount"));
2424
- return;
2425
- }
2426
- const groupBtn = t.closest(sel("group-add"));
2427
- if (groupBtn) {
2428
- e.preventDefault();
2429
- busy(groupBtn, () => addGroup(groupBtn));
2430
- }
2431
- });
2432
- document.addEventListener("change", (e) => {
2433
- const t = e.target;
2434
- if (!(t == null ? void 0 : t.closest)) return;
2435
- const lineEl = t.closest(dataKeySel());
2436
- const id = lineEl == null ? void 0 : lineEl.getAttribute(`data-${cfg.prefix}-key`);
2437
- if (id && t.matches(`input${sel("line", "quantity")}`)) {
2438
- Cart.updateLine(id, parseInt(t.value, 10) || 0).catch((err) => reportError(err, "cart line"));
2439
- } else if (id && t.matches(`select${sel("line-plan")}`)) {
2440
- Cart.updateLine(id, { sellingPlanId: t.value || null }).catch((err) => reportError(err, "cart line plan"));
2441
- } else if (t.matches(`select${sel("country-select")}`)) {
2442
- if (t.value) setCountry(t.value).catch((err) => reportError(err, "country"));
2443
- } else if (t.matches(sel("cart-note"))) {
2444
- Cart.setNote(t.value).catch((err) => reportError(err, "cart note"));
2445
- }
2446
- });
2447
- document.addEventListener("submit", (e) => {
2448
- var _a, _b;
2449
- const form = (_b = (_a = e.target).closest) == null ? void 0 : _b.call(_a, sel("discount-form"));
2450
- if (!form) return;
2451
- e.preventDefault();
2452
- const input = form.querySelector(sel("discount-input")) || form.querySelector("input");
2453
- const code = input == null ? void 0 : input.value.trim();
2454
- if (!code) return;
2455
- busy(form, async () => {
2456
- var _a2;
2457
- try {
2458
- const cart = await Cart.applyDiscount(code);
2459
- const ok = (_a2 = ((cart == null ? void 0 : cart.discountCodes) || []).find((d) => d.code.toLowerCase() === code.toLowerCase())) == null ? void 0 : _a2.applicable;
2460
- form.classList.toggle("is-invalid", !ok);
2461
- emit(ok ? "discount-applied" : "discount-invalid", { code }, form);
2462
- if (ok && input) input.value = "";
2463
- } catch (err) {
2464
- form.classList.add("is-invalid");
2465
- reportError(err, "discount");
2466
- }
2467
- });
2468
- });
2469
- document.addEventListener("keydown", (e) => {
2470
- if (e.key === "Escape" && Cart.isOpen()) Cart.close();
2471
- });
2472
- }
2473
- function dataKeySel() {
2474
- return `[data-${cfg.prefix}-key]`;
2475
- }
2476
- async function addGroup(btn) {
2477
- const group = btn.closest(sel("group"));
2478
- if (!group) return null;
2479
- const name = get(group, "group");
2480
- const extra = toAttributes(parseJSONAttr(group, "line-attributes"));
2481
- const groupAttrs = name ? [{ key: "_group", value: name }, ...extra] : extra;
2482
- const lines = qsa(group, sel("product")).map((el) => scopes.get(el)).filter((s) => {
2483
- var _a;
2484
- return s && ((_a = s.variant) == null ? void 0 : _a.availableForSale) && s.quantity > 0;
2485
- }).map((s) => {
2486
- const l = s.getLine();
2487
- l.attributes = [...l.attributes || [], ...groupAttrs];
2488
- return l;
2489
- });
2490
- if (!lines.length) {
2491
- warn("group-add: nothing selected");
2492
- return null;
2493
- }
2494
- try {
2495
- const cart = await Cart.add(lines);
2496
- emit("group-added", { group: name, lines }, group);
2497
- return cart;
2498
- } catch (err) {
2499
- reportError(err, "group add");
2500
- return null;
2501
- }
2502
- }
2503
- function init(overrides = {}) {
2504
- var _a;
2505
- if (initPromise && !Object.keys(overrides).length) return initPromise;
2506
- configure({ ...window.TETHER_CONFIG || {}, ...overrides });
2507
- if (!isConfigured()) {
2508
- warn("TETHER_CONFIG is missing domain or token. Tether is idle.");
2509
- initPromise = Promise.resolve(null);
2510
- return initPromise;
2511
- }
2512
- const market = detectMarket(cfg.markets);
2513
- if (market.country) cfg.country = market.country;
2514
- if (market.language) cfg.language = market.language;
2515
- if (market.locale && !overrides.locale && !((_a = window.TETHER_CONFIG) == null ? void 0 : _a.locale)) cfg.locale = market.locale;
2516
- if (market.sources && Object.keys(market.sources).length) log("market", market);
2517
- bindControls();
2518
- if (cfg.observe) startObserver();
2519
- initPromise = Cart.init().then(async (cart) => {
2520
- var _a2;
2521
- const want = cfg.country ? String(cfg.country).toUpperCase() : null;
2522
- if (cart && want && ((_a2 = cart.buyerIdentity) == null ? void 0 : _a2.countryCode) && cart.buyerIdentity.countryCode !== want) {
2523
- try {
2524
- cart = await Cart.setBuyerIdentity({ countryCode: want }, { create: false });
2525
- } catch (err) {
2526
- reportError(err, "cart country");
2527
- }
2528
- }
2529
- emit("ready", { cart });
2530
- return api;
2531
- });
2532
- return initPromise;
2533
- }
2534
- function startObserver() {
2535
- if (observer || typeof MutationObserver === "undefined") return;
2536
- let t = null;
2537
- const queued = /* @__PURE__ */ new Set();
2538
- observer = new MutationObserver((muts) => {
2539
- muts.forEach((m) => m.addedNodes.forEach((n) => {
2540
- if (n.nodeType === 1) queued.add(n);
2541
- }));
2542
- clearTimeout(t);
2543
- t = setTimeout(() => {
2544
- queued.forEach((n) => {
2545
- if (n.isConnected) mount(n);
2546
- });
2547
- queued.clear();
2548
- }, 50);
2549
- });
2550
- observer.observe(document.body, { childList: true, subtree: true });
2551
- }
2552
- var api = {
2553
- VERSION,
2554
- init,
2555
- mount,
2556
- unmount,
2557
- get config() {
2558
- return cfg;
2559
- },
2560
- get ready() {
2561
- return initPromise || Promise.resolve(null);
2562
- },
2563
- query: gql,
2564
- withContext,
2565
- cart: Cart,
2566
- getProduct,
2567
- getProducts,
2568
- getRecommendations,
2569
- getScope,
2570
- addGroup,
2571
- primeProduct,
2572
- normalizeProductKey,
2573
- clearProductCache,
2574
- formatMoney,
2575
- plans: selling_plans_exports,
2576
- TetherError,
2577
- resolveProductKey,
2578
- setCountry,
2579
- detectMarket: (markets) => detectMarket(markets || cfg.markets),
2580
- getLocalization,
2581
- diagnose: (opts) => diagnose({ ...opts, version: VERSION }),
2582
- on(name, fn) {
2583
- const h = (e) => fn(e.detail, e);
2584
- document.addEventListener(`${cfg.prefix}:${name}`, h);
2585
- return () => document.removeEventListener(`${cfg.prefix}:${name}`, h);
2586
- }
2587
- };
2588
- var index_default = api;
2589
- if (typeof window !== "undefined" && typeof document !== "undefined") {
2590
- window.Tether = api;
2591
- const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false;
2592
- if (auto) {
2593
- const start = () => init().then(() => mount(document));
2594
- if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });
2595
- else start();
2596
- }
2597
- }
2598
- export {
2599
- VERSION,
2600
- addGroup,
2601
- api,
2602
- index_default as default,
2603
- getScope,
2604
- init,
2605
- mount,
2606
- setCountry,
2607
- unmount
2608
- };
1
+ export * from './esm/index.js';
2
+ export { default } from './esm/index.js';