@occluze/tether 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/README.md +11 -11
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
  4. package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
  5. package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
  6. package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
  7. package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
  8. package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
  9. package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
  10. package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
  11. package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
  12. package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
  13. package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
  14. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  15. package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
  16. package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
  17. package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
  18. package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
  19. package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
  20. package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
  21. package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -2
  24. package/dist/presets/barba.esm.js +19 -23
  25. package/dist/tether.account-pages.js +320 -365
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +242 -491
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +66 -88
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +36 -33
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +28 -36
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +1270 -894
  36. package/dist/tether.markets.js +73 -90
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +44 -68
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  43. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  44. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  45. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  46. package/dist/types/server/src/shared/json.d.ts +25 -0
  47. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  48. package/dist/types/src/account/addresses-view.d.ts +5 -0
  49. package/dist/types/src/account/answers.d.ts +40 -0
  50. package/dist/types/src/account/markup.d.ts +19 -0
  51. package/dist/types/src/account/operations.d.ts +41 -0
  52. package/dist/types/src/account/order-lines.d.ts +13 -0
  53. package/dist/types/src/account/order-list.d.ts +12 -0
  54. package/dist/types/src/account/order-page.d.ts +6 -0
  55. package/dist/types/src/account/page-answers.d.ts +182 -0
  56. package/dist/types/src/account/page-events.d.ts +2 -0
  57. package/dist/types/src/account/server-client.d.ts +60 -0
  58. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  59. package/dist/types/src/account-api.d.ts +18 -0
  60. package/dist/types/src/account-marks.d.ts +15 -0
  61. package/dist/types/src/account-pages.d.ts +8 -0
  62. package/dist/types/src/account.d.ts +64 -0
  63. package/dist/types/src/amounts.d.ts +12 -0
  64. package/dist/types/src/analytics-shopify.d.ts +41 -0
  65. package/dist/types/src/analytics.d.ts +2 -0
  66. package/dist/types/src/api-version.d.ts +28 -0
  67. package/dist/types/src/astro.d.ts +42 -0
  68. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  69. package/dist/types/src/cart/checkout.d.ts +16 -0
  70. package/dist/types/src/cart/drawer.d.ts +18 -0
  71. package/dist/types/src/cart/mutations.d.ts +79 -0
  72. package/dist/types/src/cart/render.d.ts +17 -0
  73. package/dist/types/src/cart/store.d.ts +182 -0
  74. package/dist/types/src/cart.d.ts +106 -0
  75. package/dist/types/src/client.d.ts +100 -0
  76. package/dist/types/src/collection.d.ts +25 -0
  77. package/dist/types/src/config-sections.d.ts +40 -0
  78. package/dist/types/src/config.d.ts +185 -0
  79. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  80. package/dist/types/src/controls/discount.d.ts +2 -0
  81. package/dist/types/src/controls/extras.d.ts +5 -0
  82. package/dist/types/src/controls/navigation.d.ts +12 -0
  83. package/dist/types/src/diagnose-markup.d.ts +19 -0
  84. package/dist/types/src/diagnose.d.ts +60 -0
  85. package/dist/types/src/dom.d.ts +75 -0
  86. package/dist/types/src/extras.d.ts +21 -0
  87. package/dist/types/src/fragments.d.ts +158 -0
  88. package/dist/types/src/globals.d.ts +15 -0
  89. package/dist/types/src/images.d.ts +20 -0
  90. package/dist/types/src/index.d.ts +167 -0
  91. package/dist/types/src/initial.d.ts +56 -0
  92. package/dist/types/src/lazy.d.ts +28 -0
  93. package/dist/types/src/loader.d.ts +84 -0
  94. package/dist/types/src/market-codes.d.ts +6 -0
  95. package/dist/types/src/markets-ui.d.ts +32 -0
  96. package/dist/types/src/markets.d.ts +72 -0
  97. package/dist/types/src/markup.d.ts +131 -0
  98. package/dist/types/src/mounting.d.ts +46 -0
  99. package/dist/types/src/page-market.d.ts +27 -0
  100. package/dist/types/src/presets/barba.d.ts +56 -0
  101. package/dist/types/src/product-events.d.ts +2 -0
  102. package/dist/types/src/product-render.d.ts +17 -0
  103. package/dist/types/src/product-shape.d.ts +88 -0
  104. package/dist/types/src/product-state.d.ts +110 -0
  105. package/dist/types/src/product.d.ts +66 -0
  106. package/dist/types/src/repeaters.d.ts +16 -0
  107. package/dist/types/src/search.d.ts +33 -0
  108. package/dist/types/src/selling-plans.d.ts +64 -0
  109. package/dist/types/src/server.d.ts +88 -0
  110. package/dist/types/src/shopify-values.d.ts +46 -0
  111. package/dist/types/src/status.d.ts +33 -0
  112. package/dist/types/src/storage.d.ts +12 -0
  113. package/dist/types/src/store-html.d.ts +39 -0
  114. package/dist/types/src/storefront/catalog.d.ts +38 -0
  115. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  116. package/dist/types/src/variant-render.d.ts +14 -0
  117. package/package.json +24 -23
  118. package/src/account/addresses-view.ts +58 -0
  119. package/src/account/answers.ts +79 -0
  120. package/src/account/markup.ts +36 -0
  121. package/src/account/operations.ts +145 -0
  122. package/src/account/{order-lines.js → order-lines.ts} +20 -41
  123. package/src/account/order-list.ts +107 -0
  124. package/src/account/order-page.ts +137 -0
  125. package/src/account/page-answers.ts +317 -0
  126. package/src/account/{page-events.js → page-events.ts} +55 -90
  127. package/src/account/server-client.ts +183 -0
  128. package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
  129. package/src/account-api.ts +98 -0
  130. package/src/account-marks.ts +35 -0
  131. package/src/{account-pages.js → account-pages.ts} +29 -37
  132. package/src/account.ts +284 -0
  133. package/src/amounts.ts +64 -0
  134. package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
  135. package/src/analytics.ts +315 -0
  136. package/src/{api-version.js → api-version.ts} +25 -31
  137. package/src/{astro.js → astro.ts} +110 -104
  138. package/src/cart/cart-shape.ts +25 -0
  139. package/src/cart/checkout.ts +101 -0
  140. package/src/cart/{drawer.js → drawer.ts} +18 -31
  141. package/src/cart/mutations.ts +237 -0
  142. package/src/cart/{render.js → render.ts} +68 -96
  143. package/src/cart/store.ts +279 -0
  144. package/src/{cart.js → cart.ts} +82 -128
  145. package/src/client.ts +391 -0
  146. package/src/{collection.js → collection.ts} +30 -38
  147. package/src/config-sections.ts +284 -0
  148. package/src/config.ts +480 -0
  149. package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
  150. package/src/controls/{discount.js → discount.ts} +13 -34
  151. package/src/controls/{extras.js → extras.ts} +15 -24
  152. package/src/controls/{navigation.js → navigation.ts} +24 -30
  153. package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
  154. package/src/{diagnose.js → diagnose.ts} +95 -96
  155. package/src/dom.ts +268 -0
  156. package/src/{extras.js → extras.ts} +43 -52
  157. package/src/fragments.ts +261 -0
  158. package/src/globals.d.ts +15 -0
  159. package/src/{images.js → images.ts} +30 -43
  160. package/src/index.ts +415 -0
  161. package/src/initial.ts +101 -0
  162. package/src/lazy-iife.ts +197 -0
  163. package/src/lazy.ts +42 -0
  164. package/src/{loader.js → loader.ts} +149 -123
  165. package/src/market-codes.ts +6 -0
  166. package/src/{markets-ui.js → markets-ui.ts} +91 -68
  167. package/src/markets.ts +246 -0
  168. package/src/markup.ts +127 -0
  169. package/src/{mounting.js → mounting.ts} +88 -101
  170. package/src/{page-market.js → page-market.ts} +32 -38
  171. package/src/part-integrity.ts +7 -0
  172. package/src/parts/account-pages.ts +7 -0
  173. package/src/parts/account.ts +7 -0
  174. package/src/parts/analytics.ts +7 -0
  175. package/src/parts/diagnose.ts +7 -0
  176. package/src/parts/extras.ts +7 -0
  177. package/src/parts/markets.ts +7 -0
  178. package/src/parts/search.ts +7 -0
  179. package/src/presets/barba.ts +194 -0
  180. package/src/{product-events.js → product-events.ts} +33 -48
  181. package/src/{product-render.js → product-render.ts} +38 -77
  182. package/src/product-shape.ts +49 -0
  183. package/src/product-state.ts +318 -0
  184. package/src/{product.js → product.ts} +90 -126
  185. package/src/{repeaters.js → repeaters.ts} +40 -52
  186. package/src/{search.js → search.ts} +85 -103
  187. package/src/selling-plans.ts +145 -0
  188. package/src/server.ts +211 -0
  189. package/src/{shopify-values.js → shopify-values.ts} +24 -22
  190. package/src/{status.js → status.ts} +34 -54
  191. package/src/storage.ts +34 -0
  192. package/src/store-html.ts +359 -0
  193. package/src/storefront/catalog.ts +196 -0
  194. package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
  195. package/src/{variant-render.js → variant-render.ts} +24 -50
  196. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  197. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  198. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  199. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  200. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  201. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  202. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  203. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  204. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  205. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  206. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  207. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  208. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  209. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  210. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  211. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  212. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  213. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  214. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  215. package/src/account/addresses-view.js +0 -92
  216. package/src/account/browser-session.js +0 -359
  217. package/src/account/markup.js +0 -48
  218. package/src/account/operations.js +0 -180
  219. package/src/account/order-list.js +0 -91
  220. package/src/account/order-page.js +0 -192
  221. package/src/account/server-client.js +0 -172
  222. package/src/account/shared.js +0 -111
  223. package/src/account/shopify-auth.js +0 -150
  224. package/src/account-api.js +0 -118
  225. package/src/account-links.js +0 -35
  226. package/src/account.js +0 -342
  227. package/src/amounts.js +0 -36
  228. package/src/analytics.js +0 -352
  229. package/src/cart/checkout.js +0 -97
  230. package/src/cart/mutations.js +0 -234
  231. package/src/cart/store.js +0 -326
  232. package/src/client.js +0 -267
  233. package/src/config.js +0 -349
  234. package/src/dom.js +0 -287
  235. package/src/fragments.js +0 -237
  236. package/src/index.js +0 -353
  237. package/src/initial.js +0 -87
  238. package/src/lazy-iife.js +0 -116
  239. package/src/lazy.js +0 -39
  240. package/src/market-codes.js +0 -35
  241. package/src/markets.js +0 -334
  242. package/src/markup.js +0 -64
  243. package/src/parts/account-pages.js +0 -6
  244. package/src/parts/account.js +0 -6
  245. package/src/parts/analytics.js +0 -6
  246. package/src/parts/diagnose.js +0 -6
  247. package/src/parts/extras.js +0 -6
  248. package/src/parts/markets.js +0 -6
  249. package/src/parts/search.js +0 -6
  250. package/src/presets/barba.js +0 -190
  251. package/src/product-state.js +0 -318
  252. package/src/selling-plans.js +0 -154
  253. package/src/server.js +0 -215
  254. package/src/store-html.js +0 -0
  255. package/src/storefront/catalog.js +0 -199
  256. package/types/astro.d.ts +0 -28
  257. package/types/barba.d.ts +0 -16
  258. package/types/index.d.ts +0 -577
  259. package/types/server.d.ts +0 -182
