@leed-ai/cli 0.0.0-stage → 6.19.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 (263) hide show
  1. package/LICENSE.md +57 -0
  2. package/README.md +125 -2
  3. package/dist/LICENSE.md +57 -0
  4. package/dist/acorn-DAgDT1om.js +5087 -0
  5. package/dist/angular-l0_4PK8N.js +3598 -0
  6. package/dist/authShared-Dray38Ls.js +1161 -0
  7. package/dist/babel-CJeRQd5u.js +10038 -0
  8. package/dist/buildResults-tiq1nugP.js +21180 -0
  9. package/dist/chunk-1VJ9lAw7.js +42 -0
  10. package/dist/cliAuth-38JjD6hw.js +15 -0
  11. package/dist/env-B0bHiXJJ.js +29 -0
  12. package/dist/errors-CqKamGCu.js +481 -0
  13. package/dist/estree-E8WG8UHj.js +6930 -0
  14. package/dist/flow-6xaRIbPQ.js +52255 -0
  15. package/dist/glimmer-aJPjsfbT.js +7272 -0
  16. package/dist/graphql-mcgtPEiZ.js +1901 -0
  17. package/dist/html--eeea3Kl.js +6553 -0
  18. package/dist/leed.eleventy.mjs +6525 -0
  19. package/dist/leed.mjs +88952 -0
  20. package/dist/leed.sources.json +798 -0
  21. package/dist/leedLogger-Zkada5aO.js +78 -0
  22. package/dist/markdown-D73jNKwx.js +6836 -0
  23. package/dist/meriyah-CMp1nDDW.js +8932 -0
  24. package/dist/package.json +101 -0
  25. package/dist/postcss-h4fBr2Fn.js +6760 -0
  26. package/dist/prettier-CwFSKxfN.js +16821 -0
  27. package/dist/static/customer-site-worker/package.json +9 -0
  28. package/dist/static/customer-site-worker/src/assetsHydration.ts +55 -0
  29. package/dist/static/customer-site-worker/src/constants.ts +20 -0
  30. package/dist/static/customer-site-worker/src/cors.ts +120 -0
  31. package/dist/static/customer-site-worker/src/index.ts +242 -0
  32. package/dist/static/customer-site-worker/src/mcp-proxy.ts +98 -0
  33. package/dist/static/customer-site-worker/src/proxy.ts +92 -0
  34. package/dist/static/customer-site-worker/src/proxyHeaders.ts +47 -0
  35. package/dist/static/customer-site-worker/src/tier.ts +40 -0
  36. package/dist/static/customer-site-worker/src/types.ts +82 -0
  37. package/dist/static/customer-site-worker/test/functions-mock.worker.js +50 -0
  38. package/dist/static/customer-site-worker/tsconfig.json +19 -0
  39. package/dist/static/customer-site-worker/vitest.workers.config.ts +61 -0
  40. package/dist/static/install/husky/colors.mjs +5 -0
  41. package/dist/static/install/husky/hooks/commit-msg +1 -0
  42. package/dist/static/install/husky/hooks/pre-commit +1 -0
  43. package/dist/static/install/husky/hooks/pre-push +1 -0
  44. package/dist/static/install/husky/valid-command.mjs +29 -0
  45. package/dist/static/install/husky/valid-commit-msg.mjs +20 -0
  46. package/dist/static/install/package.json +10 -0
  47. package/dist/static/install/packages.txt +4 -0
  48. package/dist/static/install/tasks.json +111 -0
  49. package/dist/static/javascript/detectIncognito.js +195 -0
  50. package/dist/static/javascript/documentation.js +2781 -0
  51. package/dist/static/javascript/fp/fp.esm.js +2896 -0
  52. package/dist/static/javascript/fp/fp.js +3067 -0
  53. package/dist/static/javascript/live-search.js +986 -0
  54. package/dist/static/javascript/lunr-search.js +843 -0
  55. package/dist/static/javascript/timeme.js +451 -0
  56. package/dist/static/javascript/tracker/webmcp.js +67 -0
  57. package/dist/static/javascript/tracker/whisper.js +1 -0
  58. package/dist/static/javascript/utilities.js +819 -0
  59. package/dist/static/javascript/versions.json +74 -0
  60. package/dist/static/javascript/zoomable.js +488 -0
  61. package/dist/static/site/_data/eleventyComputed.js +228 -0
  62. package/dist/static/site/_data/leed-form-data.json +253 -0
  63. package/dist/static/site/_includes/leed/api/bits/array-type.hbs +24 -0
  64. package/dist/static/site/_includes/leed/api/bits/badges.hbs +9 -0
  65. package/dist/static/site/_includes/leed/api/bits/description.hbs +9 -0
  66. package/dist/static/site/_includes/leed/api/bits/examples.hbs +42 -0
  67. package/dist/static/site/_includes/leed/api/bits/scopes.hbs +28 -0
  68. package/dist/static/site/_includes/leed/api/object.hbs +110 -0
  69. package/dist/static/site/_includes/leed/api/parameters.hbs +11 -0
  70. package/dist/static/site/_includes/leed/api/property.hbs +153 -0
  71. package/dist/static/site/_includes/leed/api/request-body.hbs +51 -0
  72. package/dist/static/site/_includes/leed/api/responses.hbs +89 -0
  73. package/dist/static/site/_includes/leed/api/security-constraints.hbs +59 -0
  74. package/dist/static/site/_includes/leed/api/security.hbs +75 -0
  75. package/dist/static/site/_includes/leed/components/pagination.hbs +45 -0
  76. package/dist/static/site/_includes/leed/components/scroll-status-indicator-horizontal.hbs +22 -0
  77. package/dist/static/site/_includes/leed/docs/alpha/api.hbs +6 -0
  78. package/dist/static/site/_includes/leed/docs/alpha/base.hbs +26 -0
  79. package/dist/static/site/_includes/leed/docs/alpha/documentation.hbs +7 -0
  80. package/dist/static/site/_includes/leed/docs/api-article.hbs +75 -0
  81. package/dist/static/site/_includes/leed/docs/bravo/api.hbs +5 -0
  82. package/dist/static/site/_includes/leed/docs/bravo/base.hbs +31 -0
  83. package/dist/static/site/_includes/leed/docs/bravo/documentation.hbs +7 -0
  84. package/dist/static/site/_includes/leed/docs/charlie/api.hbs +6 -0
  85. package/dist/static/site/_includes/leed/docs/charlie/base.hbs +27 -0
  86. package/dist/static/site/_includes/leed/docs/charlie/documentation.hbs +10 -0
  87. package/dist/static/site/_includes/leed/docs/doc-article.hbs +40 -0
  88. package/dist/static/site/_includes/leed/docs/docs-footer.hbs +35 -0
  89. package/dist/static/site/_includes/leed/docs/docs-header.hbs +59 -0
  90. package/dist/static/site/_includes/leed/docs/document-search-modal.hbs +36 -0
  91. package/dist/static/site/_includes/leed/docs/document-search.hbs +23 -0
  92. package/dist/static/site/_includes/leed/docs/footer.hbs +12 -0
  93. package/dist/static/site/_includes/leed/docs/header/brand.hbs +19 -0
  94. package/dist/static/site/_includes/leed/docs/header/chrome.hbs +33 -0
  95. package/dist/static/site/_includes/leed/docs/header/compact.hbs +19 -0
  96. package/dist/static/site/_includes/leed/docs/header/cta.hbs +15 -0
  97. package/dist/static/site/_includes/leed/docs/header/nav.hbs +18 -0
  98. package/dist/static/site/_includes/leed/docs/header/top-search.hbs +15 -0
  99. package/dist/static/site/_includes/leed/docs/mini-bar.hbs +53 -0
  100. package/dist/static/site/_includes/leed/docs/pagination.hbs +19 -0
  101. package/dist/static/site/_includes/leed/docs/shell.hbs +56 -0
  102. package/dist/static/site/_includes/leed/docs/sidebar.hbs +52 -0
  103. package/dist/static/site/_includes/leed/docs/social-icons.hbs +81 -0
  104. package/dist/static/site/_includes/leed/docs/style.hbs +17 -0
  105. package/dist/static/site/_includes/leed/docs/toc.hbs +7 -0
  106. package/dist/static/site/_includes/leed/form.hbs +266 -0
  107. package/dist/static/site/_includes/leed/head.hbs +67 -0
  108. package/dist/static/site/_includes/leed/menu/builder.hbs +35 -0
  109. package/dist/static/site/_includes/leed/menu/item.hbs +67 -0
  110. package/dist/static/site/_includes/leed/menu/link.hbs +59 -0
  111. package/dist/static/site/_includes/leed/metadata/ogCard.hbs +40 -0
  112. package/dist/static/site/_includes/leed/metadata/siteMetadata.hbs +24 -0
  113. package/dist/static/site/_includes/leed/metadata/twitterCard.hbs +34 -0
  114. package/dist/static/site/_includes/leed/popup-video.hbs +21 -0
  115. package/dist/static/site/_includes/leed/stub-template.hbs +17 -0
  116. package/dist/static/site/_includes/leed/unsubscribe-form.hbs +30 -0
  117. package/dist/static/site/_layouts/leed-documentation.hbs +12 -0
  118. package/dist/static/site/leed-feed-atom.hbs +42 -0
  119. package/dist/static/site/leed-feed-json.hbs +56 -0
  120. package/dist/static/site/leed-feed-rss.hbs +39 -0
  121. package/dist/static/site/leed-headers.hbs +38 -0
  122. package/dist/static/site/leed-redirects.hbs +10 -0
  123. package/dist/static/site/leed-sitemap-pageType.hbs +28 -0
  124. package/dist/static/site/leed-sitemap.hbs +19 -0
  125. package/dist/static/site/leed-unsubscribe.hbs +44 -0
  126. package/dist/static/site/leed-unsubscribed.hbs +40 -0
  127. package/dist/static/tailwind/components/alerts.css +132 -0
  128. package/dist/static/tailwind/components/api-methods.css +39 -0
  129. package/dist/static/tailwind/components/breadcrumbs.css +31 -0
  130. package/dist/static/tailwind/components/code.css +88 -0
  131. package/dist/static/tailwind/components/collapsible.css +38 -0
  132. package/dist/static/tailwind/components/document-search.css +222 -0
  133. package/dist/static/tailwind/components/form.css +185 -0
  134. package/dist/static/tailwind/components/kbd.css +7 -0
  135. package/dist/static/tailwind/components/pagination.css +81 -0
  136. package/dist/static/tailwind/components/scroll-status-indicator.css +5 -0
  137. package/dist/static/tailwind/components/tab-groups.css +117 -0
  138. package/dist/static/tailwind/components/tables.css +0 -0
  139. package/dist/static/tailwind/components/toc.css +60 -0
  140. package/dist/static/tailwind/components.css +248 -0
  141. package/dist/static/tailwind/defaults.css +260 -0
  142. package/dist/static/tailwind/docs/api.css +701 -0
  143. package/dist/static/tailwind/docs/components/blockquote.css +5 -0
  144. package/dist/static/tailwind/docs/components/button.css +11 -0
  145. package/dist/static/tailwind/docs/components/docs-pagination.css +71 -0
  146. package/dist/static/tailwind/docs/components/mobile-bar.css +83 -0
  147. package/dist/static/tailwind/docs/components/navigation.css +279 -0
  148. package/dist/static/tailwind/docs/components/table.css +54 -0
  149. package/dist/static/tailwind/docs/components.css +6 -0
  150. package/dist/static/tailwind/docs/docs.css +427 -0
  151. package/dist/static/tailwind/docs/layouts/alpha.css +108 -0
  152. package/dist/static/tailwind/docs/layouts/bravo.css +218 -0
  153. package/dist/static/tailwind/docs/layouts/charlie.css +105 -0
  154. package/dist/static/tailwind/docs/layouts.css +3 -0
  155. package/dist/static/tailwind/docs/themes/amber.css +113 -0
  156. package/dist/static/tailwind/docs/themes/blue.css +94 -0
  157. package/dist/static/tailwind/docs/themes/cyan.css +111 -0
  158. package/dist/static/tailwind/docs/themes/emerald.css +111 -0
  159. package/dist/static/tailwind/docs/themes/fuchsia.css +111 -0
  160. package/dist/static/tailwind/docs/themes/green.css +95 -0
  161. package/dist/static/tailwind/docs/themes/indigo.css +111 -0
  162. package/dist/static/tailwind/docs/themes/lime.css +111 -0
  163. package/dist/static/tailwind/docs/themes/orange.css +111 -0
  164. package/dist/static/tailwind/docs/themes/pink.css +111 -0
  165. package/dist/static/tailwind/docs/themes/purple.css +111 -0
  166. package/dist/static/tailwind/docs/themes/red.css +111 -0
  167. package/dist/static/tailwind/docs/themes/rose.css +111 -0
  168. package/dist/static/tailwind/docs/themes/sky.css +111 -0
  169. package/dist/static/tailwind/docs/themes/teal.css +111 -0
  170. package/dist/static/tailwind/docs/themes/violet.css +111 -0
  171. package/dist/static/tailwind/docs/themes/yellow.css +111 -0
  172. package/dist/static/tailwind/docs/themes.css +84 -0
  173. package/dist/static/tailwind/font-awesome/free-7.3.1/brands.css +2455 -0
  174. package/dist/static/tailwind/font-awesome/free-7.3.1/fontawesome.css +8997 -0
  175. package/dist/static/tailwind/font-awesome/free-7.3.1/fonts.css +45 -0
  176. package/dist/static/tailwind/font-awesome/free-7.3.1/icons.json +2608 -0
  177. package/dist/static/tailwind/font-awesome/free-7.3.1.css +3 -0
  178. package/dist/static/tailwind/font-awesome/pro-7.0.0/brands.css +2215 -0
  179. package/dist/static/tailwind/font-awesome/pro-7.0.0/fontawesome.css +19281 -0
  180. package/dist/static/tailwind/font-awesome/pro-7.0.0/fonts.css +445 -0
  181. package/dist/static/tailwind/font-awesome/pro-7.0.0.css +3 -0
  182. package/dist/static/tailwind/font-awesome/pro-7.3.1/brands.css +2455 -0
  183. package/dist/static/tailwind/font-awesome/pro-7.3.1/fontawesome.css +20365 -0
  184. package/dist/static/tailwind/font-awesome/pro-7.3.1/fonts.css +589 -0
  185. package/dist/static/tailwind/font-awesome/pro-7.3.1.css +3 -0
  186. package/dist/static/tailwind/fonts/all.css +6 -0
  187. package/dist/static/tailwind/fonts/config.jsonc +31 -0
  188. package/dist/static/tailwind/fonts/geist-mono.css +142 -0
  189. package/dist/static/tailwind/fonts/geist.css +142 -0
  190. package/dist/static/tailwind/fonts/inter.css +215 -0
  191. package/dist/static/tailwind/fonts/jetbrains-mono.css +126 -0
  192. package/dist/static/tailwind/fonts/noto-sans.css +134 -0
  193. package/dist/static/tailwind/fonts/open-sans.css +89 -0
  194. package/dist/static/tailwind/highlighters.css +14 -0
  195. package/dist/static/tailwind/hljs/README.md +53 -0
  196. package/dist/static/tailwind/hljs/a11y.css +96 -0
  197. package/dist/static/tailwind/hljs/atom-fruit.css +90 -0
  198. package/dist/static/tailwind/hljs/atom-one.css +164 -0
  199. package/dist/static/tailwind/hljs/default.css +164 -0
  200. package/dist/static/tailwind/hljs/github.css +101 -0
  201. package/dist/static/tailwind/hljs/google-code.css +112 -0
  202. package/dist/static/tailwind/hljs/grayscale.css +115 -0
  203. package/dist/static/tailwind/hljs/gruvbox-hard.css +115 -0
  204. package/dist/static/tailwind/hljs/gruvbox-medium.css +115 -0
  205. package/dist/static/tailwind/hljs/gruvbox-soft.css +115 -0
  206. package/dist/static/tailwind/hljs/papercolor.css +120 -0
  207. package/dist/static/tailwind/hljs/qt-creator.css +91 -0
  208. package/dist/static/tailwind/hljs/summerfruit.css +115 -0
  209. package/dist/static/tailwind/hljs/tomorrow.css +115 -0
  210. package/dist/static/tailwind/leed.css +170 -0
  211. package/dist/static/tailwind/site-tokens.reference.css +105 -0
  212. package/dist/static/tailwind/utils/icons.css +102 -0
  213. package/dist/static/tailwind/utils/utilities.css +105 -0
  214. package/dist/static/templates/pages/PAGE.hbs +27 -0
  215. package/dist/static/templates/pages/PAGE_LAYOUT.hbs +19 -0
  216. package/dist/static/templates/pages/PAGE_TYPE_INDEX.hbs +89 -0
  217. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_ITEM.hbs +52 -0
  218. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_LIST.hbs +39 -0
  219. package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_PAGINATION.hbs +11 -0
  220. package/dist/static/templates/raw-content/.ci/build.sh +58 -0
  221. package/dist/static/templates/raw-content/README.md +179 -0
  222. package/dist/static/templates/raw-content/_gitignore +50 -0
  223. package/dist/static/templates/raw-content/src/404.hbs +21 -0
  224. package/dist/static/templates/raw-content/src/_data/autolink.json +1 -0
  225. package/dist/static/templates/raw-content/src/_data/ctaList.json +1 -0
  226. package/dist/static/templates/raw-content/src/_data/leedForms.json +1 -0
  227. package/dist/static/templates/raw-content/src/_data/menu.json +1 -0
  228. package/dist/static/templates/raw-content/src/_includes/footer.hbs +18 -0
  229. package/dist/static/templates/raw-content/src/_includes/header-includes.hbs +25 -0
  230. package/dist/static/templates/raw-content/src/_includes/header.hbs +5 -0
  231. package/dist/static/templates/raw-content/src/_includes/site-template.hbs +23 -0
  232. package/dist/static/templates/raw-content/src/_includes/unsubscribe-DISABLED.hbs +17 -0
  233. package/dist/static/templates/raw-content/src/_includes/unsubscribed-DISABLED.hbs +13 -0
  234. package/dist/static/templates/raw-content/src/_layouts/empty.hbs +2 -0
  235. package/dist/static/templates/raw-content/src/index.hbs +21 -0
  236. package/dist/static/templates/raw-content/src/robots.hbs +9 -0
  237. package/dist/static/templates/raw-content/tailwind/site/base.css +44 -0
  238. package/dist/static/templates/raw-content/tailwind/site.config.css +3 -0
  239. package/dist/static/templates/sandbox/_gitignore +50 -0
  240. package/dist/static/templates/sandbox/identity.json +1 -0
  241. package/dist/static/templates/sandbox/src/404.hbs +21 -0
  242. package/dist/static/templates/sandbox/src/_data/autolink.json +1 -0
  243. package/dist/static/templates/sandbox/src/_data/ctaList.json +1 -0
  244. package/dist/static/templates/sandbox/src/_data/labelList.json +9 -0
  245. package/dist/static/templates/sandbox/src/_data/leedForms.json +20 -0
  246. package/dist/static/templates/sandbox/src/_data/menu.json +19 -0
  247. package/dist/static/templates/sandbox/src/_data/pageTypeList.json +14 -0
  248. package/dist/static/templates/sandbox/src/_data/searchIndexList.json +1 -0
  249. package/dist/static/templates/sandbox/src/_data/userList.json +11 -0
  250. package/dist/static/templates/sandbox/src/_includes/footer.hbs +18 -0
  251. package/dist/static/templates/sandbox/src/_includes/header-includes.hbs +25 -0
  252. package/dist/static/templates/sandbox/src/_includes/header.hbs +5 -0
  253. package/dist/static/templates/sandbox/src/_includes/site-template.hbs +23 -0
  254. package/dist/static/templates/sandbox/src/_layouts/empty.hbs +2 -0
  255. package/dist/static/templates/sandbox/src/index.hbs +20 -0
  256. package/dist/static/templates/sandbox/src/robots.hbs +9 -0
  257. package/dist/static/templates/sandbox/src/src.11tydata.json +39 -0
  258. package/dist/static/templates/sandbox/tailwind/site/base.css +44 -0
  259. package/dist/static/templates/sandbox/tailwind/site.config.css +3 -0
  260. package/dist/thrown-sukVkp_1.js +113 -0
  261. package/dist/typescript-D4T0V_Hn.js +23004 -0
  262. package/dist/yaml-9wR8GxF_.js +5236 -0
  263. package/package.json +157 -4
