@occluze/tether 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +12 -7
  2. package/dist/esm/astro.js +2 -0
  3. package/dist/esm/chunks/chunk-HHMOPDZG.js +1 -0
  4. package/dist/esm/chunks/chunk-HXKV6QGB.js +4 -0
  5. package/dist/esm/chunks/chunk-RBVRRQIU.js +1 -0
  6. package/dist/esm/chunks/chunk-RNZMTQTX.js +1 -0
  7. package/dist/esm/chunks/chunk-VC46IEJQ.js +1 -0
  8. package/dist/esm/chunks/diagnose-J667GHGS.js +1 -0
  9. package/dist/esm/chunks/markets-ui-EGWIEUOZ.js +1 -0
  10. package/dist/esm/index.js +1 -0
  11. package/dist/esm/server.js +1 -0
  12. package/dist/presets/barba.esm.js +4 -5
  13. package/dist/tether.diagnose.js +320 -0
  14. package/dist/tether.diagnose.min.js +1 -0
  15. package/dist/tether.esm.js +2 -2608
  16. package/dist/tether.js +637 -833
  17. package/dist/tether.markets.js +213 -0
  18. package/dist/tether.markets.min.js +1 -0
  19. package/dist/tether.min.js +3 -132
  20. package/dist/tether.min.js.map +4 -4
  21. package/package.json +52 -11
  22. package/src/astro.js +73 -0
  23. package/src/cart.js +79 -7
  24. package/src/client.js +25 -15
  25. package/src/config.js +4 -0
  26. package/src/diagnose.js +1 -1
  27. package/src/dom.js +32 -9
  28. package/src/fragments.js +12 -11
  29. package/src/index.js +80 -160
  30. package/src/initial.js +66 -0
  31. package/src/lazy-iife.js +43 -0
  32. package/src/lazy.js +9 -0
  33. package/src/loader.js +50 -7
  34. package/src/markets-ui.js +89 -0
  35. package/src/markets.js +1 -1
  36. package/src/mounting.js +129 -0
  37. package/src/parts/diagnose.js +3 -0
  38. package/src/parts/markets.js +3 -0
  39. package/src/product.js +86 -61
  40. package/src/server.js +179 -0
  41. package/src/status.js +2 -6
  42. package/types/astro.d.ts +15 -0
  43. package/types/barba.d.ts +16 -0
  44. package/types/index.d.ts +347 -0
  45. package/types/server.d.ts +94 -0