package/src/config.js DELETED
@@ -1,349 +0,0 @@
1
- /**
2
- * The runtime configuration. `cfg` is one shared object that every browser module imports; `configure()` replaces its
3
- * contents in place, so those imports see the new values, and `setCountry()` changes its market keys the same way.
4
- * Options that end up in query text or attribute names are validated here, once, where they arrive.
5
- */
6
- /** @import { Money } from './shopify-values.js' */
7
-
8
- /**
9
- * @typedef {object} MarketsConfig Where the visitor's market comes from (`markets.js`).
10
- * @property {boolean | ('saved' | 'server' | 'webflow')[]} [detect] true: every source; a list: only those.
11
- * @property {Record<string, { country?: string, language?: string, locale?: string }>} [locales] Webflow locale
12
- * (subdirectory or html lang) to market.
13
- * @property {string} [cookie] The cookie a server sets with the visitor's country.
14
- * @property {boolean} [remember] Remember the visitor's choice (default true).
15
- * @property {boolean} [strict] With `locales`: ignore an html lang that isn't in the map.
16
- */
17
-
18
- /**
19
- * @typedef {object} AnalyticsConfig The optional analytics part.
20
- * @property {boolean | string} [dataLayer] GA4/GTM events, into `window.dataLayer` or the named array.
21
- * @property {boolean | { consent?: 'no-banner' | 'default-banner' | 'custom-banner', proxy?: string }} [shopify]
22
- * Shopify's own analytics.
23
- * @property {(name: string, data: Record<string, unknown>) => void} [onEvent]
24
- */
25
-
26
- /**
27
- * @typedef {object} AccountConfig The optional customer accounts part.
28
- * @property {string} [clientId] Browser mode: the Customer Account API client id.
29
- * @property {string} [server] Server mode: the Worker's account routes, a URL or a path on this site.
30
- * @property {string} [orderUrl] The site's own order page, with `{id}` for the numeric order id.
31
- * @property {string} [subscriptionsUrl] Shopify's subscription management page.
32
- * @property {string} [paymentUrl] A separate page for payment methods (default `subscriptionsUrl`).
33
- * @property {string} [callbackUrl] Where Shopify sends the visitor after login (default the current page).
34
- * @property {string} [logoutUrl] Where Shopify sends the visitor after logout (default the home page).
35
- * @property {boolean} [remember] Keep the login across tabs and restarts (localStorage, not sessionStorage).
36
- * @property {string} [tokenUrl] The site's own token endpoint, for when the browser can't call Shopify's.
37
- * @property {string} [shopDomain] The *.myshopify.com domain, when `domain` is another one.
38
- * @property {string} [locale] The language of Shopify's login pages.
39
- * @property {(url: string) => void} [navigate] Replaces `location.assign` for the login redirect (popups, tests).
40
- */
41
-
42
- /** @typedef {{ namespace: string, key: string }} MetafieldIdentifier */
43
- /** @typedef {string | MetafieldIdentifier} MetafieldRef `'namespace.key'` or the identifier itself. */
44
- /** @typedef {{ code: string, message: string, target?: string }} CartWarning Shopify's note on a cart change. */
45
- /** @typedef {'_self' | '_blank'} CheckoutTarget */
46
- /** @typedef {'one-time' | 'subscription'} PurchaseKind */
47
-
48
- /**
49
- * @typedef {object} TetherConfig Every option, with its default filled in.
50
- * @property {string} domain The store's domain, without protocol or path.
51
- * @property {string} token The public Storefront API access token.
52
- * @property {string} apiVersion A dated Shopify API version.
53
- * @property {string | undefined} country An ISO country code for `@inContext(country:)`.
54
- * @property {string | undefined} language A language for `@inContext(language:)`.
55
- * @property {string | undefined} locale An `Intl` locale for money and dates.
56
- * @property {MarketsConfig | null} markets
57
- * @property {string} prefix The attribute prefix: `tether` reads `tether-product`, `tether-bind`.
58
- * @property {string} productPath A folder (`/product`) or a template with `{handle}` / `{id}`.
59
- * @property {string} collectionPath
60
- * @property {MetafieldRef[]} metafields The product metafields to fetch.
61
- * @property {number} imagesFirst
62
- * @property {number} variantsFirst
63
- * @property {number} sellingPlanGroupsFirst
64
- * @property {number} sellingPlansFirst
65
- * @property {number} collectionFirst
66
- * @property {number} retries Retries of a request that failed on the network, a 429/5xx or Shopify's throttling.
67
- * @property {boolean} revalidate Refresh server-rendered products once in the background.
68
- * @property {string} storageKey The localStorage key of the cart id (default `<prefix>:cart:<domain>`).
69
- * @property {string} cartDrawerOpenClass
70
- * @property {boolean} openCartOnAdd
71
- * @property {boolean} cartFocus Move focus into the open cart, keep Tab inside it, restore it on close.
72
- * @property {CheckoutTarget} checkoutTarget
73
- * @property {PurchaseKind} defaultPurchase
74
- * @property {boolean} variantUrl `?variant=` follows the visitor's choice on the page's main product.
75
- * @property {number} maxAge Seconds a Shopify answer may be cached; 0 leaves it to Shopify, which can be hours.
76
- * @property {number} lowStock `tether-show="low-stock"` at this many or fewer (needs inventory access).
77
- * @property {number} searchDelay Milliseconds the search part waits after the last keystroke.
78
- * @property {boolean} autoMount Mount the document on DOMContentLoaded.
79
- * @property {boolean} observe Mount elements added later (CMS load more, filters).
80
- * @property {boolean} astro Mount on Astro's `astro:page-load` and close the cart on navigation.
81
- * @property {string | null} partsUrl The folder of the optional parts, for the `<script>` build.
82
- * @property {AnalyticsConfig | null} analytics
83
- * @property {AccountConfig | null} account
84
- * @property {((warning: CartWarning) => string | null | undefined) | null} warningText The site's text for a cart
85
- * warning; nothing keeps Shopify's.
86
- * @property {((money: Money, locale: string | undefined) => string) | null} formatMoney
87
- * @property {((reason: string) => void) | null} onLayoutChange Default: `ScrollTrigger.refresh()` when present.
88
- * @property {((drawers: HTMLElement[]) => void) | null} onCartOpen
89
- * @property {((drawers: HTMLElement[]) => void) | null} onCartClose
90
- * @property {((node: HTMLElement) => void) | null} onLineEnter
91
- * @property {((node: HTMLElement, done: () => void) => void) | null} onLineLeave Animates a line out, then calls
92
- * `done()`.
93
- * @property {((element: HTMLElement, previousText: string) => void) | null} onValueChange
94
- * @property {((error: unknown, context: string) => void) | null} onError Every failure that reaches the visitor.
95
- * @property {boolean} debug
96
- */
97
-
98
- /** @typedef {{ [Key in keyof TetherConfig]?: TetherConfig[Key] }} TetherOptions */
99
-
100
- /** @type {Readonly<TetherConfig>} */
101
- export const DEFAULTS = {
102
- domain: '',
103
- token: '',
104
- apiVersion: '2026-07',
105
- country: undefined,
106
- language: undefined,
107
- locale: undefined,
108
- markets: null,
109
- prefix: 'tether',
110
- productPath: '/product',
111
- collectionPath: '/collection',
112
- metafields: [],
113
- imagesFirst: 25,
114
- variantsFirst: 100,
115
- sellingPlanGroupsFirst: 10,
116
- sellingPlansFirst: 25,
117
- collectionFirst: 24,
118
- retries: 2,
119
- revalidate: true,
120
- storageKey: '',
121
- cartDrawerOpenClass: 'is-open',
122
- openCartOnAdd: true,
123
- cartFocus: true,
124
- checkoutTarget: '_self',
125
- defaultPurchase: 'one-time',
126
- variantUrl: false,
127
- maxAge: 60,
128
- lowStock: 5,
129
- searchDelay: 150,
130
- autoMount: true,
131
- observe: false,
132
- astro: true,
133
- partsUrl: null,
134
- analytics: null,
135
- account: null,
136
- warningText: null,
137
- formatMoney: null,
138
- onLayoutChange: null,
139
- onCartOpen: null,
140
- onCartClose: null,
141
- onLineEnter: null,
142
- onLineLeave: null,
143
- onValueChange: null,
144
- onError: null,
145
- debug: false,
146
- };
147
-
148
- /** @type {TetherConfig} */
149
- export const cfg = { ...DEFAULTS };
150
-
151
- /** The prefix becomes part of attribute names, CSS selectors and event names, so it is kept to those characters. */
152
- const PREFIX = /^[a-z][a-z0-9-]*$/;
153
- /**
154
- * Metafield identifiers are written into the product fragment's text: a GraphQL fragment can't declare variables, so
155
- * they can't be sent as variables. Only these characters are let through, which covers Shopify's namespaces and keys.
156
- */
157
- const METAFIELD_PART = /^[A-Za-z0-9_.-]+$/;
158
- /** A namespace is the same, or an app-reserved one: `$app` or `$app:<name>`. */
159
- const METAFIELD_NAMESPACE = /^(\$app(:[A-Za-z0-9_-]+)?|[A-Za-z0-9_.-]+)$/;
160
-
161
- /**
162
- * @typedef {'imagesFirst' | 'variantsFirst' | 'sellingPlanGroupsFirst' | 'sellingPlansFirst' | 'collectionFirst'
163
- * | 'retries' | 'maxAge' | 'lowStock'} CountOption
164
- */
165
- /**
166
- * @typedef {'warningText' | 'formatMoney' | 'onLayoutChange' | 'onCartOpen' | 'onCartClose' | 'onLineEnter'
167
- * | 'onLineLeave' | 'onValueChange' | 'onError'} HookOption
168
- */
169
-
170
- /** @type {readonly CountOption[]} Options that are written into queries as list sizes or used as counts. */
171
- const COUNT_OPTIONS = [
172
- 'imagesFirst',
173
- 'variantsFirst',
174
- 'sellingPlanGroupsFirst',
175
- 'sellingPlansFirst',
176
- 'collectionFirst',
177
- 'retries',
178
- 'maxAge',
179
- 'lowStock',
180
- ];
181
- /** @type {readonly HookOption[]} */
182
- const HOOK_OPTIONS = [
183
- 'warningText',
184
- 'formatMoney',
185
- 'onLayoutChange',
186
- 'onCartOpen',
187
- 'onCartClose',
188
- 'onLineEnter',
189
- 'onLineLeave',
190
- 'onValueChange',
191
- 'onError',
192
- ];
193
-
194
- /**
195
- * @template T
196
- * @param {string} name
197
- * @param {unknown} value
198
- * @param {T} fallback
199
- * @returns {T} `fallback`, with a warning unless `value` was left out.
200
- */
201
- function invalidOption(name, value, fallback) {
202
- if (value !== undefined) warn(`invalid ${name} ${JSON.stringify(value)}: using ${JSON.stringify(fallback)}`);
203
- return fallback;
204
- }
205
-
206
- /**
207
- * @param {CountOption} name
208
- * @param {unknown} value
209
- * @returns {number} `value` when it is a non-negative integer, else the default.
210
- */
211
- function countOption(name, value) {
212
- if (typeof value === 'number' && Number.isInteger(value) && value >= 0) return value;
213
- return invalidOption(name, value, DEFAULTS[name]);
214
- }
215
-
216
- /**
217
- * @template {string} Choice
218
- * @param {string} name
219
- * @param {unknown} value
220
- * @param {readonly [Choice, ...Choice[]]} choices The first one is the default.
221
- * @returns {Choice}
222
- */
223
- function choiceOption(name, value, choices) {
224
- return choices.find((choice) => choice === value) ?? invalidOption(name, value, choices[0]);
225
- }
226
-
227
- /**
228
- * @param {unknown} value
229
- * @returns {string} The store's domain without protocol or path: `https://shop.example/x` → `shop.example`.
230
- */
231
- function domainOption(value) {
232
- if (typeof value !== 'string') return '';
233
- return value.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
234
- }
235
-
236
- /**
237
- * @param {unknown} ref
238
- * @returns {MetafieldIdentifier | null} The identifier of `'namespace.key'` or `{ namespace, key }`, when both parts are
239
- * valid identifiers.
240
- */
241
- export function toMetafieldIdentifier(ref) {
242
- /** @type {unknown} */
243
- let namespace;
244
- /** @type {unknown} */
245
- let key;
246
- if (typeof ref === 'string') {
247
- const dot = ref.indexOf('.');
248
- namespace = dot > 0 ? ref.slice(0, dot) : '';
249
- key = ref.slice(dot + 1);
250
- } else if (typeof ref === 'object' && ref !== null) {
251
- namespace = Reflect.get(ref, 'namespace');
252
- key = Reflect.get(ref, 'key');
253
- }
254
- if (typeof namespace !== 'string' || typeof key !== 'string') return null;
255
- return METAFIELD_NAMESPACE.test(namespace) && METAFIELD_PART.test(key) ? { namespace, key } : null;
256
- }
257
-
258
- /**
259
- * @param {unknown} value
260
- * @returns {MetafieldRef[]} The refs that name a valid identifier, as written; each other one is skipped with a warning.
261
- */
262
- function metafieldsOption(value) {
263
- if (!Array.isArray(value)) return invalidOption('metafields', value, []);
264
- /** @type {MetafieldRef[]} */
265
- const refs = [];
266
- for (const ref of /** @type {unknown[]} */ (value)) {
267
- const identifier = toMetafieldIdentifier(ref);
268
- if (identifier) refs.push(typeof ref === 'string' ? ref : identifier);
269
- else invalidOption('metafield', ref, null);
270
- }
271
- return refs;
272
- }
273
-
274
- /**
275
- * The configuration `options` describe: defaults filled in, counts, prefix, choices and metafields validated (an
276
- * invalid value warns and falls back to its default), hooks that aren't functions dropped so callers only check for
277
- * null.
278
- *
279
- * @param {TetherOptions} options
280
- * @returns {TetherConfig}
281
- */
282
- export function resolveConfig(options) {
283
- const domain = domainOption(options.domain);
284
- const prefix =
285
- typeof options.prefix === 'string' && PREFIX.test(options.prefix)
286
- ? options.prefix
287
- : invalidOption('prefix', options.prefix, DEFAULTS.prefix);
288
- /** @type {TetherConfig} */
289
- const resolved = {
290
- ...DEFAULTS,
291
- ...options,
292
- domain,
293
- prefix,
294
- storageKey: options.storageKey || `${prefix}:cart:${domain}`,
295
- metafields: metafieldsOption(options.metafields ?? []),
296
- checkoutTarget: choiceOption('checkoutTarget', options.checkoutTarget, ['_self', '_blank']),
297
- defaultPurchase: choiceOption('defaultPurchase', options.defaultPurchase, ['one-time', 'subscription']),
298
- };
299
- for (const name of COUNT_OPTIONS) resolved[name] = countOption(name, options[name]);
300
- for (const name of HOOK_OPTIONS) if (typeof resolved[name] !== 'function') resolved[name] = null;
301
- if (resolved.apiVersion === 'unstable') {
302
- warn('apiVersion "unstable" changes without notice. Pin a dated version (e.g. 2026-07).');
303
- }
304
- return resolved;
305
- }
306
-
307
- /**
308
- * Replaces the shared configuration with `options` over the defaults (see `resolveConfig`).
309
- *
310
- * @param {TetherOptions} [options]
311
- * @returns {TetherConfig} The shared `cfg`.
312
- */
313
- export function configure(options = {}) {
314
- const resolved = resolveConfig(options);
315
- for (const key of Object.keys(cfg)) {
316
- if (!(key in resolved)) Reflect.deleteProperty(cfg, key);
317
- }
318
- return Object.assign(cfg, resolved);
319
- }
320
-
321
- /** @returns {boolean} True once a domain and a token are set. */
322
- export const isConfigured = () => Boolean(cfg.domain && cfg.token);
323
-
324
- /** @param {...unknown} args */
325
- export function log(...args) {
326
- if (cfg.debug) console.log(`[${cfg.prefix}]`, ...args);
327
- }
328
-
329
- /** @param {...unknown} args */
330
- export function warn(...args) {
331
- console.warn(`[${cfg.prefix}]`, ...args);
332
- }
333
-
334
- /**
335
- * Logs a failure that reaches the visitor and hands it to `onError`. A throwing `onError` is logged, never rethrown:
336
- * the site's hook must not break the render that reported the failure.
337
- *
338
- * @param {unknown} error
339
- * @param {string} context What failed, e.g. `product coffee-box`.
340
- */
341
- export function reportError(error, context) {
342
- warn(context, error instanceof Error && error.message ? error.message : error);
343
- if (!cfg.onError) return;
344
- try {
345
- cfg.onError(error, context);
346
- } catch (hookError) {
347
- warn('onError threw', hookError);
348
- }
349
- }
package/src/dom.js DELETED
@@ -1,287 +0,0 @@
1
- /**
2
- * The browser modules' DOM toolkit: what Shopify values look like on the page (money, dates, text, images, lists)
3
- * and how elements show, hide and report layout changes. Attribute names go through `attr()` so the prefix is
4
- * configurable. The optional parts import all of it from here (`window.Tether.__core['dom.js']`).
5
- */
6
- import { cfg, reportError, warn } from './config.js';
7
- import { attr, emit, get } from './markup.js';
8
- import { storeHtmlFragment } from './store-html.js';
9
- import { formatMoney as formatMoneyIn, productUrl as productUrlFor } from './shopify-values.js';
10
- /** @import { Money } from './shopify-values.js' */
11
-
12
- export { attr, sel, get, has, qsa, scoped, emit } from './markup.js';
13
- export { imageUrl, cdnUrl, sameImage, setImage } from './images.js';
14
- export { hasTemplate, repeat, repeatKeyed } from './repeaters.js';
15
-
16
- /**
17
- * State Tether keeps on elements, as `data-tether-*` attributes whatever the prefix: sites style and test against
18
- * these names, so they stay fixed where the `tether-*` markup attributes follow `prefix`.
19
- */
20
- const SEEN = 'data-tether-seen';
21
- const SHOWN = 'data-tether-shown';
22
- const PREVIOUS_DISPLAY = 'data-tether-prev-display';
23
-
24
- /** Async renders change the page height in bursts; one notification per burst. */
25
- const LAYOUT_DEBOUNCE_MS = 120;
26
-
27
- /**
28
- * The product page of a product (`productPath`).
29
- *
30
- * @param {string | null | undefined} handle
31
- * @param {string | null} [id]
32
- */
33
- export function productUrl(handle, id) {
34
- return productUrlFor(cfg.productPath || '/product', handle, id);
35
- }
36
-
37
- /**
38
- * `Intl` throws a RangeError for a locale or currency it doesn't know; the caller then shows a plain fallback.
39
- *
40
- * @template T
41
- * @param {() => T} format
42
- * @param {T} fallback
43
- * @returns {T}
44
- */
45
- function formatOr(format, fallback) {
46
- try {
47
- return format();
48
- } catch (error) {
49
- if (error instanceof RangeError) return fallback;
50
- throw error;
51
- }
52
- }
53
-
54
- /**
55
- * `30` → `30%` (en, nl) or `30 %` (de, fr), in the configured locale.
56
- *
57
- * @param {number | null | undefined} percentage
58
- * @param {string | undefined} [locale]
59
- * @returns {string} `''` for no or a zero percentage.
60
- */
61
- export function formatPercent(percentage, locale = cfg.locale) {
62
- if (!percentage) return '';
63
- const format = () =>
64
- new Intl.NumberFormat(locale || undefined, { style: 'percent', maximumFractionDigits: 0 }).format(percentage / 100);
65
- return formatOr(format, `${percentage}%`);
66
- }
67
-
68
- /**
69
- * Money in the configured locale, through the site's `formatMoney` when it has one.
70
- *
71
- * @param {Money | null | undefined} money
72
- */
73
- export function formatMoney(money) {
74
- if (!money) return '';
75
- if (cfg.formatMoney) return cfg.formatMoney(money, cfg.locale);
76
- return formatMoneyIn(money, cfg.locale);
77
- }
78
-
79
- /**
80
- * @param {string | null | undefined} iso
81
- * @returns {string} The date in the configured locale, `''` without one.
82
- */
83
- export function formatDate(iso) {
84
- if (!iso) return '';
85
- return formatOr(() => new Date(iso).toLocaleDateString(cfg.locale || undefined), '');
86
- }
87
-
88
- /**
89
- * Writes HTML from the Shopify admin (product and collection descriptions) into `el`, with script removed
90
- * (`store-html.js`). Everything else from Shopify is written as text (`setText`).
91
- *
92
- * @param {Element} el
93
- * @param {string | null | undefined} html
94
- */
95
- export function setStoreHtml(el, html) {
96
- el.replaceChildren(storeHtmlFragment(html, el.ownerDocument));
97
- }
98
-
99
- /**
100
- * @param {Element} el
101
- * @returns {el is HTMLInputElement | HTMLTextAreaElement}
102
- */
103
- const isTextField = (el) => el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
104
-
105
- /**
106
- * @param {Element} el
107
- * @returns {el is HTMLInputElement}
108
- */
109
- const isInput = (el) => el.tagName === 'INPUT';
110
-
111
- /**
112
- * Sets the text of `el`, or its value when it is a form field.
113
- *
114
- * @param {Element} el
115
- * @param {string | number | boolean | null | undefined} value `null` and `undefined` clear it.
116
- */
117
- export function setText(el, value) {
118
- const text = value == null ? '' : String(value);
119
- if (isTextField(el)) el.value = text;
120
- else el.textContent = text;
121
- }
122
-
123
- /** @param {Element} el */
124
- const readText = (el) => (isTextField(el) ? el.value : el.textContent);
125
-
126
- /**
127
- * `setText` that hands a change after the first render to `onValueChange`, for scramble and roll effects.
128
- *
129
- * @param {HTMLElement} el
130
- * @param {string | number | boolean | null | undefined} value
131
- */
132
- export function setTextTracked(el, value) {
133
- const before = readText(el);
134
- setText(el, value);
135
- if (before !== readText(el) && el.hasAttribute(SEEN) && cfg.onValueChange) {
136
- try {
137
- cfg.onValueChange(el, before);
138
- } catch (error) {
139
- reportError(error, 'onValueChange');
140
- }
141
- }
142
- el.setAttribute(SEEN, '1');
143
- }
144
-
145
- /**
146
- * Shows an element hidden before the script ran (a class, an inline `display: none`, a `hidden` attribute), as
147
- * `block` or the value of `tether-display`.
148
- *
149
- * @param {HTMLElement} el
150
- */
151
- function reveal(el) {
152
- if (el.hasAttribute(SHOWN)) return;
153
- el.setAttribute(SHOWN, '');
154
- el.removeAttribute('hidden');
155
- const previous = el.getAttribute(PREVIOUS_DISPLAY);
156
- if (previous !== null) {
157
- el.style.display = previous;
158
- el.removeAttribute(PREVIOUS_DISPLAY);
159
- } else if (el.style.display === 'none') {
160
- el.style.display = '';
161
- }
162
- if (typeof getComputedStyle === 'function' && getComputedStyle(el).display === 'none') {
163
- el.style.display = el.getAttribute(attr('display')) || 'block';
164
- }
165
- }
166
-
167
- /**
168
- * Shows or hides `el`, restoring its own display value when shown again.
169
- *
170
- * @param {HTMLElement} el
171
- * @param {boolean} visible
172
- */
173
- export function show(el, visible) {
174
- if (visible) {
175
- reveal(el);
176
- return;
177
- }
178
- el.removeAttribute(SHOWN);
179
- // The display value to restore is a real one, never 'none'.
180
- if (!el.hasAttribute(PREVIOUS_DISPLAY) && el.style.display !== 'none') {
181
- el.setAttribute(PREVIOUS_DISPLAY, el.style.display);
182
- }
183
- el.style.display = 'none';
184
- }
185
-
186
- /**
187
- * Marks an option button or input as chosen: `is-selected`, `aria-pressed`, and its own or inner radio/checkbox.
188
- *
189
- * @param {Element} el
190
- * @param {boolean} selected
191
- */
192
- export function setSelected(el, selected) {
193
- el.classList.toggle('is-selected', selected);
194
- el.setAttribute('aria-pressed', selected ? 'true' : 'false');
195
- if (isInput(el) && (el.type === 'radio' || el.type === 'checkbox')) el.checked = selected;
196
- const input = el.querySelector('input[type="radio"],input[type="checkbox"]');
197
- if (input && isInput(input)) input.checked = selected;
198
- }
199
-
200
- /** @type {ReturnType<typeof setTimeout> | undefined} */
201
- let layoutTimer;
202
-
203
- /**
204
- * Async renders change the page height after the site's page-transition hooks ran. After a burst of them this calls
205
- * `onLayoutChange` (else refreshes ScrollTrigger) and emits `tether:layout-changed`.
206
- *
207
- * @param {string} reason
208
- */
209
- export function notifyLayoutChanged(reason) {
210
- clearTimeout(layoutTimer);
211
- layoutTimer = setTimeout(() => {
212
- // GSAP's ScrollTrigger, when the site has it, measures positions that a height change makes stale.
213
- const { ScrollTrigger } = /** @type {Window & { ScrollTrigger?: { refresh?: () => void } }} */ (window);
214
- if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
215
- else ScrollTrigger?.refresh?.();
216
- emit('layout-changed', { reason });
217
- }, LAYOUT_DEBOUNCE_MS);
218
- }
219
-
220
- /**
221
- * Notifies a layout change (`<reason>:images`) once every image in `root` that is still loading has loaded or failed.
222
- *
223
- * @param {ParentNode} root
224
- * @param {string} reason
225
- */
226
- export function watchImages(root, reason) {
227
- const loading = [...root.querySelectorAll('img')].filter((img) => !img.complete);
228
- let left = loading.length;
229
- const done = () => {
230
- left -= 1;
231
- if (left === 0) notifyLayoutChanged(`${reason}:images`);
232
- };
233
- loading.forEach((img) => {
234
- img.addEventListener('load', done, { once: true });
235
- img.addEventListener('error', done, { once: true });
236
- });
237
- }
238
-
239
- /**
240
- * Runs `task` with `aria-busy` on `el`; does nothing while `el` is already busy.
241
- *
242
- * @template T
243
- * @param {Element} el
244
- * @param {() => T | Promise<T>} task
245
- * @returns {Promise<T | undefined>} The task's result, `undefined` when it didn't run.
246
- */
247
- export function busy(el, task) {
248
- if (el.getAttribute('aria-busy') === 'true') return Promise.resolve(undefined);
249
- el.setAttribute('aria-busy', 'true');
250
- return Promise.resolve()
251
- .then(task)
252
- .finally(() => {
253
- el.removeAttribute('aria-busy');
254
- });
255
- }
256
-
257
- /**
258
- * @param {Element | null | undefined} el
259
- * @param {string} name
260
- * @returns {unknown} The attribute's JSON, `null` when it is missing or not JSON (with a warning).
261
- */
262
- export function parseJSONAttr(el, name) {
263
- const raw = el ? get(el, name) : null;
264
- if (!raw) return null;
265
- try {
266
- return /** @type {unknown} */ (JSON.parse(raw));
267
- } catch {
268
- warn(`invalid JSON in ${attr(name)}`, el);
269
- return null;
270
- }
271
- }
272
-
273
- /** @typedef {string | number | boolean | null | undefined} AttributeValue */
274
-
275
- /**
276
- * `{ a: 1 }` or `[{ key, value }]` → `[{ key, value }]` as Shopify wants cart and line attributes.
277
- *
278
- * @param {Record<string, AttributeValue> | { key: string | number, value?: AttributeValue }[] | null | undefined} attributes
279
- * @returns {{ key: string, value: string }[]}
280
- */
281
- export function toAttributes(attributes) {
282
- if (!attributes) return [];
283
- if (Array.isArray(attributes)) {
284
- return attributes.map((attribute) => ({ key: String(attribute.key), value: String(attribute.value ?? '') }));
285
- }
286
- return Object.entries(attributes).map(([key, value]) => ({ key, value: String(value ?? '') }));
287
- }