@@ -0,0 +1,819 @@
1
+ window.KNOWN_USER_CHECK = undefined;
2
+ const downloadState = "downloading";
3
+ const thanksState = "thanks";
4
+ const errorState = "error";
5
+ const emailSentState = "email-sent";
6
+ const sendingState = "sending";
7
+ const submitState = "submit";
8
+
9
+ // duplicated from data-types/src/util/index
10
+ function cleanRecord(record) {
11
+ const result = {};
12
+ Object.assign(result, record);
13
+ Object.keys(result).forEach((key) => {
14
+ if (result[key] == null || result[key] === "" || result[key] === "null") {
15
+ delete result[key];
16
+ }
17
+ });
18
+ return result;
19
+ }
20
+
21
+ function activeFormElement(formId, elementName) {
22
+ const stateSubmit = document.getElementById(`${formId}-button-${submitState}`);
23
+ const stateSending = document.getElementById(`${formId}-button-${sendingState}`);
24
+ const stateEmailSent = document.getElementById(`${formId}-button-${emailSentState}`);
25
+ const stateError = document.getElementById(`${formId}-button-${errorState}`);
26
+ const stateThanks = document.getElementById(`${formId}-button-${thanksState}`);
27
+ const stateDownloading = document.getElementById(`${formId}-button-${downloadState}`);
28
+ const states = [stateSubmit, stateSending, stateEmailSent, stateError, stateThanks, stateDownloading];
29
+
30
+ let activeElement;
31
+ states.forEach((element) => {
32
+ element.style.display = "none";
33
+ if (element.id.endsWith(elementName)) {
34
+ activeElement = element;
35
+ }
36
+ });
37
+ stateDownloading.style.setProperty("display", "none", "important");
38
+
39
+ activeElement.style.display = "block";
40
+ }
41
+
42
+ function leedEnableFormButton(event) {
43
+ const formId = event.currentTarget.id;
44
+ document.getElementById(`${formId}-button`).disabled = false;
45
+ }
46
+
47
+ function emailChangeListener() {
48
+ const formId = this.form.id;
49
+ activeFormElement(formId, submitState);
50
+ }
51
+
52
+ async function downloadFile(url, filename) {
53
+ await fetch(url)
54
+ .then((response) => response.blob())
55
+ .then((blob) => {
56
+ const a = document.createElement("a");
57
+ a.href = URL.createObjectURL(blob);
58
+ a.download = filename;
59
+ document.body.appendChild(a);
60
+ a.click();
61
+ document.body.removeChild(a);
62
+ })
63
+ .catch((error) => {
64
+ console.error("Error downloading file:", error);
65
+ });
66
+ }
67
+
68
+ async function leedFormSubmit(event) {
69
+ event.preventDefault();
70
+ // this is a shared promise. needs to finish before we call services.
71
+ await leedSessionReady("form_fill");
72
+
73
+ const formId = event.srcElement.id;
74
+ const formButton = document.getElementById(`${formId}-button`);
75
+
76
+ /**
77
+ * We disable the button to prevent double/triple clicks.
78
+ * If successful we leave the button disabled, and only
79
+ * reenable on change to the form for resubmission.
80
+ */
81
+ formButton.disabled = true;
82
+ activeFormElement(formId, sendingState);
83
+
84
+ const fd = new FormData(event.srcElement);
85
+ const data = cleanRecord(Object.fromEntries(fd.entries()));
86
+ let timezone;
87
+ try {
88
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
89
+ } catch (_e) {
90
+ // Older browsers may not support Intl.DateTimeFormat — omit timezone.
91
+ }
92
+ Object.assign(data, {
93
+ knownUser: window.KNOWN_USER_CHECK?.known || false,
94
+ formId,
95
+ pageId: pid, // declared in each page
96
+ pageTypeId: pt, // declared in each page
97
+ timezone,
98
+ });
99
+
100
+ const response = await fetch("/api/clerk", {
101
+ method: "POST",
102
+ body: JSON.stringify(data),
103
+ headers: { "content-type": "application/json" },
104
+ });
105
+
106
+ const s = event.srcElement.dataset.successcallback;
107
+ const f = event.srcElement.dataset.failurecallback;
108
+ if (response.status === 204) {
109
+ activeFormElement(formId, thanksState);
110
+ } else if (response.status === 200) {
111
+ if (response.body) {
112
+ const result = await response.json();
113
+ if (result?.url && result.filename) {
114
+ activeFormElement(formId, downloadState);
115
+ await downloadFile(result.url, result.filename);
116
+ activeFormElement(formId, thanksState);
117
+ } else if (result?.message && result.message === "sent") {
118
+ activeFormElement(formId, emailSentState);
119
+ } else {
120
+ activeFormElement(formId, thanksState);
121
+ }
122
+ } else {
123
+ activeFormElement(formId, thanksState);
124
+ }
125
+
126
+ if (s && window[s] && typeof window[s] === "function") {
127
+ console.log("Success callback...");
128
+ window[s].apply(null, [formId]);
129
+ }
130
+ } else if (response.status === 422) {
131
+ activeFormElement(formId, errorState);
132
+
133
+ if (f && window[f] && typeof window[f] === "function") {
134
+ console.log("Turnstile failure...");
135
+ window[f].apply(null, [formId]);
136
+ }
137
+ formButton.disabled = false;
138
+ } else {
139
+ activeFormElement(formId, errorState);
140
+
141
+ if (f && window[f] && typeof window[f] === "function") {
142
+ console.log("Failure callback...");
143
+ window[f].apply(null, [formId]);
144
+ }
145
+ formButton.disabled = false;
146
+ }
147
+ }
148
+
149
+ async function leedAutofill(formId) {
150
+ if (!window.KNOWN_USER_CHECK) {
151
+ // this is a shared promise. needs to finish before we call services.
152
+ await leedSessionReady("pageview");
153
+
154
+ // console.log("Checking if this is a known user...");
155
+ try {
156
+ const resp = await fetch(`/api/poirot`);
157
+ window.KNOWN_USER_CHECK = await resp.json();
158
+ } catch (e) {
159
+ window.KNOWN_USER_CHECK = { known: false };
160
+ }
161
+ }
162
+
163
+ if (window?.KNOWN_USER_CHECK?.known) {
164
+ const record = window?.KNOWN_USER_CHECK;
165
+ const form = document.getElementById(formId);
166
+
167
+ /**
168
+ * If fields are configured to be hidden, we hide them based on how the user is known.
169
+ */
170
+ if (record.details.source != null && record.details.source !== "form") {
171
+ const hideable = form.querySelectorAll("div.known-valid-hideable");
172
+ for (let i = 0; i < hideable.length; i++) {
173
+ const div = hideable[i];
174
+ const all = div.querySelectorAll("*");
175
+ Array.from(all).forEach((element) => {
176
+ if (element.hasAttribute("required")) {
177
+ element.required = false;
178
+ }
179
+ });
180
+ div.style.display = "none";
181
+ }
182
+ }
183
+
184
+ const details = Object.keys(record.details).reduce((acc, key) => {
185
+ acc[key.toLowerCase()] = record.details[key];
186
+ return acc;
187
+ }, {});
188
+
189
+ /**
190
+ * Iterate over all the fields in the form to grab any fillable values.
191
+ */
192
+ const wbDiv = document.getElementById(`${formId}-wb`);
193
+ let message = wbDiv.innerHTML;
194
+ const formElements = form.elements;
195
+ for (let i = 0; i < formElements.length; i++) {
196
+ const formElement = formElements[i];
197
+ const key = formElement.name;
198
+ if (key) {
199
+ /**
200
+ * We only populate the fields if they were known by a non-validated
201
+ * form fill, because we still want them to update fields.
202
+ */
203
+ if (record.details.source === "form" && details[key]) {
204
+ formElement.value = details[key];
205
+ }
206
+
207
+ /**
208
+ * Build the welcome back message. If a value is empty
209
+ * it will just get replaced with the empty value so the
210
+ * string should still format well.
211
+ */
212
+ const tK = `{{${key}}}`;
213
+ const newValue = details[key] ? details[key] : "";
214
+ message = message.replace(tK, newValue);
215
+ }
216
+ }
217
+ if (message !== "") {
218
+ wbDiv.innerHTML = message;
219
+ wbDiv.style.display = "block";
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Wrap this in onload to ensure that it happens after the page has loaded.
226
+ */
227
+ function setupLeedForms() {
228
+ const forms = document.getElementsByClassName("LeedForm");
229
+ for (let i = 0; i < forms.length; i++) {
230
+ const form = forms.item(i);
231
+ form.onsubmit = leedFormSubmit;
232
+ form.oninput = leedEnableFormButton;
233
+
234
+ /**
235
+ * Find the email field, add reset functionality in case they
236
+ * want to change their email address and resubmit the form.
237
+ */
238
+ const emailField = document.getElementById(`${form.id}-email`);
239
+ if (emailField) {
240
+ emailField.addEventListener("input", emailChangeListener, false);
241
+ }
242
+ }
243
+ }
244
+
245
+ window.addEventListener("load", setupLeedForms);
246
+
247
+ function submitLeadUnsubscribe() {
248
+ const form = document.getElementById("unsubscribeForm");
249
+ const reason = form.elements.reason.value;
250
+
251
+ let params = `?reason=${reason}`;
252
+ const doNotContact = form.elements.donotcontact.checked;
253
+ if (doNotContact) {
254
+ params += "&donotcontact=true";
255
+ }
256
+
257
+ // This is the unsubscribe page, and we want to submit to the out page.
258
+ const url = window.location.pathname.replace(".unsubscribe", ".out") + params;
259
+
260
+ window.location.href = url;
261
+ return false;
262
+ }
263
+
264
+ async function fetchJson(endpointName) {
265
+ const resp = await fetch(`/api/${endpointName}/v2/${pid}`); // pid declared in each page
266
+ if (resp.status !== 200) return null;
267
+ try {
268
+ return await resp.json();
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Stamp the click-attribution reason onto every anchor in `container`, replacing
276
+ * the build-time `addAnchorReason` shortcode. This has to stay client-side:
277
+ * utm_content derives from each anchor's rendered innerText, which HTMLRewriter
278
+ * cannot see at the point it could still set an attribute.
279
+ *
280
+ * Pass the wrapper or the cloned DocumentFragment, never the stub anchor itself
281
+ * — `element.querySelectorAll("a")` does not match the element it is called on,
282
+ * and a stub template's root IS the `<a>`. A fragment's does include it.
283
+ *
284
+ * The reason string is parsed server-side as a URL query (`addUrchinParams`), so
285
+ * the link text is encoded — an unencoded `&` or `=` in a title like
286
+ * "Q&A: Getting Started" would otherwise split into bogus params. It is also
287
+ * collapsed and capped: a stub's anchor wraps the WHOLE card, so its text is the
288
+ * title, date, summary, labels and authors run together.
289
+ */
290
+ const UTM_CONTENT_MAX = 100;
291
+
292
+ /**
293
+ * Cap the link text and percent-encode it for the reason query string.
294
+ *
295
+ * Array.from iterates a string by CODE POINT, so the cap can never land between
296
+ * the halves of a surrogate pair. `.slice()` counts UTF-16 code units and can:
297
+ * an emoji straddling the cap leaves a lone surrogate, encodeURIComponent
298
+ * throws URIError on one, and applyReason runs synchronously inside the
299
+ * per-item loop — so a single emoji would drop every remaining card. The catch
300
+ * covers link text that arrived unpaired to begin with: lose utm_content, never
301
+ * the card.
302
+ */
303
+ function utmContent(text) {
304
+ const capped = Array.from(text).slice(0, UTM_CONTENT_MAX).join("");
305
+ try {
306
+ return encodeURIComponent(capped);
307
+ } catch (_e) {
308
+ return "";
309
+ }
310
+ }
311
+
312
+ function applyReason(container, medium, term) {
313
+ const base = `utm_campaign=leed&utm_source=internal&utm_medium=${medium}&utm_term=${term}`;
314
+ container.querySelectorAll("a").forEach((a) => {
315
+ const content = utmContent((a.innerText || "").replace(/\s+/g, " ").trim());
316
+ a.dataset.reason = content ? `${base}&utm_content=${content}` : base;
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Render a date the way the build-time `{{#date "default"}}` shortcode did.
322
+ *
323
+ * The stub used to be rendered by Eleventy, which resolved the company/page-type
324
+ * dateFormat in the site's timezone via dayjs. Moving the card to the client
325
+ * moved that off the server, and `toLocaleDateString()` would give a different
326
+ * string per visitor locale and timezone — visibly unlike every other date on
327
+ * the page. The `<template>` is emitted at build time, so it carries the site's
328
+ * `dateFormat`/`timezone` and these format from those instead. Defaults match
329
+ * `plugins/dates.ts` exactly.
330
+ *
331
+ * Tokens are the dayjs date/time set. Anything unrecognized passes through, and
332
+ * `[...]` escapes a literal, both as dayjs does.
333
+ */
334
+ const DATE_MONTHS = [
335
+ "January",
336
+ "February",
337
+ "March",
338
+ "April",
339
+ "May",
340
+ "June",
341
+ "July",
342
+ "August",
343
+ "September",
344
+ "October",
345
+ "November",
346
+ "December",
347
+ ];
348
+ const DATE_TOKENS = /\[([^\]]*)\]|YYYY|YY|MMMM|MMM|MM|M|dddd|ddd|dd|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
349
+
350
+ function zonedParts(when, timezone) {
351
+ const options = {
352
+ year: "numeric",
353
+ month: "2-digit",
354
+ day: "2-digit",
355
+ hour: "2-digit",
356
+ minute: "2-digit",
357
+ second: "2-digit",
358
+ weekday: "long",
359
+ hourCycle: "h23",
360
+ };
361
+ let formatted;
362
+ try {
363
+ formatted = new Intl.DateTimeFormat("en-US", { ...options, timeZone: timezone }).formatToParts(when);
364
+ } catch (_e) {
365
+ // Intl throws RangeError on an unknown IANA zone; the build-time helper
366
+ // defaults to UTC, so match it rather than dropping the date.
367
+ formatted = new Intl.DateTimeFormat("en-US", { ...options, timeZone: "UTC" }).formatToParts(when);
368
+ }
369
+ const parts = {};
370
+ formatted.forEach((part) => {
371
+ parts[part.type] = part.value;
372
+ });
373
+ return parts;
374
+ }
375
+
376
+ function formatCardDate(when, dateFormat, timezone) {
377
+ const parts = zonedParts(when, timezone || "UTC");
378
+ const month = DATE_MONTHS[Number(parts.month) - 1];
379
+ const hour24 = Number(parts.hour);
380
+ const hour12 = hour24 % 12 || 12;
381
+ const values = {
382
+ YYYY: parts.year,
383
+ YY: parts.year.slice(-2),
384
+ MMMM: month,
385
+ MMM: month.slice(0, 3),
386
+ MM: parts.month,
387
+ M: String(Number(parts.month)),
388
+ dddd: parts.weekday,
389
+ ddd: parts.weekday.slice(0, 3),
390
+ dd: parts.weekday.slice(0, 2),
391
+ DD: parts.day,
392
+ D: String(Number(parts.day)),
393
+ HH: parts.hour,
394
+ H: String(hour24),
395
+ hh: String(hour12).padStart(2, "0"),
396
+ h: String(hour12),
397
+ mm: parts.minute,
398
+ m: String(Number(parts.minute)),
399
+ ss: parts.second,
400
+ s: String(Number(parts.second)),
401
+ A: hour24 < 12 ? "AM" : "PM",
402
+ a: hour24 < 12 ? "am" : "pm",
403
+ };
404
+ return (dateFormat || "YYYY-MM-DD").replace(DATE_TOKENS, (token, literal) =>
405
+ literal === undefined ? values[token] : literal,
406
+ );
407
+ }
408
+
409
+ /**
410
+ * Reading time in whole minutes, the client-side twin of the `readingTime`
411
+ * shortcode — the stub used to render it at build time and would otherwise lose
412
+ * it. The arithmetic is ported verbatim, including the 200 wpm fallback, which
413
+ * is deliberately NOT the config default of 250 (`defaults.ts`): the shortcode
414
+ * only falls back when `readingWpm` is null or 0, and collapsing the two
415
+ * numbers would silently change displayed reading times on live sites.
416
+ *
417
+ * Returns "" for a page with no word count, so the falsy-slot rule removes the
418
+ * element the way the old `{{#if list.data.wordCount}}` guard did. Never zero —
419
+ * a countable page always reads as at least one minute.
420
+ */
421
+ function readingMinutes(wordCount, readingWpm) {
422
+ if (!wordCount) return "";
423
+ const wpm = readingWpm != null && readingWpm !== 0 ? readingWpm : 200;
424
+ const t = Math.floor(wordCount / wpm);
425
+ return String(t > 0 ? t : 1);
426
+ }
427
+
428
+ /**
429
+ * Cloudflare Images serves a URL as `/cdn-cgi/imagedelivery/<hash>/<id>/<variant>`,
430
+ * where the variant is the LAST path segment. Recommendation payloads carry the
431
+ * page's `featureImage` at whatever variant was indexed -- in practice
432
+ * `original`, which is up to 5120px for a card that renders a few hundred wide.
433
+ *
434
+ * The variant is swapped on the segment rather than by replacing the string
435
+ * "original" anywhere in the URL, which would corrupt any id that happens to
436
+ * contain it. Any query or hash is preserved.
437
+ *
438
+ * A template opts in with `data-leed-variant`; `medium` is used when it says
439
+ * nothing, so a site that ejected `stub-template.hbs` before this existed still
440
+ * stops shipping full-size images.
441
+ */
442
+ const CDN_IMAGE_PATH = "/cdn-cgi/imagedelivery/";
443
+ const DEFAULT_CARD_VARIANT = "medium";
444
+
445
+ function cardImageUrl(url, variant) {
446
+ const name = variant || DEFAULT_CARD_VARIANT;
447
+ // Only a Cloudflare Images URL has a variant segment to swap.
448
+ if (typeof url !== "string" || !url.includes(CDN_IMAGE_PATH)) return url;
449
+
450
+ const cut = url.search(/[?#]/);
451
+ const base = cut === -1 ? url : url.slice(0, cut);
452
+ const suffix = cut === -1 ? "" : url.slice(cut);
453
+
454
+ const lastSlash = base.lastIndexOf("/");
455
+ // No segment after the id means there is no variant to replace.
456
+ if (lastSlash === -1 || lastSlash === base.length - 1) return url;
457
+
458
+ return `${base.slice(0, lastSlash + 1)}${name}${suffix}`;
459
+ }
460
+
461
+ /**
462
+ * Fill one cloned stub template from a recommendation object.
463
+ *
464
+ * `[data-leed-each="key"]` on a nested <template> repeats its content once per
465
+ * array element; `[data-leed-slot="key"]` receives a single value. A falsy value
466
+ * removes its slot element, so a page with no featureImage renders no empty
467
+ * <img>. Values are always written with textContent, never innerHTML — the
468
+ * payload is assembled from Vectorize metadata and D1/KV records and is
469
+ * untrusted at the DOM boundary.
470
+ *
471
+ * `dates` carries the site's `{ dateFormat, timezone }` off the build-time
472
+ * <template>, so a <time> slot renders the same string the rest of the page
473
+ * does rather than the visitor's locale default.
474
+ */
475
+ function fillSlots(root, item, dates) {
476
+ // Slots FIRST, repeats second. querySelectorAll does not descend into a
477
+ // <template>'s content, so while the repeated rows are still inside their
478
+ // <template> host this pass cannot see their slots — which is exactly what we
479
+ // want, because those belong to a row object, not to `item`. Run the repeat
480
+ // pass first and this pass would find the row slots after they were spliced
481
+ // in, look them up against `item`, miss, and delete every one of them.
482
+ root.querySelectorAll("[data-leed-slot]").forEach((el) => {
483
+ const value = item[el.dataset.leedSlot];
484
+ if (!value) {
485
+ el.remove();
486
+ return;
487
+ }
488
+ if (el.tagName === "IMG") {
489
+ el.src = cardImageUrl(value, el.dataset.leedVariant);
490
+ if (!el.alt) el.alt = item.title || "";
491
+ } else if (el.tagName === "A") {
492
+ el.href = value;
493
+ } else if (el.tagName === "TIME" && typeof value === "number") {
494
+ // The payload's only instant is `publishedAt`, an epoch number — so a
495
+ // NUMBER in a <time> is a timestamp and gets datetime + a formatted date.
496
+ // A <time> fed anything else (reading time, say) falls through to text,
497
+ // which is exactly what the build-time stub rendered into it.
498
+ const when = new Date(value);
499
+ // An out-of-range value would make toISOString() throw, rejecting
500
+ // recommendations() mid-loop so the remaining cards never render.
501
+ if (Number.isNaN(when.getTime())) {
502
+ el.remove();
503
+ return;
504
+ }
505
+ el.dateTime = when.toISOString();
506
+ // A template can pin its own format with `data-leed-date-format` — a card
507
+ // is a compact surface, and the site-wide `dateFormat` is often authored
508
+ // for a full article header (a time, a timezone) that reads as noise next
509
+ // to "5 min read". Falls back to the site format when unset, so a template
510
+ // that predates this keeps rendering exactly what it did.
511
+ el.textContent = formatCardDate(when, el.dataset.leedDateFormat || dates?.dateFormat, dates?.timezone);
512
+ } else {
513
+ el.textContent = value;
514
+ }
515
+ });
516
+
517
+ root.querySelectorAll("[data-leed-each]").forEach((host) => {
518
+ const rows = item[host.dataset.leedEach];
519
+ if (!Array.isArray(rows) || rows.length === 0) {
520
+ host.remove();
521
+ return;
522
+ }
523
+ const filled = document.createDocumentFragment();
524
+ rows.forEach((row) => {
525
+ const clone = host.content.cloneNode(true);
526
+ fillSlots(clone, row, dates);
527
+ filled.appendChild(clone);
528
+ });
529
+ host.replaceWith(filled);
530
+ });
531
+ }
532
+
533
+ /**
534
+ * Hide the widget when there is nothing to recommend.
535
+ *
536
+ * The `<id>-container` wrapper is the long-standing convention of the id-based
537
+ * API — it holds the heading as well as the cards, so hiding it is what stops a
538
+ * bare "Related reading" heading sitting over an empty box. A declarative host
539
+ * has no such convention, so the host itself is hidden.
540
+ */
541
+ function hideEmptyRecommendations(target) {
542
+ const container = (target.id && document.getElementById(`${target.id}-container`)) || target;
543
+ // setProperty with "important" so the hide beats an `!important` display class
544
+ // on the container (e.g. Tailwind's `block!`), which a plain inline style
545
+ // cannot override.
546
+ container.style.setProperty("display", "none", "important");
547
+ container.appendChild(document.createComment("No recommendations located - container was hidden"));
548
+ }
549
+
550
+ /** Render the recommendation cards into `target`. */
551
+ async function renderRecommendations(target) {
552
+ // this is a shared promise. needs to finish before we call services.
553
+ await leedSessionReady("pageview");
554
+ const items = await fetchJson("abby");
555
+ const template = document.getElementById("leed-stub-template");
556
+
557
+ if (!Array.isArray(items) || items.length === 0 || !template) {
558
+ hideEmptyRecommendations(target);
559
+ return;
560
+ }
561
+
562
+ // Stamped onto the <template> at build time by the stub-template transform,
563
+ // so card dates and reading times match what the rest of the site renders.
564
+ // `|| 0` normalizes a missing or non-numeric attribute to the "unset" value
565
+ // readingMinutes falls back from.
566
+ const dates = { dateFormat: template.dataset.dateFormat, timezone: template.dataset.timezone };
567
+ const readingWpm = Number(template.dataset.readingWpm) || 0;
568
+ items.forEach((item) => {
569
+ const stub = template.content.cloneNode(true);
570
+ // readingTime is derived, not carried on the payload — wordCount is.
571
+ fillSlots(stub, { ...item, readingTime: readingMinutes(item.wordCount, readingWpm) }, dates);
572
+ // On the fragment, so the template's root anchor is included.
573
+ applyReason(stub, "recommendation", item.pageId);
574
+ // Captured before the append empties the fragment. Inert while stubs carry
575
+ // no data-id — giving them one would change ClickTracker's attribution for
576
+ // stub clicks, which is out of scope.
577
+ const root = stub.firstElementChild;
578
+ target.appendChild(stub);
579
+ window.leedObserveElement?.(root);
580
+ });
581
+ }
582
+
583
+ /**
584
+ * Deprecated id-based entry point, kept working for sites that call it directly.
585
+ * The declarative `data-leed-recommendations` attribute is the supported API.
586
+ */
587
+ async function recommendations(targetElementId) {
588
+ const target = document.getElementById(targetElementId);
589
+ if (target) await renderRecommendations(target);
590
+ }
591
+
592
+ /**
593
+ * Find every recommendation widget on the page and fill it.
594
+ *
595
+ * Two ways in, both declarative — no inline script, so a site can ship a strict
596
+ * CSP with no `unsafe-inline`:
597
+ * - `data-leed-recommendations` on the element that receives the cards.
598
+ * - `data-leed-recommendations-target="id"` on the hidden marker the deprecated
599
+ * `{{{ Recommendations "id" }}}` shortcode emits, which renders into the
600
+ * element with that id — the shortcode's original semantic, preserved.
601
+ *
602
+ * A page with neither runs nothing and makes no request.
603
+ */
604
+ function initRecommendations() {
605
+ const targets = [...document.querySelectorAll("[data-leed-recommendations]")];
606
+ document.querySelectorAll("[data-leed-recommendations-target]").forEach((marker) => {
607
+ const target = document.getElementById(marker.dataset.leedRecommendationsTarget);
608
+ if (target) {
609
+ if (!targets.includes(target)) targets.push(target);
610
+ } else {
611
+ console.warn(`(H) recommendations: no element with id "${marker.dataset.leedRecommendationsTarget}"`);
612
+ }
613
+ });
614
+ // Fired, not awaited — widgets fill independently, and one slow fetch must not
615
+ // hold up the others.
616
+ targets.forEach((target) => {
617
+ renderRecommendations(target);
618
+ });
619
+ }
620
+
621
+ /** Build and insert the CTA into `target`. Renders nothing when there is no CTA. */
622
+ async function renderCallToAction(target) {
623
+ // this is a shared promise. needs to finish before we call services.
624
+ await leedSessionReady("pageview");
625
+ const cta = await fetchJson("sway");
626
+ if (!cta?.ctaId || !cta.content) return;
627
+
628
+ const el = document.createElement("div");
629
+ // Both attributes are load-bearing: ClickTracker.buildClickDetails walks up
630
+ // from the clicked anchor to the nearest [data-id], and ElementViewTracker's
631
+ // selector is `body div[name="cta"][data-id]`.
632
+ el.setAttribute("name", "cta");
633
+ el.dataset.id = `cta-${cta.ctaId}`;
634
+ // innerHTML is correct HERE ONLY — content is the same CMS-authored HTML the
635
+ // pre-rendered stub used to inline.
636
+ el.innerHTML = cta.content;
637
+ applyReason(el, "cta", cta.ctaId);
638
+ target.appendChild(el);
639
+ window.leedObserveElement?.(el);
640
+ }
641
+
642
+ /**
643
+ * Deprecated id-based entry point, kept working for sites that call it directly.
644
+ * The declarative `data-leed-cta` attribute is the supported API.
645
+ */
646
+ async function callToAction(targetElementId) {
647
+ const target = document.getElementById(targetElementId);
648
+ if (target) await renderCallToAction(target);
649
+ }
650
+
651
+ /**
652
+ * Find every CTA slot on the page and fill it. Same two declarative ways in as
653
+ * recommendations, and the same guarantee: a page with neither runs nothing and
654
+ * makes no request.
655
+ *
656
+ * Unlike recommendations there is no empty-state hide — a page with no CTA has
657
+ * always simply rendered nothing there.
658
+ */
659
+ function initCallToAction() {
660
+ const targets = [...document.querySelectorAll("[data-leed-cta]")];
661
+ document.querySelectorAll("[data-leed-cta-target]").forEach((marker) => {
662
+ const target = document.getElementById(marker.dataset.leedCtaTarget);
663
+ if (target) {
664
+ if (!targets.includes(target)) targets.push(target);
665
+ } else {
666
+ console.warn(`(H) cta: no element with id "${marker.dataset.leedCtaTarget}"`);
667
+ }
668
+ });
669
+ // Fired, not awaited — slots fill independently.
670
+ targets.forEach((target) => {
671
+ renderCallToAction(target);
672
+ });
673
+ }
674
+
675
+ /**
676
+ * Find the nearest scrollable parent element, or return null if window is the scrollable container
677
+ */
678
+ function findScrollableParent(element) {
679
+ if (!element || element === document.body || element === document.documentElement) {
680
+ return null; // Window is scrollable
681
+ }
682
+
683
+ let parent = element.parentElement;
684
+ while (parent && parent !== document.body && parent !== document.documentElement) {
685
+ const style = window.getComputedStyle(parent);
686
+ const hasOverflow =
687
+ style.overflow === "auto" ||
688
+ style.overflow === "scroll" ||
689
+ style.overflowY === "auto" ||
690
+ style.overflowY === "scroll";
691
+ const isScrollable = parent.scrollHeight > parent.clientHeight;
692
+
693
+ if (hasOverflow && isScrollable) {
694
+ return parent;
695
+ }
696
+ parent = parent.parentElement;
697
+ }
698
+
699
+ return null; // No scrollable parent found, window is scrollable
700
+ }
701
+
702
+ /**
703
+ * Scroll status indicator and related scroll functionality
704
+ */
705
+ function scrollStatusIndicatorHorizontalHandler(elementId = null) {
706
+ return {
707
+ percent: 0,
708
+ contentElement: null,
709
+ scrollContainer: null,
710
+ windowScrolls: false,
711
+
712
+ calculatePercentage() {
713
+ if (this.windowScrolls) {
714
+ // Window is scrollable - track content element's position through viewport
715
+ const rect = this.contentElement.getBoundingClientRect();
716
+ const elementTop = rect.top;
717
+ const elementBottom = rect.bottom;
718
+ const elementHeight = rect.height;
719
+ const viewportHeight = window.innerHeight;
720
+
721
+ // 0% when top of element reaches top of viewport, 100% when bottom reaches bottom
722
+ const totalDistance = viewportHeight - elementHeight;
723
+ const currentProgress = -elementTop;
724
+
725
+ let ratio = 0;
726
+ if (totalDistance > 0) {
727
+ ratio = (currentProgress / totalDistance) * 100;
728
+ } else if (totalDistance === 0) {
729
+ ratio = elementTop <= 0 && elementBottom >= viewportHeight ? 100 : 0;
730
+ } else {
731
+ const maxScroll = elementHeight - viewportHeight;
732
+ const currentScroll = -elementTop;
733
+ ratio = maxScroll > 0 ? (currentScroll / maxScroll) * 100 : 0;
734
+ }
735
+
736
+ this.percent = Math.min(100, Math.max(0, ratio));
737
+ // console.log("percent (window scroll)", this.percent);
738
+ } else {
739
+ // Element container is scrollable - calculate based on contentElement's position within container
740
+ const containerRect = this.scrollContainer.getBoundingClientRect();
741
+ const contentRect = this.contentElement.getBoundingClientRect();
742
+
743
+ const containerTop = containerRect.top;
744
+ const containerHeight = containerRect.height;
745
+ const contentTop = contentRect.top;
746
+ const contentBottom = contentRect.bottom;
747
+ const contentHeight = contentRect.height;
748
+
749
+ // Positions relative to container's viewport
750
+ const contentTopRelative = contentTop - containerTop;
751
+ const contentBottomRelative = contentBottom - containerTop;
752
+
753
+ // 0% when top reaches container top, 100% when bottom reaches container bottom
754
+ const totalDistance = containerHeight - contentHeight;
755
+ const currentProgress = -contentTopRelative;
756
+
757
+ let ratio = 0;
758
+ if (totalDistance > 0) {
759
+ ratio = (currentProgress / totalDistance) * 100;
760
+ } else if (totalDistance === 0) {
761
+ ratio = contentTopRelative <= 0 && contentBottomRelative >= containerHeight ? 100 : 0;
762
+ } else {
763
+ const maxScroll = contentHeight - containerHeight;
764
+ const currentScroll = -contentTopRelative;
765
+ ratio = maxScroll > 0 ? (currentScroll / maxScroll) * 100 : 0;
766
+ }
767
+
768
+ this.percent = Math.min(100, Math.max(0, ratio));
769
+ // console.log("percent (element container)", this.percent);
770
+ }
771
+ },
772
+
773
+ init() {
774
+ if (!elementId) {
775
+ console.warn("(H) scrollStatusIndicator: no elementId provided");
776
+ return;
777
+ }
778
+
779
+ this.contentElement = document.getElementById(elementId);
780
+ if (!this.contentElement) {
781
+ console.warn(`(H) scrollStatusIndicator: elementId "${elementId}" not found`);
782
+ return;
783
+ }
784
+
785
+ // Find the nearest scrollable parent
786
+ const foundContainer = findScrollableParent(this.contentElement);
787
+ this.scrollContainer = foundContainer || window;
788
+ this.windowScrolls = this.scrollContainer === window;
789
+
790
+ if (!this.scrollContainer) {
791
+ console.warn(
792
+ `(H) scrollStatusIndicator: could not located a scrollable container for elementId "${elementId}"`,
793
+ );
794
+ return;
795
+ }
796
+
797
+ const scrollHandler = () => {
798
+ requestAnimationFrame(() => this.calculatePercentage());
799
+ };
800
+
801
+ this.scrollContainer.addEventListener("scroll", scrollHandler, { passive: true });
802
+ this.calculatePercentage();
803
+ },
804
+ };
805
+ }
806
+
807
+ // Recommendation and CTA widgets are declarative, so nothing on the page calls
808
+ // in — find them as soon as the DOM is walkable, whether or not this script was
809
+ // deferred.
810
+ function initLeedWidgets() {
811
+ initRecommendations();
812
+ initCallToAction();
813
+ }
814
+
815
+ if (document.readyState === "loading") {
816
+ document.addEventListener("DOMContentLoaded", initLeedWidgets);
817
+ } else {
818
+ initLeedWidgets();
819
+ }