@occluze/tether 0.3.6 → 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.
@@ -0,0 +1,3 @@
1
+ import * as part from '../diagnose.js';
2
+
3
+ window.Tether.__parts.diagnose = part;
@@ -0,0 +1,3 @@
1
+ import * as part from '../markets-ui.js';
2
+
3
+ window.Tether.__parts.markets = part;
package/src/product.js CHANGED
@@ -269,14 +269,17 @@ export class ProductScope {
269
269
  const p = this.product;
270
270
 
271
271
  this.$(sel('bind')).forEach((el) => {
272
- const key = get(el, 'bind');
272
+ const key = get(el, 'bind') || '';
273
+ const txt = {
274
+ title: p.title,
275
+ 'description-text': p.description,
276
+ vendor: p.vendor,
277
+ type: p.productType,
278
+ handle: p.handle,
279
+ }[key];
280
+ if (txt !== undefined) return setText(el, txt);
273
281
  switch (key) {
274
- case 'title': return setText(el, p.title);
275
282
  case 'description': el.innerHTML = p.descriptionHtml || ''; return undefined;
276
- case 'description-text': return setText(el, p.description);
277
- case 'vendor': return setText(el, p.vendor);
278
- case 'type': return setText(el, p.productType);
279
- case 'handle': return setText(el, p.handle);
280
283
  case 'published-at': return setText(el, formatDate(p.publishedAt));
281
284
  case 'min-price': return setText(el, formatMoney(p.priceRange.minVariantPrice));
282
285
  case 'max-price': return setText(el, formatMoney(p.priceRange.maxVariantPrice));
@@ -297,17 +300,15 @@ export class ProductScope {
297
300
  }
298
301
  default:
299
302
  // image:2 = the second product image (falls back to the featured image)
300
- if (key && /^image:\d+$/.test(key)) {
301
- const img = p.images.nodes[parseInt(key.slice(6), 10) - 1] || p.featuredImage;
303
+ if (/^image:\d+$/.test(key)) {
304
+ const img = p.images.nodes[key.slice(6) - 1] || p.featuredImage;
302
305
  return setImage(el, img?.url, img?.altText || p.title);
303
306
  }
304
- if (key && key.startsWith('metafield:')) {
305
- const mf = metafieldValue(p, key.slice('metafield:'.length));
307
+ if (key.startsWith('metafield:')) {
308
+ const mf = metafieldValue(p, key.slice(10));
306
309
  if (!mf) return setText(el, '');
307
310
  if (mf.reference?.image) return setImage(el, mf.reference.image.url, mf.reference.image.altText);
308
- if (mf.type === 'rich_text_field' || mf.type === 'multi_line_text_field') {
309
- el.textContent = mf.value; el.style.whiteSpace = 'pre-line'; return undefined;
310
- }
311
+ if (/^(rich_text|multi_line_text)_field$/.test(mf.type)) el.style.whiteSpace = 'pre-line';
311
312
  return setText(el, mf.value);
312
313
  }
313
314
  return undefined;
@@ -393,35 +394,35 @@ export class ProductScope {
393
394
  const plan = this.plan;
394
395
  const total = { amount: (parseFloat(price.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: price.currencyCode };
395
396
 
397
+ const fm = formatMoney;
398
+ const pct = (n) => (n ? `${n}%` : '');
399
+ // Text binds: name -> value (functions, so only the binds on the page are computed).
400
+ const text = {
401
+ price: () => fm(price),
402
+ 'total-price': () => fm(total),
403
+ 'one-time-price': () => fm(oneTimePrice),
404
+ 'subscription-price': () => fm(subPrice),
405
+ 'subscription-savings': () => pct(savings.percentage),
406
+ 'subscription-savings-amount': () => fm(savings.amount),
407
+ 'checkout-charge': () => fm(sub ? planCheckoutCharge(v, this.planId) : price),
408
+ 'plan-name': () => plan?.name,
409
+ 'plan-description': () => plan?.description,
410
+ 'plan-group': () => plan?.group?.name,
411
+ 'compare-at-price': () => (onSale ? fm(compareAt) : ''),
412
+ 'unit-price': () => fm(v.unitPrice),
413
+ 'discount-percentage': () => `${discountPct}%`,
414
+ 'discount-amount': () => (onSale ? fm({ amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)), currencyCode: price.currencyCode }) : ''),
415
+ 'variant-title': () => v.title,
416
+ 'variant-id': () => v.id,
417
+ sku: () => v.sku,
418
+ barcode: () => v.barcode,
419
+ stock: () => v.quantityAvailable,
420
+ quantity: () => this.quantity,
421
+ };
396
422
  this.$(sel('bind')).forEach((el) => {
397
- switch (get(el, 'bind')) {
398
- case 'price': return setText(el, formatMoney(price));
399
- case 'total-price': return setText(el, formatMoney(total));
400
- case 'one-time-price': return setText(el, formatMoney(oneTimePrice));
401
- case 'subscription-price': return setText(el, formatMoney(subPrice));
402
- case 'subscription-savings': return setText(el, savings.percentage ? `${savings.percentage}%` : '');
403
- case 'subscription-savings-amount': return setText(el, formatMoney(savings.amount));
404
- case 'checkout-charge': return setText(el, formatMoney(sub ? planCheckoutCharge(v, this.planId) : price));
405
- case 'plan-name': return setText(el, plan?.name || '');
406
- case 'plan-description': return setText(el, plan?.description || '');
407
- case 'plan-group': return setText(el, plan?.group?.name || '');
408
- case 'compare-at-price': return setText(el, onSale ? formatMoney(compareAt) : '');
409
- case 'unit-price': return setText(el, formatMoney(v.unitPrice));
410
- case 'discount-percentage': return setText(el, `${discountPct}%`);
411
- case 'discount-amount':
412
- return setText(el, onSale ? formatMoney({
413
- amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)),
414
- currencyCode: price.currencyCode,
415
- }) : '');
416
- case 'variant-title': return setText(el, v.title);
417
- case 'variant-id': return setText(el, v.id);
418
- case 'sku': return setText(el, v.sku);
419
- case 'barcode': return setText(el, v.barcode);
420
- case 'stock': return setText(el, v.quantityAvailable ?? '');
421
- case 'quantity': return setText(el, this.quantity);
422
- case 'variant-image': return setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
423
- default: return undefined;
424
- }
423
+ const k = get(el, 'bind');
424
+ if (k === 'variant-image') setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
425
+ else if (text[k]) setText(el, text[k]());
425
426
  });
426
427
 
427
428
  this.$(sel('option-selected')).forEach((el) => setText(el, this.selected[get(el, 'option-selected')] || ''));
@@ -458,17 +459,16 @@ export class ProductScope {
458
459
  qsa(node, sel('plan-bind')).forEach((el) => {
459
460
  const adj = planAdjustment(planObj);
460
461
  const s = planSavings(v, id);
461
- switch (get(el, 'plan-bind')) {
462
- case 'name': return setText(el, planObj?.name);
463
- case 'description': return setText(el, planObj?.description || '');
464
- case 'price': return setText(el, formatMoney(planPrice(v, id)));
465
- case 'savings': return setText(el, s.percentage ? `${s.percentage}%` : '');
466
- case 'savings-amount': return setText(el, formatMoney(s.amount));
467
- case 'adjustment':
468
- if (!adj) return setText(el, '');
469
- return setText(el, adj.type === 'percentage' ? `${adj.value}%` : formatMoney(adj.value));
470
- default: return undefined;
471
- }
462
+ const k = get(el, 'plan-bind');
463
+ const val = {
464
+ name: () => planObj?.name,
465
+ description: () => planObj?.description,
466
+ price: () => fm(planPrice(v, id)),
467
+ savings: () => pct(s.percentage),
468
+ 'savings-amount': () => fm(s.amount),
469
+ adjustment: () => (!adj ? '' : adj.type === 'percentage' ? `${adj.value}%` : fm(adj.value)),
470
+ }[k];
471
+ if (val) setText(el, val());
472
472
  });
473
473
  });
474
474
 
@@ -510,6 +510,7 @@ export class ProductScope {
510
510
  this.$(sel('add-to-cart')).forEach((btn) => {
511
511
  const disabled = !v.availableForSale;
512
512
  btn.classList.toggle('is-disabled', disabled);
513
+ btn.setAttribute('aria-disabled', String(disabled));
513
514
  if ('disabled' in btn) btn.disabled = disabled;
514
515
  const soldOut = get(btn, 'soldout-text');
515
516
  const subText = get(btn, 'subscribe-text');
package/src/status.js CHANGED
@@ -64,6 +64,7 @@ export function setScopeStatus(root, status, err) {
64
64
  if (status === 'error' || notFound) {
65
65
  scoped(root, sel('add-to-cart'), boundary()).forEach((btn) => {
66
66
  btn.classList.add('is-disabled');
67
+ btn.setAttribute('aria-disabled', 'true');
67
68
  if ('disabled' in btn) btn.disabled = true;
68
69
  });
69
70
  }
@@ -0,0 +1,16 @@
1
+ import type { TetherConfig } from './index.js';
2
+
3
+ export interface BarbaPresetOptions {
4
+ gsap?: () => unknown;
5
+ lenis?: (() => { resize?(): void; stop?(): void; start?(): void } | null | undefined) | null;
6
+ scrollTrigger?: () => unknown;
7
+ lockScroll?: ((drawer: HTMLElement) => void) | null;
8
+ unlockScroll?: (() => void) | null;
9
+ onResize?: (() => void) | null;
10
+ onValueChange?: ((el: HTMLElement, previousText: string) => void) | null;
11
+ animateLines?: boolean;
12
+ lineDuration?: number;
13
+ }
14
+
15
+ /** Hooks for sites with page transitions, smooth scroll and GSAP: spread into Tether.init(). */
16
+ export declare function barbaPreset(options?: BarbaPresetOptions): Partial<TetherConfig>;
@@ -0,0 +1,332 @@
1
+ // Type definitions for @occluze/tether
2
+ // Shopify Storefront API objects are typed loosely (`ShopifyProduct` etc.): Tether passes them
3
+ // through as Shopify returns them. See https://shopify.dev/docs/api/storefront for the full shape.
4
+
5
+ export interface Money { amount: string; currencyCode: string }
6
+ export interface ShopifyImage { url: string; altText?: string | null; width?: number; height?: number }
7
+
8
+ export interface ShopifySellingPlan {
9
+ id: string;
10
+ name: string;
11
+ description?: string | null;
12
+ recurringDeliveries?: boolean;
13
+ options: { name: string; value: string }[];
14
+ priceAdjustments: unknown[];
15
+ /** Added by Tether: the plan's group. */
16
+ group?: { name: string; [k: string]: unknown };
17
+ }
18
+
19
+ export interface ShopifyVariant {
20
+ id: string;
21
+ title: string;
22
+ availableForSale: boolean;
23
+ currentlyNotInStock?: boolean;
24
+ quantityAvailable?: number | null;
25
+ sku?: string | null;
26
+ barcode?: string | null;
27
+ selectedOptions: { name: string; value: string }[];
28
+ image?: ShopifyImage | null;
29
+ price: Money;
30
+ compareAtPrice?: Money | null;
31
+ unitPrice?: Money | null;
32
+ sellingPlanAllocations?: { nodes: unknown[] };
33
+ [k: string]: unknown;
34
+ }
35
+
36
+ export interface ShopifyProduct {
37
+ id: string;
38
+ handle: string;
39
+ title: string;
40
+ description: string;
41
+ descriptionHtml: string;
42
+ productType?: string;
43
+ vendor?: string;
44
+ tags: string[];
45
+ availableForSale: boolean;
46
+ requiresSellingPlan?: boolean;
47
+ featuredImage?: ShopifyImage | null;
48
+ images: { nodes: ShopifyImage[] };
49
+ priceRange: { minVariantPrice: Money; maxVariantPrice: Money };
50
+ options: { name: string; optionValues: { name: string; swatch?: unknown }[] }[];
51
+ variants: { nodes: ShopifyVariant[] };
52
+ sellingPlanGroups?: { nodes: unknown[] };
53
+ metafields?: ({ namespace: string; key: string; type: string; value: string; reference?: unknown } | null)[];
54
+ [k: string]: unknown;
55
+ }
56
+
57
+ export interface ShopifyCartLine {
58
+ id: string;
59
+ quantity: number;
60
+ merchandise: ShopifyVariant & { product: Pick<ShopifyProduct, 'id' | 'handle' | 'title'> & Record<string, unknown> };
61
+ sellingPlanAllocation?: { sellingPlan: { id: string; name: string } } | null;
62
+ attributes?: { key: string; value: string }[];
63
+ cost: Record<string, Money | null | undefined>;
64
+ [k: string]: unknown;
65
+ }
66
+
67
+ export interface ShopifyCart {
68
+ id: string;
69
+ checkoutUrl: string;
70
+ totalQuantity: number;
71
+ note?: string | null;
72
+ lines: { nodes: ShopifyCartLine[] };
73
+ cost: Record<string, Money | null | undefined>;
74
+ discountCodes: { code: string; applicable: boolean }[];
75
+ attributes: { key: string; value: string }[];
76
+ buyerIdentity?: { countryCode?: string | null; email?: string | null };
77
+ [k: string]: unknown;
78
+ }
79
+
80
+ export interface MarketsConfig {
81
+ /** true: saved choice, server hints, Webflow Localization. Or a list of those sources. false: off. */
82
+ detect?: boolean | ('saved' | 'server' | 'webflow')[];
83
+ /** Webflow locale (subdirectory or html lang) -> market. */
84
+ locales?: Record<string, { country?: string; language?: string; locale?: string }>;
85
+ /** Cookie with the visitor's country, set by your server (default "tether_country"). */
86
+ cookie?: string;
87
+ /** Remember the visitor's country choice (default true). */
88
+ remember?: boolean;
89
+ /** With locales: ignore an html lang that isn't in the map. */
90
+ strict?: boolean;
91
+ }
92
+
93
+ export interface TetherConfig {
94
+ /** your-store.myshopify.com */
95
+ domain: string;
96
+ /** Public Storefront API access token (never an Admin token). */
97
+ token: string;
98
+ /** Pinned Shopify API version, e.g. "2026-07". Never "unstable". */
99
+ apiVersion?: string;
100
+ /** ISO country code for prices and currency, e.g. "NL". */
101
+ country?: string;
102
+ /** Language for translated content, e.g. "NL". */
103
+ language?: string;
104
+ /** Intl locale for money and dates, e.g. "nl-NL". */
105
+ locale?: string;
106
+ markets?: MarketsConfig | null;
107
+ /** Attribute prefix (default "tether"). */
108
+ prefix?: string;
109
+ /** Product URL template: "/product" or "/product?handle={handle}" ({handle}, {id}). */
110
+ productPath?: string;
111
+ collectionPath?: string;
112
+ /** Metafields to fetch: "namespace.key" or { namespace, key }. */
113
+ metafields?: (string | { namespace: string; key: string })[];
114
+ imagesFirst?: number;
115
+ variantsFirst?: number;
116
+ sellingPlanGroupsFirst?: number;
117
+ sellingPlansFirst?: number;
118
+ collectionFirst?: number;
119
+ retries?: number;
120
+ storageKey?: string | null;
121
+ cartDrawerOpenClass?: string;
122
+ openCartOnAdd?: boolean;
123
+ /** Move focus into the open cart, keep Tab inside it, restore it on close (default true). */
124
+ cartFocus?: boolean;
125
+ checkoutTarget?: '_self' | '_blank';
126
+ defaultPurchase?: 'one-time' | 'subscription';
127
+ /** Mount the document on DOMContentLoaded (default true). False with page transitions. */
128
+ autoMount?: boolean;
129
+ /** Mount elements added later (CMS load more, filters). */
130
+ observe?: boolean;
131
+ /** Folder of the optional parts (tether.markets.min.js, tether.diagnose.min.js) for the <script> build. */
132
+ partsUrl?: string | null;
133
+ formatMoney?: ((money: Money, locale?: string) => string) | null;
134
+ onLayoutChange?: ((reason: string) => void) | null;
135
+ onCartOpen?: ((drawers: HTMLElement[]) => void) | null;
136
+ onCartClose?: ((drawers: HTMLElement[]) => void) | null;
137
+ onLineEnter?: ((node: HTMLElement) => void) | null;
138
+ onLineLeave?: ((node: HTMLElement, done: () => void) => void) | null;
139
+ onValueChange?: ((el: HTMLElement, previousText: string) => void) | null;
140
+ /** Every failure that reaches the visitor. Send it to Sentry or similar. */
141
+ onError?: ((error: TetherError | Error, context: string) => void) | null;
142
+ debug?: boolean;
143
+ }
144
+
145
+ export type TetherErrorCode = 'NOT_FOUND' | 'NETWORK' | 'NOT_CONFIGURED' | 'INVALID_COUNTRY' | 'ERROR' | `HTTP_${number}`;
146
+
147
+ export declare class TetherError extends Error {
148
+ name: 'TetherError';
149
+ code: TetherErrorCode | string;
150
+ status?: number;
151
+ errors?: { message: string; extensions?: Record<string, unknown> }[];
152
+ userErrors?: { field?: string[]; message: string }[];
153
+ constructor(message: string, opts?: { status?: number; errors?: unknown[]; userErrors?: unknown[]; code?: string });
154
+ }
155
+
156
+ export type Attributes = Record<string, string | number | boolean> | { key: string; value: string }[];
157
+
158
+ export interface LineInput {
159
+ variantId?: string;
160
+ merchandiseId?: string;
161
+ quantity?: number;
162
+ sellingPlanId?: string | null;
163
+ attributes?: Attributes;
164
+ }
165
+
166
+ export interface Cart {
167
+ readonly state: ShopifyCart | null;
168
+ readonly lines: ShopifyCartLine[];
169
+ readonly count: number;
170
+ lastError: Error | null;
171
+ id(): string | null;
172
+ init(): Promise<ShopifyCart | null>;
173
+ /** add(variantId, qty) · add({ variantId, quantity, sellingPlanId, attributes }) · add([line, line]) */
174
+ add(input: string | LineInput | LineInput[], quantity?: number, opts?: Omit<LineInput, 'variantId' | 'quantity'>): Promise<ShopifyCart>;
175
+ /** updateLine(id, 3) · updateLine(id, { sellingPlanId: null }) switches to one-time. */
176
+ updateLine(lineId: string, patch: number | (LineInput & { quantity?: number })): Promise<ShopifyCart>;
177
+ removeLine(lineIds: string | string[]): Promise<ShopifyCart>;
178
+ clear(): Promise<ShopifyCart | null>;
179
+ setNote(note: string): Promise<ShopifyCart>;
180
+ setAttributes(attributes: Attributes): Promise<ShopifyCart>;
181
+ setDiscountCodes(codes: string[]): Promise<ShopifyCart>;
182
+ applyDiscount(code: string): Promise<ShopifyCart>;
183
+ removeDiscount(code: string): Promise<ShopifyCart>;
184
+ setBuyerIdentity(identity: { countryCode?: string; email?: string; phone?: string }, opts?: { create?: boolean }): Promise<ShopifyCart | null>;
185
+ checkout(target?: '_self' | '_blank'): Promise<void>;
186
+ open(): void;
187
+ close(): void;
188
+ toggle(): void;
189
+ isOpen(): boolean;
190
+ drawers(): HTMLElement[];
191
+ render(): void;
192
+ }
193
+
194
+ export interface ProductScope {
195
+ readonly root: HTMLElement;
196
+ product: ShopifyProduct;
197
+ variant: ShopifyVariant | null;
198
+ selected: Record<string, string>;
199
+ quantity: number;
200
+ plans: ShopifySellingPlan[];
201
+ purchase: 'one-time' | 'subscription';
202
+ planId: string | null;
203
+ readonly plan: ShopifySellingPlan | null;
204
+ readonly isSubscription: boolean;
205
+ readonly subscriptionAvailable: boolean;
206
+ select(option: string, value: string): void;
207
+ selectVariant(id: string): void;
208
+ setPurchase(mode: 'one-time' | 'subscription'): void;
209
+ selectPlan(ref: string): void;
210
+ setQuantity(n: number | string): void;
211
+ getLine(): LineInput | null;
212
+ addToCart(): Promise<ShopifyCart | null>;
213
+ update(product: ShopifyProduct): void;
214
+ }
215
+
216
+ export interface SellingPlanHelpers {
217
+ allPlans(product: ShopifyProduct): ShopifySellingPlan[];
218
+ hasPlans(product: ShopifyProduct): boolean;
219
+ allocationFor(variant: ShopifyVariant, planId: string | null): unknown | null;
220
+ plansForVariant(product: ShopifyProduct, variant: ShopifyVariant): ShopifySellingPlan[];
221
+ planPrice(variant: ShopifyVariant, planId: string): Money | null;
222
+ planCheckoutCharge(variant: ShopifyVariant, planId: string): Money | null;
223
+ planSavings(variant: ShopifyVariant, planId: string): { amount: Money | null; percentage: number };
224
+ planAdjustment(plan: ShopifySellingPlan | null | undefined): { type: 'percentage' | 'amount' | 'price'; value: number | Money } | null;
225
+ findPlan(product: ShopifyProduct, ref: string | null | undefined): ShopifySellingPlan | null;
226
+ toGid(value: string | number, type: string): string | null;
227
+ GID: { product(v: string | number): string | null; variant(v: string | number): string | null; sellingPlan(v: string | number): string | null };
228
+ }
229
+
230
+ export interface ProductPage {
231
+ collection: Record<string, unknown> | null;
232
+ products: ShopifyProduct[];
233
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
234
+ }
235
+
236
+ export interface Localization {
237
+ country?: { isoCode: string; name: string; currency?: { isoCode: string; symbol: string } };
238
+ language?: { isoCode: string; name: string };
239
+ availableCountries: { isoCode: string; name: string; currency?: { isoCode: string; symbol: string }; availableLanguages?: { isoCode: string; name: string }[] }[];
240
+ }
241
+
242
+ export interface DiagnoseReport {
243
+ version: string;
244
+ config: Record<string, unknown>;
245
+ apiVersion: { version: string; ok: boolean; monthsLeft?: number; next?: string; note: string };
246
+ market: { country?: string; language?: string; locale?: string; sources: Record<string, string> } | null;
247
+ reachable: boolean;
248
+ shop: { name?: string; url?: string; currency?: string } | null;
249
+ latencyMs: number | null;
250
+ products: { key: string; handle?: string; title?: string; plans?: number; error?: string }[];
251
+ sellingPlans: { product: string; group?: string; plan: string; id: string }[];
252
+ problems: { level: 'error' | 'warn'; message: string; element?: string }[];
253
+ }
254
+
255
+ /** Events on document, as `tether:<name>` (or your prefix). Detail types for Tether.on(). */
256
+ export interface TetherEvents {
257
+ ready: { cart: ShopifyCart | null };
258
+ 'product-ready': { product: ShopifyProduct; scope: ProductScope };
259
+ 'product-updated': { product: ShopifyProduct; scope: ProductScope };
260
+ 'product-not-found': { key: string | null; source: string };
261
+ 'variant-changed': { product: ShopifyProduct; variant: ShopifyVariant; selected: Record<string, string>; purchase: 'one-time' | 'subscription'; sellingPlanId: string | null };
262
+ 'product-added': { product: ShopifyProduct; variant: ShopifyVariant; line: LineInput };
263
+ added: { cart: ShopifyCart; lines: LineInput[] };
264
+ 'group-added': { group: string | null; lines: LineInput[] };
265
+ 'cart-updated': { cart: ShopifyCart | null; reason: string };
266
+ 'cart-toggled': { open: boolean };
267
+ 'cart-error': { error: Error };
268
+ checkout: { url: string; cart: ShopifyCart };
269
+ 'list-ready': { products: ShopifyProduct[]; collection: Record<string, unknown> | null };
270
+ 'country-changed': { country: string; language?: string };
271
+ 'discount-applied': { code: string };
272
+ 'discount-invalid': { code: string };
273
+ 'layout-changed': { reason: string };
274
+ error: { error: Error; context: string; key?: string };
275
+ }
276
+
277
+ export interface Tether {
278
+ readonly VERSION: string;
279
+ /** Configure (window.TETHER_CONFIG + overrides) and load the cart. */
280
+ init(overrides?: Partial<TetherConfig>): Promise<Tether | null>;
281
+ /** Mount product scopes and lists inside root (call per page container with Barba/Swup). */
282
+ mount(root?: ParentNode): number;
283
+ /** Call before a page transition removes the page (closes the cart drawer). */
284
+ unmount(): void;
285
+ readonly config: TetherConfig;
286
+ readonly ready: Promise<Tether | null>;
287
+ /** Any Storefront API query, with @inContext added for the current market. */
288
+ query<T = any>(query: string, variables?: Record<string, unknown>, opts?: { context?: boolean }): Promise<T>;
289
+ withContext(query: string, ctx?: { country?: string; language?: string }): string;
290
+ cart: Cart;
291
+ getProduct(handleOrId: string): Promise<ShopifyProduct>;
292
+ getProducts(opts?: { collection?: string; query?: string; first?: number; after?: string | null; sortKey?: string; reverse?: boolean; filters?: unknown[] }): Promise<ProductPage>;
293
+ getRecommendations(productId: string, intent?: 'RELATED' | 'COMPLEMENTARY'): Promise<ShopifyProduct[]>;
294
+ getScope(el: Element | null | undefined): ProductScope | null;
295
+ addGroup(button: Element): Promise<ShopifyCart | null>;
296
+ primeProduct(product: ShopifyProduct): void;
297
+ normalizeProductKey(raw: string | number): string | null;
298
+ clearProductCache(): void;
299
+ formatMoney(money: Money | null | undefined): string;
300
+ plans: SellingPlanHelpers;
301
+ TetherError: typeof TetherError;
302
+ resolveProductKey(el: Element, location?: Location | URL): { key: string | null; fromUrl: boolean; raw: string };
303
+ /** Switch market: prices, currency, cart and every product on the page follow. */
304
+ setCountry(code: string, opts?: { language?: string; remember?: boolean }): Promise<ShopifyCart | null>;
305
+ detectMarket(markets?: MarketsConfig): Promise<{ country?: string; language?: string; locale?: string; sources: Record<string, string> }>;
306
+ getLocalization(): Promise<Localization>;
307
+ /** Logs and returns what's wrong on this page (config, token, API version, plans, markup). */
308
+ diagnose(opts?: { root?: ParentNode; silent?: boolean }): Promise<DiagnoseReport>;
309
+ /** Listen to a Tether event; returns a function that stops listening. */
310
+ on<K extends keyof TetherEvents>(name: K, fn: (detail: TetherEvents[K], event: CustomEvent<TetherEvents[K]>) => void): () => void;
311
+ on(name: string, fn: (detail: any, event: CustomEvent) => void): () => void;
312
+ }
313
+
314
+ export declare const VERSION: string;
315
+ export declare function init(overrides?: Partial<TetherConfig>): Promise<Tether | null>;
316
+ export declare function mount(root?: ParentNode): number;
317
+ export declare function unmount(): void;
318
+ export declare function getScope(el: Element | null | undefined): ProductScope | null;
319
+ export declare function addGroup(button: Element): Promise<ShopifyCart | null>;
320
+ export declare function setCountry(code: string, opts?: { language?: string; remember?: boolean }): Promise<ShopifyCart | null>;
321
+ export declare const api: Tether;
322
+ declare const tether: Tether;
323
+ export default tether;
324
+
325
+ declare global {
326
+ interface Window {
327
+ Tether: Tether;
328
+ TETHER_CONFIG?: Partial<TetherConfig>;
329
+ TETHER_COUNTRY?: string;
330
+ TETHER_LANGUAGE?: string;
331
+ }
332
+ }