@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
@@ -3,26 +3,27 @@
3
3
  * first change in another tab) and shareable variant links (on the first variant change, with `variantUrl` or
4
4
  * `tether-variant-url`).
5
5
  */
6
- import { cfg, warn, reportError } from './config.js';
7
- import { sel, get, has, qsa, emit, parseJSONAttr, toAttributes } from './dom.js';
8
- import { scopes } from './product.js';
9
- import { Cart } from './cart.js';
10
- /** @import { ShopifyCart } from './cart/store.js' */
6
+ import { cfg, warn, reportError } from './config.ts';
7
+ import { sel, get, has, qsa, emit, parseJSONAttr, attributesFromJson } from './dom.ts';
8
+ import { scopes } from './product.ts';
9
+ import { Cart } from './cart.ts';
10
+ import { isRecord } from '../server/src/shared/json.ts';
11
+ import type { ShopifyCart } from './cart/store.ts';
11
12
 
12
- /** @typedef {{ country: string, language: string | null, locale: string | undefined }} StoredMarket */
13
- /**
14
- * @typedef {(country: string, options: { language: string | null, locale: string | undefined, remember: boolean })
15
- * => Promise<unknown>} SetCountry
16
- */
13
+ interface StoredMarket {
14
+ country: string;
15
+ language: string | null;
16
+ locale: string | undefined;
17
+ }
17
18
 