@@ -0,0 +1,347 @@
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: { totalAmount: Money; subtotalAmount: Money; amountPerQuantity: Money; compareAtAmountPerQuantity?: Money | null };
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
+ /** Products handed over by the server (tether/server hydrate()): refresh once in the background (default true). */
121
+ revalidate?: boolean;
122
+ /** Mount on Astro View Transitions (astro:page-load) and close the cart on navigation (default true). */
123
+ astro?: boolean;
124
+ storageKey?: string | null;
125
+ cartDrawerOpenClass?: string;
126
+ openCartOnAdd?: boolean;
127
+ /** Move focus into the open cart, keep Tab inside it, restore it on close (default true). */
128
+ cartFocus?: boolean;
129
+ checkoutTarget?: '_self' | '_blank';
130
+ defaultPurchase?: 'one-time' | 'subscription';
131
+ /** Mount the document on DOMContentLoaded (default true). False with page transitions. */
132
+ autoMount?: boolean;
133
+ /** Mount elements added later (CMS load more, filters). */
134
+ observe?: boolean;
135
+ /** Folder of the optional parts (tether.markets.min.js, tether.diagnose.min.js) for the <script> build. */
136
+ partsUrl?: string | null;
137
+ formatMoney?: ((money: Money, locale?: string) => string) | null;
138
+ onLayoutChange?: ((reason: string) => void) | null;
139
+ onCartOpen?: ((drawers: HTMLElement[]) => void) | null;
140
+ onCartClose?: ((drawers: HTMLElement[]) => void) | null;
141
+ onLineEnter?: ((node: HTMLElement) => void) | null;
142
+ onLineLeave?: ((node: HTMLElement, done: () => void) => void) | null;
143
+ onValueChange?: ((el: HTMLElement, previousText: string) => void) | null;
144
+ /** Every failure that reaches the visitor. Send it to Sentry or similar. */
145
+ onError?: ((error: TetherError | Error, context: string) => void) | null;
146
+ debug?: boolean;
147
+ }
148
+
149
+ export type TetherErrorCode = 'NOT_FOUND' | 'NETWORK' | 'NOT_CONFIGURED' | 'INVALID_COUNTRY' | 'ERROR' | `HTTP_${number}`;
150
+
151
+ export declare class TetherError extends Error {
152
+ name: 'TetherError';
153
+ code: TetherErrorCode | string;
154
+ status?: number;
155
+ errors?: { message: string; extensions?: Record<string, unknown> }[];
156
+ userErrors?: { field?: string[]; message: string }[];
157
+ constructor(message: string, opts?: { status?: number; errors?: unknown[]; userErrors?: unknown[]; code?: string });
158
+ }
159
+
160
+ export type Attributes = Record<string, string | number | boolean> | { key: string; value: string }[];
161
+
162
+ export interface LineInput {
163
+ variantId?: string;
164
+ merchandiseId?: string;
165
+ quantity?: number;
166
+ sellingPlanId?: string | null;
167
+ attributes?: Attributes;
168
+ }
169
+
170
+ export interface CartWarning { code: string; message: string }
171
+
172
+ export interface Cart {
173
+ readonly state: ShopifyCart | null;
174
+ readonly lines: ShopifyCartLine[];
175
+ readonly count: number;
176
+ lastError: Error | null;
177
+ /** Shopify's notes on the last change, e.g. less stock than asked (code MERCHANDISE_NOT_ENOUGH_STOCK). */
178
+ readonly warnings: CartWarning[];
179
+ /** true once the saved cart (or the absence of one) is known. */
180
+ readonly loaded: boolean;
181
+ id(): string | null;
182
+ init(): Promise<ShopifyCart | null>;
183
+ /** add(variantId, qty) · add({ variantId, quantity, sellingPlanId, attributes }) · add([line, line]) */
184
+ add(input: string | LineInput | LineInput[], quantity?: number, opts?: Omit<LineInput, 'variantId' | 'quantity'>): Promise<ShopifyCart>;
185
+ /** updateLine(id, 3) · updateLine(id, { sellingPlanId: null }) switches to one-time. */
186
+ updateLine(lineId: string, patch: number | (LineInput & { quantity?: number })): Promise<ShopifyCart>;
187
+ removeLine(lineIds: string | string[]): Promise<ShopifyCart>;
188
+ clear(): Promise<ShopifyCart | null>;
189
+ setNote(note: string): Promise<ShopifyCart>;
190
+ setAttributes(attributes: Attributes): Promise<ShopifyCart>;
191
+ setDiscountCodes(codes: string[]): Promise<ShopifyCart>;
192
+ applyDiscount(code: string): Promise<ShopifyCart>;
193
+ removeDiscount(code: string): Promise<ShopifyCart>;
194
+ setBuyerIdentity(identity: { countryCode?: string; email?: string; phone?: string }, opts?: { create?: boolean }): Promise<ShopifyCart | null>;
195
+ checkout(target?: '_self' | '_blank'): Promise<void>;
196
+ open(): void;
197
+ close(): void;
198
+ toggle(): void;
199
+ isOpen(): boolean;
200
+ drawers(): HTMLElement[];
201
+ render(): void;
202
+ /** For framework islands: fn(cart, meta) now, when the saved cart has loaded and after every change. Returns unsubscribe. */
203
+ subscribe(fn: (cart: ShopifyCart | null, meta: { open: boolean; count: number; loaded: boolean; warnings: CartWarning[] }) => void): () => void;
204
+ }
205
+
206
+ export interface ProductScope {
207
+ readonly root: HTMLElement;
208
+ product: ShopifyProduct;
209
+ variant: ShopifyVariant | null;
210
+ selected: Record<string, string>;
211
+ quantity: number;
212
+ plans: ShopifySellingPlan[];
213
+ purchase: 'one-time' | 'subscription';
214
+ planId: string | null;
215
+ readonly plan: ShopifySellingPlan | null;
216
+ readonly isSubscription: boolean;
217
+ readonly subscriptionAvailable: boolean;
218
+ select(option: string, value: string): void;
219
+ selectVariant(id: string): void;
220
+ setPurchase(mode: 'one-time' | 'subscription'): void;
221
+ selectPlan(ref: string): void;
222
+ setQuantity(n: number | string): void;
223
+ getLine(): LineInput | null;
224
+ addToCart(): Promise<ShopifyCart | null>;
225
+ update(product: ShopifyProduct): void;
226
+ }
227
+
228
+ export interface SellingPlanHelpers {
229
+ allPlans(product: ShopifyProduct): ShopifySellingPlan[];
230
+ hasPlans(product: ShopifyProduct): boolean;
231
+ allocationFor(variant: ShopifyVariant, planId: string | null): unknown | null;
232
+ plansForVariant(product: ShopifyProduct, variant: ShopifyVariant): ShopifySellingPlan[];
233
+ planPrice(variant: ShopifyVariant, planId: string): Money | null;
234
+ planCheckoutCharge(variant: ShopifyVariant, planId: string): Money | null;
235
+ planSavings(variant: ShopifyVariant, planId: string): { amount: Money | null; percentage: number };
236
+ planAdjustment(plan: ShopifySellingPlan | null | undefined): { type: 'percentage' | 'amount' | 'price'; value: number | Money } | null;
237
+ findPlan(product: ShopifyProduct, ref: string | null | undefined): ShopifySellingPlan | null;
238
+ toGid(value: string | number, type: string): string | null;
239
+ GID: { product(v: string | number): string | null; variant(v: string | number): string | null; sellingPlan(v: string | number): string | null };
240
+ }
241
+
242
+ export interface ProductPage {
243
+ collection: Record<string, unknown> | null;
244
+ products: ShopifyProduct[];
245
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
246
+ }
247
+
248
+ export interface Localization {
249
+ country?: { isoCode: string; name: string; currency?: { isoCode: string; symbol: string } };
250
+ language?: { isoCode: string; name: string };
251
+ availableCountries: { isoCode: string; name: string; currency?: { isoCode: string; symbol: string }; availableLanguages?: { isoCode: string; name: string }[] }[];
252
+ }
253
+
254
+ export interface DiagnoseReport {
255
+ version: string;
256
+ config: Record<string, unknown>;
257
+ apiVersion: { version: string; ok: boolean; monthsLeft?: number; next?: string; note: string };
258
+ market: { country?: string; language?: string; locale?: string; sources: Record<string, string> } | null;
259
+ reachable: boolean;
260
+ shop: { name?: string; url?: string; currency?: string } | null;
261
+ latencyMs: number | null;
262
+ products: { key: string; handle?: string; title?: string; plans?: number; error?: string }[];
263
+ sellingPlans: { product: string; group?: string; plan: string; id: string }[];
264
+ problems: { level: 'error' | 'warn'; message: string; element?: string }[];
265
+ }
266
+
267
+ /** Events on document, as `tether:<name>` (or your prefix). Detail types for Tether.on(). */
268
+ export interface TetherEvents {
269
+ ready: { cart: ShopifyCart | null };
270
+ 'product-ready': { product: ShopifyProduct; scope: ProductScope };
271
+ 'product-updated': { product: ShopifyProduct; scope: ProductScope };
272
+ 'product-not-found': { key: string | null; source: string };
273
+ 'variant-changed': { product: ShopifyProduct; variant: ShopifyVariant; selected: Record<string, string>; purchase: 'one-time' | 'subscription'; sellingPlanId: string | null };
274
+ 'product-added': { product: ShopifyProduct; variant: ShopifyVariant; line: LineInput };
275
+ added: { cart: ShopifyCart; lines: LineInput[] };
276
+ 'group-added': { group: string | null; lines: LineInput[] };
277
+ 'cart-warning': { warnings: CartWarning[] };
278
+ 'cart-updated': { cart: ShopifyCart | null; reason: string };
279
+ 'cart-toggled': { open: boolean };
280
+ 'cart-error': { error: Error };
281
+ checkout: { url: string; cart: ShopifyCart };
282
+ 'list-ready': { products: ShopifyProduct[]; collection: Record<string, unknown> | null };
283
+ 'country-changed': { country: string; language?: string };
284
+ 'discount-applied': { code: string };
285
+ 'discount-invalid': { code: string };
286
+ 'layout-changed': { reason: string };
287
+ error: { error: Error; context: string; key?: string };
288
+ }
289
+
290
+ export interface Tether {
291
+ readonly VERSION: string;
292
+ /** Configure (window.TETHER_CONFIG + overrides) and load the cart. */
293
+ init(overrides?: Partial<TetherConfig>): Promise<Tether | null>;
294
+ /** Mount product scopes and lists inside root (call per page container with Barba/Swup). */
295
+ mount(root?: ParentNode): number;
296
+ /** Call before a page transition removes the page (closes the cart drawer). */
297
+ unmount(): void;
298
+ readonly config: TetherConfig;
299
+ readonly ready: Promise<Tether | null>;
300
+ /** Any Storefront API query, with @inContext added for the current market. */
301
+ query<T = any>(query: string, variables?: Record<string, unknown>, opts?: { context?: boolean }): Promise<T>;
302
+ withContext(query: string, ctx?: { country?: string; language?: string }): string;
303
+ cart: Cart;
304
+ getProduct(handleOrId: string): Promise<ShopifyProduct>;
305
+ getProducts(opts?: { collection?: string; query?: string; first?: number; after?: string | null; sortKey?: string; reverse?: boolean; filters?: unknown[] }): Promise<ProductPage>;
306
+ getRecommendations(product: string | Pick<ShopifyProduct, 'id'>, intent?: 'RELATED' | 'COMPLEMENTARY'): Promise<ShopifyProduct[]>;
307
+ getScope(el: Element | null | undefined): ProductScope | null;
308
+ addGroup(button: Element): Promise<ShopifyCart | null>;
309
+ primeProduct(product: ShopifyProduct): void;
310
+ normalizeProductKey(raw: string | number): string | null;
311
+ clearProductCache(): void;
312
+ formatMoney(money: Money | null | undefined): string;
313
+ /** Shopify CDN image at a size: imageUrl(line.merchandise.image?.url, { width: 160 }). */
314
+ imageUrl(url: string | null | undefined, size?: { width?: number; height?: number; crop?: string }): string;
315
+ plans: SellingPlanHelpers;
316
+ TetherError: typeof TetherError;
317
+ resolveProductKey(el: Element, location?: Location | URL): { key: string | null; fromUrl: boolean; raw: string };
318
+ /** Switch market: prices, currency, cart and every product on the page follow. */
319
+ setCountry(code: string, opts?: { language?: string; remember?: boolean }): Promise<ShopifyCart | null>;
320
+ detectMarket(markets?: MarketsConfig): Promise<{ country?: string; language?: string; locale?: string; sources: Record<string, string> }>;
321
+ getLocalization(): Promise<Localization>;
322
+ /** Logs and returns what's wrong on this page (config, token, API version, plans, markup). */
323
+ diagnose(opts?: { root?: ParentNode; silent?: boolean }): Promise<DiagnoseReport>;
324
+ /** Listen to a Tether event; returns a function that stops listening. */
325
+ on<K extends keyof TetherEvents>(name: K, fn: (detail: TetherEvents[K], event: CustomEvent<TetherEvents[K]>) => void): () => void;
326
+ on(name: string, fn: (detail: any, event: CustomEvent) => void): () => void;
327
+ }
328
+
329
+ export declare const VERSION: string;
330
+ export declare function init(overrides?: Partial<TetherConfig>): Promise<Tether | null>;
331
+ export declare function mount(root?: ParentNode): number;
332
+ export declare function unmount(): void;
333
+ export declare function getScope(el: Element | null | undefined): ProductScope | null;
334
+ export declare function addGroup(button: Element): Promise<ShopifyCart | null>;
335
+ export declare function setCountry(code: string, opts?: { language?: string; remember?: boolean }): Promise<ShopifyCart | null>;
336
+ export declare const api: Tether;
337
+ declare const tether: Tether;
338
+ export default tether;
339
+
340
+ declare global {
341
+ interface Window {
342
+ Tether: Tether;
343
+ TETHER_CONFIG?: Partial<TetherConfig>;
344
+ TETHER_COUNTRY?: string;
345
+ TETHER_LANGUAGE?: string;
346
+ }
347
+ }
@@ -0,0 +1,94 @@
1
+ import type { ShopifyProduct, ShopifyVariant, Money, TetherError, SellingPlanHelpers, TetherConfig } from './index.js';
2
+ export type { ShopifyProduct, ShopifyVariant, ShopifyCart, ShopifyCartLine, Money } from './index.js';
3
+
4
+ export interface StorefrontOptions extends Partial<Pick<TetherConfig,
5
+ 'apiVersion' | 'country' | 'language' | 'locale' | 'productPath' | 'metafields' | 'imagesFirst' | 'variantsFirst' | 'sellingPlanGroupsFirst' | 'sellingPlansFirst' | 'retries'>> {
6
+ /** your-store.myshopify.com (or pass import.meta.env with PUBLIC_SHOPIFY_DOMAIN). */
7
+ domain?: string;
8
+ /** Public Storefront API token (or PUBLIC_SHOPIFY_STOREFRONT_TOKEN in import.meta.env). */
9
+ token?: string;
10
+ /** Products per request for allProducts() (default 25). */
11
+ pageSize?: number;
12
+ /** Custom fetch (tests, caching). */
13
+ fetch?: typeof fetch;
14
+ PUBLIC_SHOPIFY_DOMAIN?: string;
15
+ PUBLIC_SHOPIFY_STOREFRONT_TOKEN?: string;
16
+ [env: string]: unknown;
17
+ }
18
+
19
+ export interface ShopifyCollection {
20
+ id: string; handle: string; title: string; description: string; descriptionHtml: string;
21
+ image?: { url: string; altText?: string | null } | null;
22
+ seo?: { title?: string | null; description?: string | null };
23
+ }
24
+
25
+ export interface ProductQuery {
26
+ collection?: string;
27
+ query?: string;
28
+ first?: number;
29
+ after?: string | null;
30
+ sortKey?: string;
31
+ reverse?: boolean;
32
+ filters?: unknown[];
33
+ }
34
+
35
+ export interface Storefront {
36
+ readonly config: Required<Pick<StorefrontOptions, 'domain' | 'token'>> & StorefrontOptions;
37
+ query<T = any>(query: string, variables?: Record<string, unknown>, opts?: { context?: boolean }): Promise<T>;
38
+ /** By handle, numeric id, gid or product URL. null when it doesn't exist. */
39
+ getProduct(handleOrId: string | number): Promise<ShopifyProduct | null>;
40
+ getProducts(opts?: ProductQuery): Promise<{ collection: ShopifyCollection | null; products: ShopifyProduct[]; pageInfo: { hasNextPage: boolean; endCursor: string | null } }>;
41
+ /** Every product (of a collection or a search query), page by page. For getStaticPaths(). */
42
+ allProducts(opts?: Omit<ProductQuery, 'first' | 'after'>): Promise<ShopifyProduct[]>;
43
+ getCollection(handle: string, opts?: Omit<ProductQuery, 'collection'>): Promise<(ShopifyCollection & { products: ShopifyProduct[]; pageInfo: { hasNextPage: boolean; endCursor: string | null } }) | null>;
44
+ allCollections(): Promise<ShopifyCollection[]>;
45
+ /** By handle, id or product (runs in parallel with getProduct when you pass the handle). */
46
+ getRecommendations(product: string | number | Pick<ShopifyProduct, 'id'>, intent?: 'RELATED' | 'COMPLEMENTARY'): Promise<ShopifyProduct[]>;
47
+ /**
48
+ * A <script type="application/json" data-tether-hydrate> tag: the browser script renders these
49
+ * products without its own request, then refreshes prices and stock once in the background.
50
+ * Astro: <Fragment set:html={shop.hydrate(product)} />
51
+ */
52
+ hydrate(products: ShopifyProduct | ShopifyProduct[]): string;
53
+ formatMoney(money: Money | null | undefined, locale?: string): string;
54
+ productUrl(handle: string, opts?: { productPath?: string; id?: string }): string;
55
+ /** Shopify CDN image at a size. Use the element's max-width value, so the browser keeps the image. */
56
+ imageUrl(url: string | null | undefined, size?: ImageSize): string;
57
+ /**
58
+ * What Tether shows first for this product (variant, price, purchase mode, tether-show conditions),
59
+ * so the server renders the same and nothing flashes. Pass Astro.url.searchParams.get('variant') in SSR.
60
+ */
61
+ initialState(product: ShopifyProduct, opts?: InitialOptions): InitialState | null;
62
+ plans: SellingPlanHelpers;
63
+ }
64
+
65
+ export interface InitialState {
66
+ variant: ShopifyVariant;
67
+ /** option name -> value of that variant: is-selected on those buttons. */
68
+ selected: Record<string, string>;
69
+ purchase: 'one-time' | 'subscription';
70
+ plan: { id: string; name: string } | null;
71
+ plans: { id: string; name: string; price: Money; savings: number; selected: boolean; available: boolean }[];
72
+ savings: number;
73
+ /** The price Tether shows first (the plan price for products that require a subscription). */
74
+ price: Money;
75
+ /** Set only when on sale. */
76
+ compareAtPrice: Money | null;
77
+ available: boolean;
78
+ /** tether-show conditions: hidden={!state.show['out-of-stock']}. */
79
+ show: Record<'in-stock' | 'out-of-stock' | 'backorder' | 'on-sale' | 'not-on-sale' | 'has-variants' | 'single-variant' | 'low-stock' | 'subscription-available' | 'subscription-unavailable' | 'subscription-only' | 'one-time-available' | 'subscription-selected' | 'one-time-selected', boolean>;
80
+ /** true for option values without an available variant: class is-unavailable. */
81
+ isUnavailable(optionName: string, value: string): boolean;
82
+ }
83
+ export interface InitialOptions { variant?: string | null; sellingPlan?: string | null; defaultPurchase?: 'one-time' | 'subscription' }
84
+ export declare function initialState(product: ShopifyProduct, opts?: InitialOptions): InitialState | null;
85
+
86
+ export interface ImageSize { width?: number; height?: number; crop?: 'center' | 'top' | 'bottom' | 'left' | 'right' }
87
+ export declare function imageUrl(url: string | null | undefined, size?: ImageSize): string;
88
+ export declare function createStorefront(options?: StorefrontOptions): Storefront;
89
+ export declare function formatMoney(money: Money | null | undefined, locale?: string): string;
90
+ export declare function productUrl(handle: string, opts?: { productPath?: string; id?: string }): string;
91
+ export declare function productKey(raw: string | number): string | null;
92
+ export declare function withContext(query: string, ctx?: { country?: string; language?: string }): string;
93
+ export declare const plans: SellingPlanHelpers;
94
+ export { TetherError };