18
- /**
19
- * @param {Element} group
20
- * @param {string | null} name The group's `tether-group` value.
21
- */
22
- function groupAttributes(group, name) {
23
- const extra = toAttributes(
24
- /** @type {Parameters<typeof toAttributes>[0]} */ (parseJSONAttr(group, 'line-attributes')),
25
- );
19
+ type SetCountry = (
20
+ country: string,
21
+ options: { language: string | null; locale: string | undefined; remember: boolean },
22
+ ) => Promise<unknown>;
23
+
24
+ /** @param name The group's `tether-group` value. */
25
+ function groupAttributes(group: Element, name: string | null) {
26
+ const extra = attributesFromJson(parseJSONAttr(group, 'line-attributes'));
26
27
  return name ? [{ key: '_group', value: name }, ...extra] : extra;
27
28
  }
28
29
 
@@ -30,10 +31,9 @@ function groupAttributes(group, name) {
30
31
  * `[tether-group]` with `[tether-group-add]`: adds every product scope inside the group in one request, for bundles,
31
32
  * build-a-box and "complete the set". The lines get the attribute `_group` and the group's `tether-line-attributes`.
32
33
  *
33
- * @param {Element} button
34
- * @returns {Promise<ShopifyCart | null>} The cart, `null` with nothing to add or after a reported failure.
34
+ * @returns The cart, `null` with nothing to add or after a reported failure.
35
35
  */
36
- export async function addGroup(button) {
36
+ export async function addGroup(button: Element): Promise<ShopifyCart | null> {
37
37
  const group = button.closest(sel('group'));
38
38
  if (!group) return null;
39
39
  const name = get(group, 'group');
@@ -58,22 +58,16 @@ export async function addGroup(button) {
58
58
  }
59
59
  }
60
60
 
61
- /**
62
- * Another tab writes the market as JSON; a value that isn't (written by something else under the key) is skipped.
63
- *
64
- * @param {string} text
65
- * @returns {StoredMarket | null}
66
- */
67
- function parseStoredMarket(text) {
68
- /** @type {unknown} */
69
- let value;
61
+ /** Another tab writes the market as JSON; a value that isn't (written by something else under the key) is skipped. */
62
+ function parseStoredMarket(text: string): StoredMarket | null {
63
+ let value: unknown;
70
64
  try {
71
65
  value = JSON.parse(text);
72
66
  } catch {
73
67
  return null;
74
68
  }
75
- if (typeof value !== 'object' || value === null) return null;
76
- const { country, language, locale } = /** @type {Record<string, unknown>} */ (value);
69
+ if (!isRecord(value)) return null;
70
+ const { country, language, locale } = value;
77
71
  if (typeof country !== 'string' || !country) return null;
78
72
  return {
79
73
  country,
@@ -82,41 +76,36 @@ function parseStoredMarket(text) {
82
76
  };
83
77
  }
84
78
 
85
- /**
86
- * Another tab changed the cart (it is loaded again here) or the visitor's market (this tab follows it).
87
- *
88
- * @param {StorageEvent} event
89
- */
90
- export function onStorage(event) {
79
+ /** Another tab changed the cart (it is loaded again here) or the visitor's market (this tab follows it). */
80
+ export function onStorage(event: StorageEvent): void {
91
81
  if (event.key === cfg.storageKey || event.key === `${cfg.storageKey}:seen`) {
92
- Cart.init().catch((/** @type {unknown} */ error) => {
82
+ Cart.init().catch((error: unknown) => {
93
83
  reportError(error, 'cart from another tab');
94
84
  });
95
85
  return;
96
86
  }
97
87
  if (event.key !== `${cfg.prefix}:market:${cfg.domain}` || !event.newValue) return;
98
88
  const market = parseStoredMarket(event.newValue);
99
- const { Tether } = /** @type {Window & { Tether?: { setCountry: SetCountry } }} */ (window);
100
- if (!market || !Tether) return;
89
+ // The core's `setCountry` takes the `null` language and `undefined` locale stored here, which the public type of
90
+ // `window.Tether` leaves out.
91
+ const tether = Reflect.get(window, 'Tether') as { setCountry: SetCountry } | undefined;
92
+ if (!market || !tether) return;
101
93
  const options = { language: market.language, locale: market.locale, remember: false };
102
- Tether.setCountry(market.country, options).catch((/** @type {unknown} */ error) => {
94
+ tether.setCountry(market.country, options).catch((error: unknown) => {
103
95
  reportError(error, 'market from another tab');
104
96
  });
105
97
  }
106
98
 
107
- /** @type {WeakMap<Element, string>} Scope root → its last variant and plan, so the first render doesn't write. */
108
- const urlKeys = new WeakMap();
99
+ /** Scope root → its last variant and plan, so the first render doesn't write. */
100
+ const urlKeys = new WeakMap<Element, string>();
109
101
 
110
- /** @param {string} gid */
111
- const numericId = (gid) => gid.split('/').pop() ?? gid;
102
+ const numericId = (gid: string) => gid.split('/').pop() ?? gid;
112
103
 
113
104
  /**
114
105
  * Safari throws a SecurityError past 100 history changes in 10 seconds; the address then keeps the previous choice
115
106
  * until the next one.
116
- *
117
- * @param {URL} url
118
107
  */
119
- function replaceUrlIfAllowed(url) {
108
+ function replaceUrlIfAllowed(url: URL) {
120
109
  try {
121
110
  window.history.replaceState(window.history.state, '', url);
122
111
  } catch {
@@ -127,10 +116,12 @@ function replaceUrlIfAllowed(url) {
127
116
  /**
128
117
  * `?variant=` (and `selling_plan=`) follow the visitor's choice.
129
118
  *
130
- * @param {Element} root The product scope's root.
131
- * @param {{ variant: { id: string }, sellingPlanId: string | null | undefined }} choice
119
+ * @param root The product scope's root.
132
120
  */
133
- export function writeVariantUrl(root, { variant, sellingPlanId }) {
121
+ export function writeVariantUrl(
122
+ root: Element,
123
+ { variant, sellingPlanId }: { variant: { id: string }; sellingPlanId: string | null | undefined },
124
+ ): void {
134
125
  const key = `${variant.id}|${sellingPlanId ?? ''}`;
135
126
  const before = urlKeys.get(root);
136
127
  urlKeys.set(root, key);
@@ -0,0 +1,261 @@
1
+ /**
2
+ * The GraphQL fragments of what Tether renders. They are built at call time because the list sizes and metafields come
3
+ * from the config; the browser passes its validated `cfg`, the server helpers their own. Operations put these after
4
+ * the operation, where `withContext` expects them.
5
+ */
6
+ import { cfg } from './config.ts';
7
+ import { toMetafieldIdentifier } from './config-sections.ts';
8
+ import type { TetherConfig, MetafieldIdentifier } from './config.ts';
9
+ import type { Money } from './shopify-values.ts';
10
+ import type { StoreAccess } from './client.ts';
11
+
12
+ export interface Image {
13
+ url: string;
14
+ altText: string | null;
15
+ width: number | null;
16
+ height: number | null;
17
+ }
18
+
19
+ /** An option value's swatch: a colour, an image, or both, as the merchant set them in the admin. */
20
+ interface Swatch {
21
+ color: string | null;
22
+ image: { previewImage: Image | null } | null;
23
+ }
24
+
25
+ export type SellingPlanAdjustmentValue =
26
+ | { __typename: 'SellingPlanPercentagePriceAdjustment'; adjustmentPercentage: number }
27
+ | { __typename: 'SellingPlanFixedAmountPriceAdjustment'; adjustmentAmount: Money }
28
+ | { __typename: 'SellingPlanFixedPriceAdjustment'; price: Money };
29
+
30
+ export interface SellingPlan {
31
+ id: string;
32
+ name: string;
33
+ description: string | null;
34
+ recurringDeliveries: boolean;
35
+ options: { name: string; value: string }[];
36
+ priceAdjustments: { orderCount: number | null; adjustmentValue: SellingPlanAdjustmentValue }[];
37
+ }
38
+
39
+ export interface SellingPlanGroup {
40
+ name: string;
41
+ appName: string | null;
42
+ options: { name: string; values: string[] }[];
43
+ sellingPlans: { nodes: SellingPlan[] };
44
+ }
45
+
46
+ /** A variant's price on one plan. */
47
+ export interface SellingPlanAllocation {
48
+ sellingPlan: { id: string };
49
+ /** What the customer pays at checkout (more for a prepaid plan). */
50
+ checkoutChargeAmount: Money;
51
+ remainingBalanceChargeAmount: Money;
52
+ priceAdjustments: { price: Money; compareAtPrice: Money; perDeliveryPrice: Money; unitPrice: Money | null }[];
53
+ }
54
+
55
+ export interface ProductVariant {
56
+ id: string;
57
+ title: string;
58
+ availableForSale: boolean;
59
+ currentlyNotInStock: boolean;
60
+ /** Absent when the store's token can't read inventory. */
61
+ quantityAvailable?: number | null;
62
+ sku: string | null;
63
+ barcode: string | null;
64
+ selectedOptions: { name: string; value: string }[];
65
+ image: Image | null;
66
+ price: Money;
67
+ compareAtPrice: Money | null;
68
+ unitPrice: Money | null;
69
+ sellingPlanAllocations: { nodes: SellingPlanAllocation[] };
70
+ }
71
+
72
+ export interface Metafield {
73
+ namespace: string;
74
+ key: string;
75
+ type: string;
76
+ value: string;
77
+ /** A file reference's image. */
78
+ reference: { image?: Image } | null;
79
+ }
80
+
81
+ /** The fields of `fragment TetherProduct`. */
82
+ export interface Product {
83
+ id: string;
84
+ handle: string;
85
+ title: string;
86
+ description: string;
87
+ descriptionHtml: string;
88
+ productType: string;
89
+ vendor: string;
90
+ tags: string[];
91
+ availableForSale: boolean;
92
+ publishedAt: string | null;
93
+ onlineStoreUrl: string | null;
94
+ requiresSellingPlan: boolean;
95
+ seo: { title: string | null; description: string | null };
96
+ featuredImage: Image | null;
97
+ images: { nodes: Image[] };
98
+ options: { name: string; optionValues: { name: string; swatch: Swatch | null }[] }[];
99
+ priceRange: { minVariantPrice: Money; maxVariantPrice: Money };
100
+ compareAtPriceRange: { minVariantPrice: Money; maxVariantPrice: Money };
101
+ /** Present when metafields are configured; `null` for one that isn't set. */
102
+ metafields?: (Metafield | null)[];
103
+ sellingPlanGroups: { nodes: SellingPlanGroup[] };
104
+ variants: { nodes: ProductVariant[] };
105
+ }
106
+
107
+ export type ProductFragmentConfig = Pick<
108
+ TetherConfig,
109
+ 'imagesFirst' | 'variantsFirst' | 'sellingPlanGroupsFirst' | 'sellingPlansFirst' | 'metafields'
110
+ >;
111
+
112
+ const MONEY = 'amount currencyCode';
113
+ /** What a token reads unless the store turned out to refuse more (client.ts). */
114
+ const FULL_ACCESS: StoreAccess = { inventory: true };
115
+
116
+ /** The stock field, for a token that may read inventory. */
117
+ const inventoryField = (access: StoreAccess): string => (access.inventory ? 'quantityAvailable' : '');
118
+ const IMAGE = 'url altText width height';
119
+
120
+ /**
121
+ * The `metafields(identifiers:)` field. A fragment can't declare variables, so the identifiers are written into the
122
+ * text: only valid ones (`toMetafieldIdentifier`), each quoted as a string literal.
123
+ */
124
+ function metafieldsField(metafields: ProductFragmentConfig['metafields']) {
125
+ const identifiers: MetafieldIdentifier[] = [];
126
+ for (const ref of metafields) {
127
+ const identifier = toMetafieldIdentifier(ref);
128
+ if (identifier) identifiers.push(identifier);
129
+ }
130
+ if (!identifiers.length) return '';
131
+ const list = identifiers
132
+ .map(({ namespace, key }) => `{namespace: ${JSON.stringify(namespace)}, key: ${JSON.stringify(key)}}`)
133
+ .join(', ');
134
+ return `metafields(identifiers: [${list}]) {
135
+ namespace key type value
136
+ reference { ... on MediaImage { image { ${IMAGE} } } }
137
+ }`;
138
+ }
139
+
140
+ const SELLING_PLAN_FIELDS = `
141
+ id name description recurringDeliveries
142
+ options { name value }
143
+ priceAdjustments {
144
+ orderCount
145
+ adjustmentValue {
146
+ __typename
147
+ ... on SellingPlanPercentagePriceAdjustment { adjustmentPercentage }
148
+ ... on SellingPlanFixedAmountPriceAdjustment { adjustmentAmount { ${MONEY} } }
149
+ ... on SellingPlanFixedPriceAdjustment { price { ${MONEY} } }
150
+ }
151
+ }`;
152
+
153
+ const ALLOCATION_FIELDS = `
154
+ sellingPlan { id }
155
+ checkoutChargeAmount { ${MONEY} }
156
+ remainingBalanceChargeAmount { ${MONEY} }
157
+ priceAdjustments {
158
+ price { ${MONEY} }
159
+ compareAtPrice { ${MONEY} }
160
+ perDeliveryPrice { ${MONEY} }
161
+ unitPrice { ${MONEY} }
162
+ }`;
163
+
164
+ /**
165
+ * @param o The list sizes and metafields.
166
+ * @param access What the token may read.
167
+ * @returns `fragment TetherProduct on Product`.
168
+ */
169
+ export function productFragment(o: ProductFragmentConfig = cfg, access: StoreAccess = FULL_ACCESS): string {
170
+ return `
171
+ fragment TetherProduct on Product {
172
+ id handle title description descriptionHtml productType vendor tags
173
+ availableForSale publishedAt onlineStoreUrl requiresSellingPlan
174
+ seo { title description }
175
+ featuredImage { ${IMAGE} }
176
+ images(first: ${o.imagesFirst}) { nodes { ${IMAGE} } }
177
+ options { name optionValues { name swatch { color image { previewImage { ${IMAGE} } } } } }
178
+ priceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
179
+ compareAtPriceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
180
+ ${metafieldsField(o.metafields)}
181
+ sellingPlanGroups(first: ${o.sellingPlanGroupsFirst}) {
182
+ nodes {
183
+ name appName
184
+ options { name values }
185
+ sellingPlans(first: ${o.sellingPlansFirst}) { nodes { ${SELLING_PLAN_FIELDS} } }
186
+ }
187
+ }
188
+ variants(first: ${o.variantsFirst}) {
189
+ nodes {
190
+ id title availableForSale currentlyNotInStock ${inventoryField(access)} sku barcode
191
+ selectedOptions { name value }
192
+ image { ${IMAGE} }
193
+ price { ${MONEY} }
194
+ compareAtPrice { ${MONEY} }
195
+ unitPrice { ${MONEY} }
196
+ sellingPlanAllocations(first: ${o.sellingPlansFirst}) { nodes { ${ALLOCATION_FIELDS} } }
197
+ }
198
+ }
199
+ }`;
200
+ }
201
+
202
+ /**
203
+ * @param access What the token may read.
204
+ * @returns `fragment TetherCart on Cart`.
205
+ */
206
+ export function cartFragment(access: StoreAccess = FULL_ACCESS): string {
207
+ return `
208
+ fragment TetherCart on Cart {
209
+ id checkoutUrl totalQuantity note createdAt updatedAt
210
+ attributes { key value }
211
+ buyerIdentity { countryCode email }
212
+ discountCodes { code applicable }
213
+ cost {
214
+ subtotalAmount { ${MONEY} }
215
+ totalAmount { ${MONEY} }
216
+ checkoutChargeAmount { ${MONEY} }
217
+ }
218
+ lines(first: 100) {
219
+ nodes {
220
+ id quantity
221
+ attributes { key value }
222
+ cost {
223
+ totalAmount { ${MONEY} }
224
+ subtotalAmount { ${MONEY} }
225
+ amountPerQuantity { ${MONEY} }
226
+ compareAtAmountPerQuantity { ${MONEY} }
227
+ }
228
+ discountAllocations { discountedAmount { ${MONEY} } }
229
+ sellingPlanAllocation {
230
+ sellingPlan { id name description recurringDeliveries options { name value } }
231
+ checkoutChargeAmount { ${MONEY} }
232
+ priceAdjustments { price { ${MONEY} } compareAtPrice { ${MONEY} } perDeliveryPrice { ${MONEY} } }
233
+ }
234
+ merchandise {
235
+ ... on ProductVariant {
236
+ id title sku availableForSale requiresShipping ${inventoryField(access)}
237
+ image { ${IMAGE} }
238
+ price { ${MONEY} }
239
+ compareAtPrice { ${MONEY} }
240
+ selectedOptions { name value }
241
+ product { id title handle vendor featuredImage { ${IMAGE} } }
242
+ }
243
+ }
244
+ ... on ComponentizableCartLine {
245
+ lineComponents {
246
+ id quantity
247
+ merchandise { ... on ProductVariant { id title product { title handle } } }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }`;
253
+ }
254
+
255
+ /** The collection fields Tether renders, for inside a `collection { }` selection. */
256
+ export function collectionFields(): string {
257
+ return `
258
+ id handle title description descriptionHtml
259
+ image { ${IMAGE} }
260
+ seo { title description }`;
261
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What Tether reads from and sets on `window`, for sites that use the `<script>` build or `window.Tether`. Only the
3
+ * published declarations carry it (scripts/build.ts adds it to the generated `index.d.ts`): the source reads these
4
+ * names as unknown and validates them, so they stay out of its own program.
5
+ */
6
+ import type { Tether, TetherConfig } from './index.ts';
7
+
8
+ declare global {
9
+ interface Window {
10
+ Tether: Tether;
11
+ TETHER_CONFIG?: TetherConfig;
12
+ TETHER_COUNTRY?: string;
13
+ TETHER_LANGUAGE?: string;
14
+ }
15
+ }
@@ -2,25 +2,26 @@
2
2
  * Shopify images on the page: sized through the CDN's URL parameters, written to an `<img>` or as a CSS background,
3
3
  * and never downloaded twice when the server already rendered the same image.
4
4
  */
5
- import { attr } from './markup.js';
5
+ import { attr } from './markup.ts';
6
6
 
7
- /**
8
- * @typedef {object} ImageSize Shopify CDN parameters.
9
- * @property {string | number | null} [width]
10
- * @property {string | number | null} [height]
11
- * @property {string | null} [crop] `center`, `top`, `bottom`, `left` or `right`; only applies with a width or height.
12
- */
7
+ /** Shopify CDN parameters. */
8
+ export interface ImageSize {
9
+ width?: string | number | null;
10
+ height?: string | number | null;
11
+ /** `center`, `top`, `bottom`, `left` or `right`; only applies with a width or height. */
12
+ crop?: string | null;
13
+ }
13
14
 
14
15
  /**
15
16
  * Shopify CDN image at a size: `imageUrl(url, { width: 800 })`. Use the same width as the element's max-width
16
17
  * attribute, so the browser script keeps the image instead of loading it again.
17
18
  *
18
- * @template {string | null | undefined} Url
19
- * @param {Url} url
20
- * @param {ImageSize} [size]
21
- * @returns {Url} The sized URL; `url` itself when it is empty or not absolute.
19
+ * @returns The sized URL; `url` itself when it is empty or not absolute.
22
20
  */
23
- export function imageUrl(url, { width, height, crop } = {}) {
21
+ export function imageUrl<Url extends string | null | undefined>(
22
+ url: Url,
23
+ { width, height, crop }: ImageSize = {},
24
+ ): Url {
24
25
  if (!url) return url;
25
26
  let parsed;
26
27
  try {
@@ -33,59 +34,45 @@ export function imageUrl(url, { width, height, crop } = {}) {
33
34
  if (width) parsed.searchParams.set('width', String(width));
34
35
  if (height) parsed.searchParams.set('height', String(height));
35
36
  if (crop) parsed.searchParams.set('crop', crop);
36
- return /** @type {Url} */ (parsed.toString());
37
+ // `url` is a non-empty string here, so `Url` is a string type.
38
+ return parsed.toString() as Url;
37
39
  }
38
40
 
39
- /**
40
- * @param {Element} el
41
- * @param {string} name
42
- * @returns {string | null} `name` as Webflow's custom attribute, or in valid HTML as `<prefix>-name` / `data-name`.
43
- */
44
- function sizeAttribute(el, name) {
41
+ /** @returns `name` as Webflow's custom attribute, or in valid HTML as `<prefix>-name` / `data-name`. */
42
+ function sizeAttribute(el: Element, name: string): string | null {
45
43
  return el.getAttribute(name) ?? el.getAttribute(attr(name)) ?? el.getAttribute(`data-${name}`);
46
44
  }
47
45
 
48
- /**
49
- * @template {string | null | undefined} Url
50
- * @param {Url} url
51
- * @param {Element | null | undefined} el The element whose max-width / max-height / crop attributes size the image.
52
- * @returns {Url}
53
- */
54
- export function cdnUrl(url, el) {
46
+ /** @param el The element whose max-width / max-height / crop attributes size the image. */
47
+ export function cdnUrl<Url extends string | null | undefined>(url: Url, el: Element | null | undefined): Url {
55
48
  if (!url || !el) return url;
56
49
  const width = sizeAttribute(el, 'max-width');
57
50
  const height = sizeAttribute(el, 'max-height');
58
51
  return imageUrl(url, { width, height, crop: width || height ? sizeAttribute(el, 'crop') : null });
59
52
  }
60
53
 
61
- /**
62
- * @param {string | null | undefined} a
63
- * @param {string | null | undefined} b
64
- * @returns {boolean} True for the same image at any size parameters: the server-rendered one is kept.
65
- */
66
- export const sameImage = (a, b) => Boolean(a && b) && a?.split('?')[0] === b?.split('?')[0];
54
+ /** @returns The URL without its query (the size parameters). */
55
+ const withoutQuery = (url: string) => url.replace(/\?.*$/, '');
56
+
57
+ /** @returns True for the same image at any size parameters: the server-rendered one is kept. */
58
+ export function sameImage(a: string | null | undefined, b: string | null | undefined): boolean {
59
+ if (!a || !b) return false;
60
+ return withoutQuery(a) === withoutQuery(b);
61
+ }
67
62
 
68
63
  /**
69
64
  * A CSS `url()` for an image URL from Shopify. Characters that would end the string are escaped, so the URL can't
70
65
  * add declarations; only http(s), relative and data:image URLs are used.
71
- *
72
- * @param {string} url
73
66
  */
74
- function cssUrl(url) {
67
+ function cssUrl(url: string): string {
75
68
  const hasScheme = /^\s*[a-z][a-z0-9+.-]*:/i.test(url);
76
69
  if (hasScheme && !/^\s*(https?:|data:image\/)/i.test(url)) return 'none';
77
70
  const escaped = url.replace(/["\\\n\r\f]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
78
71
  return `url("${escaped}")`;
79
72
  }
80
73
 
81
- /**
82
- * Shows `url` in `el`: as the `src` of an `<img>` (lazy unless it says otherwise), else as its background image.
83
- *
84
- * @param {HTMLElement} el
85
- * @param {string | null | undefined} url
86
- * @param {string | null} [alt]
87
- */
88
- export function setImage(el, url, alt) {
74
+ /** Shows `url` in `el`: as the `src` of an `<img>` (lazy unless it says otherwise), else as its background image. */
75
+ export function setImage(el: HTMLElement, url: string | null | undefined, alt?: string | null): void {
89
76
  if (!url) return;
90
77
  const src = cdnUrl(url, el);
91
78
  if (el.tagName !== 'IMG') {