@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,2781 @@
1
+ // Configuration and constants
2
+ const CONFIG = {
3
+ NOTIFICATION_TIMEOUT: 2000,
4
+ DEBOUNCE_DELAYS: {
5
+ FAST: 30,
6
+ MEDIUM: 50,
7
+ SLOW: 100,
8
+ // Announcements only: a live region has to wait for a pause in typing, well
9
+ // past the point where the thing it describes has finished moving.
10
+ ANNOUNCE: 500,
11
+ },
12
+ BREAKPOINTS: {
13
+ SMALLEST: 640,
14
+ MOBILE: 768,
15
+ DESKTOP: 1024,
16
+ },
17
+ SELECTORS: {
18
+ CODE_BLOCKS: "pre code:not(.math)",
19
+ TABS: '[role="tab"]',
20
+ MENU_ITEMS: ".leed-menu__item, .leed-submenu__item",
21
+ SIDEBAR: ".docs-sidebar",
22
+ TOC_LINKS: ".leed-nav-toc a",
23
+ MERMAID_DIAGRAMS: "pre.mermaid",
24
+ MATH_ELEMENTS: "code.math",
25
+ },
26
+ ARIA: {
27
+ EXPANDED: "aria-expanded",
28
+ HIDDEN: "aria-hidden",
29
+ CURRENT: "aria-current",
30
+ CONTROLS: "aria-controls",
31
+ HASPOPUP: "aria-haspopup",
32
+ },
33
+ CLASSES: {
34
+ NOT_PROSE: "not-prose",
35
+ COPIED: "copied",
36
+ JUST_COPIED: "just-copied",
37
+ HIDDEN: "hidden",
38
+ BLOCK: "block",
39
+ WITH_ANCHOR: "docs-with-anchor",
40
+ },
41
+ };
42
+
43
+ // Utility functions
44
+ const Utils = {
45
+ debounce(func, wait) {
46
+ let timeout;
47
+ return (...args) => {
48
+ clearTimeout(timeout);
49
+ timeout = setTimeout(() => func(...args), wait);
50
+ };
51
+ },
52
+
53
+ isSmallest() {
54
+ return window.innerWidth < CONFIG.BREAKPOINTS.SMALLEST;
55
+ },
56
+
57
+ isMobile() {
58
+ return window.innerWidth < CONFIG.BREAKPOINTS.MOBILE;
59
+ },
60
+
61
+ isSmallerThanDesktop() {
62
+ return window.innerWidth < CONFIG.BREAKPOINTS.DESKTOP;
63
+ },
64
+
65
+ isDesktop() {
66
+ return window.innerWidth >= CONFIG.BREAKPOINTS.DESKTOP;
67
+ },
68
+ };
69
+
70
+ // Notification system
71
+ const Notification = {
72
+ show(element, message, className = "notification", timeout = CONFIG.NOTIFICATION_TIMEOUT) {
73
+ // Remove any existing notifications of the same class
74
+ const existingNotification = element.querySelector(`.${className}`);
75
+ if (existingNotification) {
76
+ existingNotification.remove();
77
+ }
78
+
79
+ const notification = document.createElement("span");
80
+ notification.className = className;
81
+ notification.textContent = message;
82
+ element.appendChild(notification);
83
+
84
+ setTimeout(() => {
85
+ if (notification.parentNode) {
86
+ notification.parentNode.removeChild(notification);
87
+ }
88
+ }, timeout);
89
+ },
90
+ };
91
+
92
+ // ARIA state management
93
+ const AriaManager = {
94
+ setExpanded(element, expanded) {
95
+ element.setAttribute(CONFIG.ARIA.EXPANDED, expanded);
96
+ },
97
+
98
+ setHidden(element, hidden) {
99
+ element.setAttribute(CONFIG.ARIA.HIDDEN, hidden);
100
+ },
101
+
102
+ setCurrent(element, current) {
103
+ element.setAttribute(CONFIG.ARIA.CURRENT, current);
104
+ },
105
+
106
+ removeCurrent(element) {
107
+ element.removeAttribute(CONFIG.ARIA.CURRENT);
108
+ },
109
+ };
110
+
111
+ // DOM cache for performance
112
+ const DOMCache = {
113
+ elements: {},
114
+
115
+ get(selector) {
116
+ if (!this.elements[selector]) {
117
+ this.elements[selector] = document.querySelector(selector);
118
+ }
119
+ return this.elements[selector];
120
+ },
121
+
122
+ getAll(selector) {
123
+ if (!this.elements[selector]) {
124
+ this.elements[selector] = document.querySelectorAll(selector);
125
+ }
126
+ return this.elements[selector];
127
+ },
128
+
129
+ clear() {
130
+ this.elements = {};
131
+ },
132
+ };
133
+
134
+ // Mermaid diagram manager
135
+ const MermaidManager = {
136
+ init() {
137
+ if (typeof mermaid === "undefined") return;
138
+
139
+ this.setupTheme();
140
+ this.ensureThemeRenders().then(() => this.renderDiagrams());
141
+ this.setupThemeListener();
142
+ },
143
+
144
+ /**
145
+ * A site can put its own palette on diagrams by shipping
146
+ * `static/js/mermaid.theme.json`, which the build publishes as
147
+ * `window.leedMermaidTheme` — one Mermaid config per scheme.
148
+ *
149
+ * It is applied here rather than where it is published because this is the
150
+ * `initialize()` that wins: it runs after the bootstrap, and again on every
151
+ * scheme change. That is also what makes a themed site switch live — the
152
+ * diagrams were already being redrawn, so they are redrawn in the other
153
+ * palette rather than waiting for a reload.
154
+ *
155
+ * `theme` comes first so a site's own `theme` (usually `"base"`, which is
156
+ * what makes `themeVariables` take effect at all) replaces our default.
157
+ * `startOnLoad` comes last and cannot be overridden — we render below, and a
158
+ * site turning it back on would get every diagram drawn twice.
159
+ */
160
+ setupTheme() {
161
+ const prefersDarkMode = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
162
+ const theme = prefersDarkMode ? "dark" : "default";
163
+ const site = window.leedMermaidTheme?.[prefersDarkMode ? "dark" : "light"];
164
+
165
+ mermaid.initialize({
166
+ theme,
167
+ ...site,
168
+ startOnLoad: false,
169
+ });
170
+ },
171
+
172
+ /**
173
+ * Prove the site's palette can render anything at all, before the real
174
+ * diagrams are drawn.
175
+ *
176
+ * A palette may name a color Mermaid cannot parse — `var(--token)` is the
177
+ * useful case, and Mermaid accepts it in the variables it uses verbatim but
178
+ * throws on the handful it derives other colors from. Which are which is
179
+ * internal to Mermaid and can move in a minor version, so a palette that
180
+ * worked can start throwing on an upgrade, and the throw would otherwise
181
+ * take every diagram on the page down to raw source.
182
+ *
183
+ * A probe rather than a reaction, deliberately. Reacting to the first render
184
+ * failure cannot tell a bad palette from one badly written diagram, and
185
+ * guessing "palette" there means a single typo'd diagram silently strips the
186
+ * colors from every other one on the page. This asks the question directly,
187
+ * on a diagram known to be valid, and costs one render of two nodes.
188
+ */
189
+ async ensureThemeRenders() {
190
+ if (!window.leedMermaidTheme) return;
191
+ try {
192
+ await mermaid.render("leed-mermaid-theme-probe", "flowchart LR\n a --> b");
193
+ } catch (error) {
194
+ const prefersDarkMode = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
195
+ console.error(
196
+ "Mermaid rejected this site's palette from static/js/mermaid.theme.json; rendering in Mermaid's own colors instead.",
197
+ error,
198
+ );
199
+ mermaid.initialize({ theme: prefersDarkMode ? "dark" : "default", startOnLoad: false });
200
+ }
201
+ },
202
+
203
+ renderDiagrams() {
204
+ const diagrams = document.querySelectorAll(CONFIG.SELECTORS.MERMAID_DIAGRAMS);
205
+ let counter = 0;
206
+
207
+ diagrams.forEach((diagram) => {
208
+ const id = `mermaid-${counter++}`;
209
+
210
+ if (diagram.originalCode === undefined) {
211
+ diagram.originalCode = diagram.textContent.trim();
212
+ }
213
+
214
+ mermaid
215
+ .render(id, diagram.originalCode)
216
+ .then(({ svg }) => {
217
+ diagram.innerHTML = svg;
218
+ })
219
+ .catch((err) => {
220
+ // This diagram's own problem. The palette was already proven in
221
+ // ensureThemeRenders, so one bad diagram never costs the others
222
+ // their colors.
223
+ console.error("Mermaid rendering failed:", err);
224
+ });
225
+ });
226
+ },
227
+
228
+ setupThemeListener() {
229
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
230
+ this.setupTheme();
231
+ this.ensureThemeRenders().then(() => this.renderDiagrams());
232
+ });
233
+ },
234
+ };
235
+
236
+ // Code block manager
237
+ const CodeBlockManager = {
238
+ init() {
239
+ this.setupCodeHighlighting();
240
+ this.setupCopyButtons();
241
+ },
242
+
243
+ setupCodeHighlighting() {
244
+ document.querySelectorAll("pre > code").forEach((element) => {
245
+ element.classList.add(CONFIG.CLASSES.NOT_PROSE);
246
+ element.parentElement.classList.add(CONFIG.CLASSES.NOT_PROSE);
247
+ });
248
+
249
+ if (typeof hljs !== "undefined") {
250
+ document.querySelectorAll("pre > code").forEach((element) => {
251
+ if (!element.classList.contains("math")) {
252
+ hljs.highlightElement(element);
253
+ }
254
+ });
255
+ }
256
+
257
+ if (typeof katex !== "undefined") {
258
+ const mathElements = document.querySelectorAll(CONFIG.SELECTORS.MATH_ELEMENTS);
259
+ mathElements.forEach((element) => {
260
+ katex.render(element.textContent, element, { strict: false });
261
+ });
262
+ }
263
+ },
264
+
265
+ setupCopyButtons() {
266
+ const codeBlocks = document.querySelectorAll(CONFIG.SELECTORS.CODE_BLOCKS);
267
+
268
+ codeBlocks.forEach((codeBlock) => {
269
+ const pre = codeBlock.parentElement;
270
+ const wrapper = document.createElement("div");
271
+ wrapper.className = "code-block-wrapper";
272
+
273
+ pre.parentNode.insertBefore(wrapper, pre);
274
+ wrapper.appendChild(pre);
275
+
276
+ const copyButton = document.createElement("button");
277
+ copyButton.className = "copy-code-button";
278
+ copyButton.setAttribute("aria-label", "Copy code to clipboard");
279
+ copyButton.innerHTML = '<i class="copy"></i>';
280
+
281
+ wrapper.appendChild(copyButton);
282
+
283
+ copyButton.addEventListener("click", async () => {
284
+ try {
285
+ await navigator.clipboard.writeText(codeBlock.textContent);
286
+
287
+ copyButton.classList.add(CONFIG.CLASSES.COPIED);
288
+ wrapper.classList.add(CONFIG.CLASSES.JUST_COPIED);
289
+
290
+ Notification.show(copyButton, "Copied!", "code-copied-notification");
291
+
292
+ setTimeout(() => {
293
+ copyButton.classList.remove(CONFIG.CLASSES.COPIED);
294
+ wrapper.classList.remove(CONFIG.CLASSES.JUST_COPIED);
295
+ }, CONFIG.NOTIFICATION_TIMEOUT);
296
+ } catch (err) {
297
+ console.error("Failed to copy: ", err);
298
+ }
299
+ });
300
+ });
301
+ },
302
+ };
303
+
304
+ // Tab manager
305
+ const TabManager = {
306
+ init() {
307
+ document.querySelectorAll(".leed-tabs__container").forEach((container) => {
308
+ this.initializeTabs(container);
309
+ });
310
+ this.restoreTabStates();
311
+ },
312
+
313
+ initializeTabs(container) {
314
+ const tabs = Array.from(container.querySelectorAll(CONFIG.SELECTORS.TABS));
315
+ const tabList = container.querySelector('[role="tablist"]');
316
+ tabList.classList.add(CONFIG.CLASSES.NOT_PROSE);
317
+
318
+ tabs.forEach((tab, index) => {
319
+ tab.setAttribute("data-tab-index", index);
320
+ tab.addEventListener("click", () => this.activateTabGroup(tab));
321
+ tab.addEventListener("keydown", this.handleTabKeydown);
322
+ });
323
+
324
+ if (tabList) {
325
+ tabList.addEventListener("keydown", this.handleTablistKeydown);
326
+ }
327
+
328
+ const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true");
329
+ if (!activeTab && tabs.length > 0) {
330
+ this.activateTab(tabs[0]);
331
+ }
332
+ },
333
+
334
+ activateTabGroup(clickedTab) {
335
+ const clickedTabIndex = parseInt(clickedTab.getAttribute("data-tab-index"), 10);
336
+ const tabContainer = clickedTab.closest(".leed-tabs__container");
337
+ const groupId = tabContainer.getAttribute("data-tabs-groupId");
338
+
339
+ if (!groupId) {
340
+ this.activateTab(clickedTab);
341
+ return;
342
+ }
343
+
344
+ this.saveTabStateByIndex(groupId, clickedTabIndex);
345
+
346
+ const containersInGroup = document.querySelectorAll(`.leed-tabs__container[data-tabs-groupId="${groupId}"]`);
347
+
348
+ containersInGroup.forEach((container) => {
349
+ const tabsInContainer = Array.from(container.querySelectorAll(CONFIG.SELECTORS.TABS));
350
+ if (clickedTabIndex < tabsInContainer.length) {
351
+ this.activateTab(tabsInContainer[clickedTabIndex]);
352
+ }
353
+ });
354
+ },
355
+
356
+ handleTabKeydown(event) {
357
+ if (event.key === " " || event.key === "Enter") {
358
+ event.preventDefault();
359
+ TabManager.activateTabGroup(event.currentTarget);
360
+ }
361
+ },
362
+
363
+ handleTablistKeydown(event) {
364
+ const tabs = Array.from(event.currentTarget.querySelectorAll(CONFIG.SELECTORS.TABS));
365
+ const currentTab = document.activeElement;
366
+
367
+ if (!tabs.includes(currentTab)) return;
368
+
369
+ const currentIndex = tabs.indexOf(currentTab);
370
+ let targetTab;
371
+
372
+ switch (event.key) {
373
+ case "ArrowRight":
374
+ case "ArrowDown":
375
+ event.preventDefault();
376
+ targetTab = tabs[(currentIndex + 1) % tabs.length];
377
+ targetTab.focus();
378
+ break;
379
+ case "ArrowLeft":
380
+ case "ArrowUp":
381
+ event.preventDefault();
382
+ targetTab = tabs[(currentIndex - 1 + tabs.length) % tabs.length];
383
+ targetTab.focus();
384
+ break;
385
+ case "Home":
386
+ event.preventDefault();
387
+ tabs[0].focus();
388
+ break;
389
+ case "End":
390
+ event.preventDefault();
391
+ tabs[tabs.length - 1].focus();
392
+ break;
393
+ }
394
+ },
395
+
396
+ activateTab(tab) {
397
+ const tabContainer = tab.closest(".leed-tabs__container");
398
+ const tabs = Array.from(tab.closest('[role="tablist"]').querySelectorAll(CONFIG.SELECTORS.TABS));
399
+ const panelId = tab.getAttribute(CONFIG.ARIA.CONTROLS);
400
+ const panel = document.getElementById(panelId);
401
+
402
+ tabs.forEach((tab) => {
403
+ tab.setAttribute("aria-selected", "false");
404
+ tab.setAttribute("tabindex", "-1");
405
+ });
406
+
407
+ tabContainer.querySelectorAll('[role="tabpanel"]').forEach((tabPanel) => {
408
+ tabPanel.classList.add(CONFIG.CLASSES.HIDDEN);
409
+ tabPanel.classList.remove(CONFIG.CLASSES.BLOCK);
410
+ });
411
+
412
+ tab.setAttribute("aria-selected", "true");
413
+ tab.setAttribute("tabindex", "0");
414
+
415
+ if (panel) {
416
+ panel.classList.remove(CONFIG.CLASSES.HIDDEN);
417
+ panel.classList.add(CONFIG.CLASSES.BLOCK);
418
+ }
419
+ },
420
+
421
+ saveTabStateByIndex(groupId, tabIndex) {
422
+ if (!groupId) return false;
423
+
424
+ try {
425
+ const storageKey = `leed-tabs-${groupId}`;
426
+ localStorage.setItem(storageKey, tabIndex.toString());
427
+ return true;
428
+ } catch (e) {
429
+ console.warn("Unable to save tab state:", e);
430
+ return false;
431
+ }
432
+ },
433
+
434
+ restoreTabStates() {
435
+ const groupIds = new Set();
436
+ document.querySelectorAll(".leed-tabs__container[data-tabs-groupId]").forEach((container) => {
437
+ const groupId = container.getAttribute("data-tabs-groupId");
438
+ if (groupId) {
439
+ groupIds.add(groupId);
440
+ }
441
+ });
442
+
443
+ groupIds.forEach((groupId) => {
444
+ try {
445
+ const storageKey = `leed-tabs-${groupId}`;
446
+ const savedTabIndex = localStorage.getItem(storageKey);
447
+
448
+ if (savedTabIndex !== null) {
449
+ const tabIndex = parseInt(savedTabIndex, 10);
450
+
451
+ document.querySelectorAll(`.leed-tabs__container[data-tabs-groupId="${groupId}"]`).forEach((container) => {
452
+ const tabs = container.querySelectorAll(CONFIG.SELECTORS.TABS);
453
+ if (tabIndex < tabs.length) {
454
+ this.activateTab(tabs[tabIndex]);
455
+ }
456
+ });
457
+ }
458
+ } catch (e) {
459
+ console.warn(`Unable to restore tab state for group ${groupId}:`, e);
460
+ }
461
+ });
462
+ },
463
+ };
464
+
465
+ // Menu manager
466
+ const MenuManager = {
467
+ createComponent() {
468
+ return {
469
+ openDropdowns: {},
470
+
471
+ init() {
472
+ this.setupMenuStructure();
473
+ this.markActivePageAndExpandMenus();
474
+ this.setupMenuToggle();
475
+ this.setupKeyboardNavigation();
476
+ },
477
+
478
+ isTopLevelMenuItem(item) {
479
+ // Check if this is a top-level menu item (direct child of .leed-menu)
480
+ return item.closest(".leed-menu") === item.parentElement;
481
+ },
482
+
483
+ shouldItemBeExpanded(item) {
484
+ // Check if the item itself has data-expanded="true"
485
+ if (item.getAttribute("data-expanded") === "true") {
486
+ return true;
487
+ }
488
+
489
+ // Check if the parent .leed-menu has data-expanded="true"
490
+ const parentMenu = item.closest(".leed-menu");
491
+ if (parentMenu && parentMenu.getAttribute("data-expanded") === "true") {
492
+ return true;
493
+ }
494
+
495
+ return false;
496
+ },
497
+
498
+ setupMenuStructure() {
499
+ this.$el.querySelectorAll(".leed-menu__item--toggle").forEach((toggle) => {
500
+ toggle.setAttribute("aria-hidden", "false");
501
+ });
502
+
503
+ this.$el.querySelectorAll(".leed-menu").forEach((menu) => {
504
+ menu.setAttribute("role", "menu");
505
+ menu.setAttribute("aria-orientation", "vertical");
506
+ });
507
+
508
+ this.$el.querySelectorAll(CONFIG.SELECTORS.MENU_ITEMS).forEach((item) => {
509
+ item.setAttribute("role", "menuitem");
510
+
511
+ const submenu = item.querySelector(".leed-submenu");
512
+ if (submenu) {
513
+ // Check if this is a top-level menu item (direct child of .leed-menu)
514
+ const isTopLevel = this.isTopLevelMenuItem(item);
515
+
516
+ if (isTopLevel) {
517
+ // Top-level menus are always expanded and not collapsible
518
+ item.setAttribute(CONFIG.ARIA.EXPANDED, "true");
519
+ item.setAttribute("aria-disabled", "true"); // Prevent interaction
520
+
521
+ const submenuId = submenu.id || `submenu-${item.dataset.submenuId}`;
522
+ submenu.id = submenuId;
523
+ item.setAttribute(CONFIG.ARIA.CONTROLS, submenuId);
524
+
525
+ submenu.setAttribute(CONFIG.ARIA.HIDDEN, "false");
526
+ submenu.setAttribute("role", "menu");
527
+
528
+ // Store the submenu ID as always open
529
+ const id = item.dataset.submenuId;
530
+ if (id) {
531
+ this.openDropdowns[id] = true;
532
+ }
533
+ } else {
534
+ // Nested menus are collapsible, but preserve items if they should be expanded
535
+ const shouldBeExpanded = this.shouldItemBeExpanded(item);
536
+
537
+ // Only set to collapsed if item should not be expanded
538
+ if (!shouldBeExpanded) {
539
+ item.setAttribute(CONFIG.ARIA.EXPANDED, "false");
540
+ // Remove focus from any focused elements within the submenu before hiding
541
+ const focusedElement = submenu.querySelector(":focus");
542
+ if (focusedElement) {
543
+ focusedElement.blur();
544
+ }
545
+ submenu.setAttribute(CONFIG.ARIA.HIDDEN, "true");
546
+ }
547
+
548
+ const submenuId = submenu.id || `submenu-${item.dataset.submenuId}`;
549
+ submenu.id = submenuId;
550
+ item.setAttribute(CONFIG.ARIA.CONTROLS, submenuId);
551
+
552
+ submenu.setAttribute("role", "menu");
553
+
554
+ // Store the submenu ID as open if it should be expanded
555
+ if (shouldBeExpanded) {
556
+ const id = item.dataset.submenuId;
557
+ if (id) {
558
+ this.openDropdowns[id] = true;
559
+ }
560
+ }
561
+ }
562
+ }
563
+ });
564
+
565
+ // Prevent sidebar from closing when clicking on menu links
566
+ this.$el.querySelectorAll("a").forEach((link) => {
567
+ link.addEventListener("click", (e) => {
568
+ // Stop the click event from bubbling up to the sidebar's @click.away
569
+ e.stopPropagation();
570
+ });
571
+ });
572
+ },
573
+
574
+ // Center the active item in the sidebar's own scroller. scrollIntoView
575
+ // would scroll every scrollable ancestor up to and including the
576
+ // viewport, so an item near the bottom of the menu — past what the
577
+ // sidebar can scroll on its own — leaves the remainder to the document
578
+ // and the page loads scrolled down. The sidebar is sticky, so that
579
+ // scroll does not even move the item.
580
+ centerInSidebar(item) {
581
+ const scroller = item.closest(CONFIG.SELECTORS.SIDEBAR);
582
+ if (!scroller || scroller.scrollHeight <= scroller.clientHeight) {
583
+ return;
584
+ }
585
+
586
+ const offset = item.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
587
+ // The browser clamps scrollTop to the scrollable range.
588
+ scroller.scrollTop += offset - (scroller.clientHeight - item.offsetHeight) / 2;
589
+ },
590
+
591
+ markActivePageAndExpandMenus() {
592
+ const currentPath = window.location.pathname;
593
+ let foundActive = false;
594
+
595
+ this.$el.querySelectorAll("a").forEach((link) => {
596
+ const href = link.getAttribute("href");
597
+
598
+ if (href && (href === currentPath || currentPath.endsWith(href))) {
599
+ foundActive = true;
600
+
601
+ const activeItem = link.closest(CONFIG.SELECTORS.MENU_ITEMS);
602
+ if (activeItem) {
603
+ AriaManager.setCurrent(activeItem, "page");
604
+ this.expandParentMenus(activeItem);
605
+ // After Alpine has applied the expansion above, so the item is
606
+ // measured at its final position.
607
+ this.$nextTick(() => this.centerInSidebar(activeItem));
608
+
609
+ if (activeItem.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
610
+ const id = activeItem.dataset.submenuId;
611
+ if (id) {
612
+ this.openDropdowns[id] = true;
613
+ }
614
+
615
+ AriaManager.setExpanded(activeItem, true);
616
+ const submenuId = activeItem.getAttribute(CONFIG.ARIA.CONTROLS);
617
+ if (submenuId) {
618
+ const submenu = document.getElementById(submenuId);
619
+ if (submenu) {
620
+ AriaManager.setHidden(submenu, false);
621
+ }
622
+ }
623
+ }
624
+
625
+ // Make current page link not clickable
626
+ link.addEventListener("click", (e) => {
627
+ e.preventDefault();
628
+ });
629
+
630
+ // Add visual indication that this is the current page
631
+ link.classList.add("cursor-default");
632
+ }
633
+ }
634
+ });
635
+ },
636
+
637
+ expandParentMenus(item) {
638
+ let parent = item.parentElement;
639
+
640
+ while (parent) {
641
+ if (parent.classList.contains("leed-submenu")) {
642
+ const parentMenuItem = parent.parentElement;
643
+
644
+ if (parentMenuItem?.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
645
+ const id = parentMenuItem.dataset.submenuId;
646
+ if (id) {
647
+ this.openDropdowns[id] = true;
648
+ }
649
+
650
+ AriaManager.setExpanded(parentMenuItem, true);
651
+ AriaManager.setHidden(parent, false);
652
+ }
653
+ }
654
+
655
+ parent = parent.parentElement;
656
+ }
657
+ },
658
+
659
+ setupMenuToggle() {
660
+ this.$el.querySelectorAll(`[${CONFIG.ARIA.HASPOPUP}='menu']`).forEach((item) => {
661
+ // Skip top-level menu items - they should not be collapsible
662
+ const isTopLevel = this.isTopLevelMenuItem(item);
663
+ if (isTopLevel) {
664
+ return; // Skip setting up toggle functionality for top-level items
665
+ }
666
+
667
+ const submenuId = item.getAttribute(CONFIG.ARIA.CONTROLS);
668
+ const submenu = document.getElementById(submenuId);
669
+ const id = item.dataset.submenuId;
670
+
671
+ if (submenu && id) {
672
+ Alpine.bind(item, {
673
+ "x-data"() {
674
+ return { id };
675
+ },
676
+ "@click.stop"() {
677
+ const isExpanded = item.getAttribute(CONFIG.ARIA.EXPANDED) === "true";
678
+ const newState = !isExpanded;
679
+
680
+ this.openDropdowns[id] = newState;
681
+ AriaManager.setExpanded(item, newState);
682
+ // Remove focus from any focused elements within the submenu before hiding
683
+ if (!newState) {
684
+ const focusedElement = submenu.querySelector(":focus");
685
+ if (focusedElement) {
686
+ focusedElement.blur();
687
+ }
688
+ }
689
+ AriaManager.setHidden(submenu, !newState);
690
+ },
691
+ });
692
+ }
693
+ });
694
+ },
695
+
696
+ setupKeyboardNavigation() {
697
+ this.$el.addEventListener("keydown", (e) => {
698
+ const activeElement = document.activeElement;
699
+ const menuItem = activeElement.closest("[role='menuitem']");
700
+
701
+ if (!menuItem) return;
702
+
703
+ // Skip keyboard navigation for top-level menu items
704
+ const isTopLevel = this.isTopLevelMenuItem(menuItem);
705
+ if (isTopLevel && menuItem.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
706
+ return; // Skip keyboard toggle for top-level items
707
+ }
708
+
709
+ switch (e.key) {
710
+ case "Enter":
711
+ case " ":
712
+ if (menuItem.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
713
+ e.preventDefault();
714
+ const isExpanded = menuItem.getAttribute(CONFIG.ARIA.EXPANDED) === "true";
715
+ const newState = !isExpanded;
716
+
717
+ AriaManager.setExpanded(menuItem, newState);
718
+
719
+ const submenuId = menuItem.getAttribute(CONFIG.ARIA.CONTROLS);
720
+ const submenu = document.getElementById(submenuId);
721
+ if (submenu) {
722
+ // Remove focus from any focused elements within the submenu before hiding
723
+ if (!newState) {
724
+ const focusedElement = submenu.querySelector(":focus");
725
+ if (focusedElement) {
726
+ focusedElement.blur();
727
+ }
728
+ }
729
+ AriaManager.setHidden(submenu, !newState);
730
+
731
+ const id = menuItem.dataset.submenuId;
732
+ if (id) this.openDropdowns[id] = newState;
733
+ }
734
+ }
735
+ break;
736
+
737
+ case "Escape": {
738
+ e.preventDefault();
739
+ const parentMenu = menuItem.closest(".leed-submenu");
740
+ if (parentMenu) {
741
+ const parentItem = parentMenu.closest(`[${CONFIG.ARIA.HASPOPUP}='menu']`);
742
+ if (parentItem) {
743
+ // Don't collapse if it's a top-level menu
744
+ const isParentTopLevel = this.isTopLevelMenuItem(parentItem);
745
+ if (!isParentTopLevel) {
746
+ AriaManager.setExpanded(parentItem, false);
747
+ // Remove focus from any focused elements within the submenu before hiding
748
+ const focusedElement = parentMenu.querySelector(":focus");
749
+ if (focusedElement) {
750
+ focusedElement.blur();
751
+ }
752
+ AriaManager.setHidden(parentMenu, true);
753
+
754
+ const id = parentItem.dataset.submenuId;
755
+ if (id) this.openDropdowns[id] = false;
756
+
757
+ parentItem.focus();
758
+ }
759
+ }
760
+ }
761
+ break;
762
+ }
763
+ }
764
+ });
765
+ },
766
+ };
767
+ },
768
+ };
769
+
770
+ // Left navigation filter. Deliberately a plain manager rather than an Alpine
771
+ // component: it runs on its own attribute channel — `data-filtering` on
772
+ // #docs-navigation, `data-filter-hidden` on the items — and never writes
773
+ // aria-expanded or MenuManager's openDropdowns, which stay Alpine's exclusively.
774
+ //
775
+ // The one exception is aria-hidden on a submenu the filter reveals. Revealing a
776
+ // collapsed section with CSS alone would paint tabbable links that are absent
777
+ // from the accessibility tree (WCAG 4.1.2), so the reveal is done in aria and
778
+ // marked with data-filter-revealed. Nothing is stashed: aria-hidden on a submenu
779
+ // is derivable, always the negation of its controlling <li>'s aria-expanded, so
780
+ // clearing recomputes it. A section the reader toggles mid-filter therefore
781
+ // keeps the reader's choice, and the two systems cannot end up disagreeing.
782
+ //
783
+ // The two systems never run at the same time either: a click on a section
784
+ // header exits filter mode in the capture phase, so MenuManager's toggle only
785
+ // ever reads a menu the filter has already handed back (see bindEvents).
786
+ const NavFilterManager = {
787
+ items: [],
788
+ pending: "",
789
+ query: "",
790
+ // The debounced writer for the live region, built in bindEvents.
791
+ setStatus: null,
792
+
793
+ // Case- and diacritic-insensitive, with the &nbsp; before an external-link
794
+ // icon folded into ordinary whitespace. A slug's word separators fold here
795
+ // rather than in slugOf, so the reader's query goes through the same fold the
796
+ // slug haystack does: fold one side only and typing the slug the way the URL
797
+ // spells it — "plans_and-pricing" — is the one query the fallback cannot
798
+ // answer, which is the fallback's own headline case.
799
+ normalize(value) {
800
+ return value
801
+ .normalize("NFD")
802
+ .replace(/[\u0300-\u036f]/g, "")
803
+ .toLowerCase()
804
+ .replace(/[-_]/g, " ")
805
+ .replace(/\s+/g, " ")
806
+ .trim();
807
+ },
808
+
809
+ // The text the slug fallback matches on: the last non-empty path segment of
810
+ // an href, and nothing else. A docs site can be served under any prefix — a
811
+ // whole site under /docs/ would otherwise match every item on "docs", and the
812
+ // prefix is the customer's, so it cannot be known here and subtracted. The
813
+ // query and the fragment are dropped first (neither is part of the path), then
814
+ // the scheme and authority, so the host of an absolute URL is never matched
815
+ // either. An href with no path segments — an external link to a bare origin,
816
+ // or a parent with no href at all — yields "" and is simply never matched.
817
+ slugOf(href) {
818
+ const raw = (href || "").split(/[?#]/)[0];
819
+
820
+ // An opaque scheme — mailto:, tel: — has no path at all, so there is
821
+ // nothing to take a slug from. Slugging the whole href instead would answer
822
+ // a query with an address the menu never shows, announced as a link match.
823
+ if (/^[a-z][a-z0-9+.-]*:(?!\/\/)/i.test(raw)) {
824
+ return "";
825
+ }
826
+
827
+ // Scheme and authority come off together, and the scheme is optional so a
828
+ // protocol-relative "//host/…" loses its host the same way an absolute URL
829
+ // does — it is an authority, not a path segment.
830
+ const path = raw.replace(/^([a-z][a-z0-9+.-]*:)?\/\/[^/]*/i, "");
831
+ const segments = path.split("/").filter(Boolean);
832
+ const segment = segments[segments.length - 1];
833
+
834
+ if (!segment) {
835
+ return "";
836
+ }
837
+
838
+ // The same normalization a name gets — including the `-`/`_` fold, which
839
+ // lives there so the reader's query is folded the same way.
840
+ return this.normalize(this.decodeSegment(segment));
841
+ },
842
+
843
+ // decodeURIComponent throws a URIError on a malformed escape (`%E0%A4%A`).
844
+ // One bad href in a menu must not cost the reader the whole filter, so the
845
+ // raw segment stands in — still matchable, just not decoded.
846
+ decodeSegment(segment) {
847
+ try {
848
+ return decodeURIComponent(segment);
849
+ } catch {
850
+ return segment;
851
+ }
852
+ },
853
+
854
+ init() {
855
+ this.nav = DOMCache.get("#docs-navigation");
856
+ this.container = DOMCache.get("#docs-nav-filter");
857
+ this.input = DOMCache.get("#docs-nav-filter-input");
858
+ this.clearButton = DOMCache.get("#docs-nav-filter-clear");
859
+ this.empty = DOMCache.get("#docs-nav-filter-empty");
860
+ this.status = DOMCache.get("#docs-nav-filter-status");
861
+
862
+ if (!this.nav || !this.container || !this.input || !this.clearButton || !this.empty || !this.status) {
863
+ return;
864
+ }
865
+
866
+ // A left menu that does not resolve renders no list at all, which would
867
+ // leave the box an orphan control. Remove it rather than hide it: any
868
+ // `display` on #docs-nav-filter outranks the user agent's [hidden] rule.
869
+ if (!this.nav.querySelector(".leed-menu")) {
870
+ this.container.remove();
871
+ this.empty.remove();
872
+ return;
873
+ }
874
+
875
+ this.cacheItems();
876
+ this.bindEvents();
877
+ },
878
+
879
+ // Names are read once. Scoped to the item's own wrapper so a parent never
880
+ // picks up a child's label, and to the name span so the API method badge —
881
+ // a CSS ::before on a sibling icon — contributes nothing. The subtree and the
882
+ // controlled submenu are resolved here too: querying them per match inside the
883
+ // match loop is O(items × matches) of DOM work on every keystroke.
884
+ //
885
+ // Slugs are cached in the same pass, for the same reason: the fallback runs on
886
+ // a keystroke like any other pass, and re-deriving a slug there would put a
887
+ // getAttribute, a split and a decode per item behind every character.
888
+ cacheItems() {
889
+ this.items = Array.from(this.nav.querySelectorAll(CONFIG.SELECTORS.MENU_ITEMS)).map((li) => {
890
+ const name = li.querySelector(":scope > .leed-menu__item--wrapper .leed-menu__item--name");
891
+ // The wrapper is an <a> only when the href resolved; a no-link parent
892
+ // renders a <div>, which has no href and so has no slug.
893
+ const wrapper = li.querySelector(":scope > .leed-menu__item--wrapper");
894
+ return {
895
+ li,
896
+ name: this.normalize(name ? name.textContent : ""),
897
+ slug: this.slugOf(wrapper?.getAttribute("href")),
898
+ descendants: Array.from(li.querySelectorAll(CONFIG.SELECTORS.MENU_ITEMS)),
899
+ submenu: li.querySelector(":scope > .leed-submenu"),
900
+ };
901
+ });
902
+ },
903
+
904
+ bindEvents() {
905
+ const runFilter = Utils.debounce(() => this.applyFilter(this.pending), CONFIG.DEBOUNCE_DELAYS.FAST);
906
+ // A debounce of its own, an order of magnitude longer than the filter's: the
907
+ // tree has to keep up with the keystroke, but a polite live region that
908
+ // speaks on every one queues an announcement per character — about eleven
909
+ // for "page editor". Clearing routes through the same debounced setter, so a
910
+ // queued announcement can never outlive the query that produced it.
911
+ this.setStatus = Utils.debounce((message) => {
912
+ this.status.textContent = message;
913
+ }, CONFIG.DEBOUNCE_DELAYS.ANNOUNCE);
914
+ // Read from the event: some engines clear a type="search" input themselves,
915
+ // so the element's value cannot be trusted once the handler has yielded.
916
+ const onQuery = (e) => {
917
+ this.pending = e.target.value;
918
+ runFilter();
919
+ };
920
+
921
+ this.input.addEventListener("input", onQuery);
922
+ // Safari fires `search` on Escape and Enter as well as `input`. Not for its
923
+ // native clear affordance — navigation.css removes that with
924
+ // `::-webkit-search-cancel-button { appearance: none }` so the explicit
925
+ // clear button is the only one on screen.
926
+ this.input.addEventListener("search", onQuery);
927
+
928
+ this.input.addEventListener("keydown", (e) => {
929
+ if (e.key !== "Escape") {
930
+ return;
931
+ }
932
+ // No preventDefault — a native search-input clear reaches the same end
933
+ // state. stopPropagation keeps Escape away from the shell's window-level
934
+ // handler; MenuManager's own handler already early-returns because the
935
+ // input has no [role="menuitem"] ancestor, so no section can collapse.
936
+ e.stopPropagation();
937
+ this.reset();
938
+ });
939
+
940
+ this.clearButton.addEventListener("click", () => {
941
+ this.reset();
942
+ this.input.focus();
943
+ });
944
+
945
+ // A section header is a live control while filtering, not a dead one.
946
+ // MenuManager's toggle derives its next state from the <li>'s aria-expanded,
947
+ // which the reveal deliberately leaves at "false" — so a click on a revealed
948
+ // section would compute "expand" on a section already on screen, move
949
+ // nothing, and leak that flipped state out when the query is cleared.
950
+ // Capture phase runs ahead of MenuManager's @click.stop handler: the filter
951
+ // exits first, the menu is back at its pre-filter state, and the toggle then
952
+ // reads an aria-expanded that matches what the reader is looking at.
953
+ //
954
+ // Only for a click that would actually reach that toggle. aria-haspopup
955
+ // sits on the section <li>, which wraps the whole submenu, so asking for
956
+ // that ancestor alone would reset on every result in the section too — and
957
+ // a target="_blank" or ctrl-clicked result leaves the reader here with
958
+ // their list gone, having bought nothing.
959
+ this.nav.addEventListener(
960
+ "click",
961
+ (e) => {
962
+ if ((this.query || this.pending) && !e.target.closest("a") && this.sectionToggleOf(e.target)) {
963
+ this.reset();
964
+ }
965
+ },
966
+ true,
967
+ );
968
+
969
+ // The keyboard reaches the same toggle by another route, and nothing on the
970
+ // click path guards it: setupKeyboardNavigation listens on this same node
971
+ // and acts on the focused item, so Enter on a revealed header hits exactly
972
+ // the dead-control case above, and Escape on a filtered result collapses
973
+ // the section out from under the reader while data-filtering is still set
974
+ // and the live region still says the result matched. Capture here lands
975
+ // ahead of that handler's bubble-phase listener, same as the click.
976
+ this.nav.addEventListener(
977
+ "keydown",
978
+ (e) => {
979
+ if ((this.query || this.pending) && this.keyTogglesSection(e.target, e.key)) {
980
+ this.reset();
981
+ }
982
+ },
983
+ true,
984
+ );
985
+
986
+ // The box can be hidden three ways — a site's own media query, the drawer's
987
+ // inline display:none, and .docs-sidebar[aria-hidden="true"] — and only the
988
+ // first is a window resize, so watch the element itself as well.
989
+ if (typeof ResizeObserver === "function") {
990
+ new ResizeObserver(() => this.clearWhenHidden()).observe(this.container);
991
+ }
992
+ ResizeManager.addListener(() => this.clearWhenHidden());
993
+ },
994
+
995
+ // isTopLevelMenuItem's own test, on the component that owns the toggles: a
996
+ // direct child of the root .leed-menu. MenuManager binds no toggle there at
997
+ // all — not on click and not on a key — so there is nothing to get out of the
998
+ // way of, and resetting would only cost the reader the query.
999
+ isTopLevelItem(item) {
1000
+ return item.closest(".leed-menu") === item.parentElement;
1001
+ },
1002
+
1003
+ // The section <li> whose toggle an event on this target reaches, or null.
1004
+ sectionToggleOf(target) {
1005
+ const item = target.closest(`[${CONFIG.ARIA.HASPOPUP}='menu']`);
1006
+ return item && !this.isTopLevelItem(item) ? item : null;
1007
+ },
1008
+
1009
+ // Which keydowns setupKeyboardNavigation actually acts on, mirrored rather
1010
+ // than approximated: a key it ignores must not cost the reader the query
1011
+ // either — Space on a focused result scrolls the page and moves no section.
1012
+ // Enter and Space toggle the focused section; Escape collapses the section
1013
+ // the focused item sits inside, which is why an ordinary result is an Escape
1014
+ // target while it is not a click target.
1015
+ keyTogglesSection(target, key) {
1016
+ const item = target.closest("[role='menuitem']");
1017
+
1018
+ if (!item || (item.hasAttribute(CONFIG.ARIA.HASPOPUP) && this.isTopLevelItem(item))) {
1019
+ return false;
1020
+ }
1021
+
1022
+ if (key === "Enter" || key === " ") {
1023
+ return item.hasAttribute(CONFIG.ARIA.HASPOPUP);
1024
+ }
1025
+
1026
+ return key === "Escape" && Boolean(this.sectionToggleOf(item.parentElement));
1027
+ },
1028
+
1029
+ // Not offsetParent === null, the usual idiom in this file: that reports a
1030
+ // visible element as hidden through the fixed drawer panel.
1031
+ isHidden() {
1032
+ return this.container.getClientRects().length === 0;
1033
+ },
1034
+
1035
+ // `pending` as well as `query`: a keystroke inside the debounce window has not
1036
+ // reached applyFilter yet, so gating on the applied query alone would let the
1037
+ // queued run land on a box that is already gone.
1038
+ clearWhenHidden() {
1039
+ if ((this.query || this.pending) && this.isHidden()) {
1040
+ this.reset();
1041
+ }
1042
+ },
1043
+
1044
+ reset() {
1045
+ this.input.value = "";
1046
+ this.pending = "";
1047
+ this.applyFilter("");
1048
+ },
1049
+
1050
+ applyFilter(value) {
1051
+ const tokens = this.normalize(value).split(" ").filter(Boolean);
1052
+
1053
+ if (tokens.length === 0) {
1054
+ this.clearFilter();
1055
+ return;
1056
+ }
1057
+
1058
+ // Names own the normal case. The slug fallback runs only where the reader
1059
+ // would otherwise be looking at "No matches", so a query that finds a title
1060
+ // never consults a URL and the ordinary result cannot regress — while a page
1061
+ // retitled away from its slug stays reachable by the slug.
1062
+ let { visible, matches } = this.collectMatches(tokens, "name");
1063
+ const byLink = matches === 0;
1064
+
1065
+ if (byLink) {
1066
+ ({ visible, matches } = this.collectMatches(tokens, "slug"));
1067
+ }
1068
+
1069
+ for (const item of this.items) {
1070
+ if (visible.has(item.li)) {
1071
+ item.li.removeAttribute("data-filter-hidden");
1072
+ this.revealSubmenu(item);
1073
+ } else {
1074
+ item.li.setAttribute("data-filter-hidden", "");
1075
+ this.restoreSubmenu(item);
1076
+ }
1077
+ }
1078
+
1079
+ this.query = tokens.join(" ");
1080
+ this.nav.setAttribute("data-filtering", "true");
1081
+ this.clearButton.hidden = false;
1082
+ this.empty.hidden = matches > 0;
1083
+ this.announce(matches, byLink);
1084
+ },
1085
+
1086
+ // One pass, parameterized only by which cached text it reads — `name` or
1087
+ // `slug`. Ancestor and subtree visibility live here rather than in the caller
1088
+ // precisely so the fallback cannot grow rules of its own: the two passes are
1089
+ // the same code over a different field, and an item with no text in that field
1090
+ // (a no-link parent's slug, say) is skipped rather than matched by everything.
1091
+ collectMatches(tokens, field) {
1092
+ const visible = new Set();
1093
+ let matches = 0;
1094
+
1095
+ for (const item of this.items) {
1096
+ const haystack = item[field];
1097
+ if (!haystack || !tokens.every((token) => haystack.includes(token))) {
1098
+ continue;
1099
+ }
1100
+
1101
+ matches += 1;
1102
+ visible.add(item.li);
1103
+
1104
+ // Ancestors keep the match reachable — including ancestors that could
1105
+ // never match themselves, which is how a no-link parent stays on screen.
1106
+ let parent = item.li.parentElement;
1107
+ while (parent && parent !== this.nav) {
1108
+ if (parent.matches(CONFIG.SELECTORS.MENU_ITEMS)) {
1109
+ visible.add(parent);
1110
+ }
1111
+ parent = parent.parentElement;
1112
+ }
1113
+
1114
+ // ...and a matching parent keeps its whole subtree.
1115
+ for (const descendant of item.descendants) {
1116
+ visible.add(descendant);
1117
+ }
1118
+ }
1119
+
1120
+ return { visible, matches };
1121
+ },
1122
+
1123
+ // A section kept visible has to be reachable, not merely painted: a submenu
1124
+ // left at aria-hidden="true" holds focusable links that assistive technology
1125
+ // cannot see. The marker records only that the filter did this, never what the
1126
+ // value was — restoreSubmenu derives that again.
1127
+ revealSubmenu(item) {
1128
+ if (item.submenu?.getAttribute(CONFIG.ARIA.HIDDEN) !== "true") {
1129
+ return;
1130
+ }
1131
+
1132
+ item.submenu.setAttribute("data-filter-revealed", "");
1133
+ item.submenu.setAttribute(CONFIG.ARIA.HIDDEN, "false");
1134
+ },
1135
+
1136
+ // Recomputed from the controlling <li>, not restored from a stashed value, so
1137
+ // a section the reader collapsed while filtering stays collapsed afterwards.
1138
+ restoreSubmenu(item) {
1139
+ if (!item.submenu?.hasAttribute("data-filter-revealed")) {
1140
+ return;
1141
+ }
1142
+
1143
+ const expanded = item.li.getAttribute(CONFIG.ARIA.EXPANDED) === "true";
1144
+ item.submenu.removeAttribute("data-filter-revealed");
1145
+ item.submenu.setAttribute(CONFIG.ARIA.HIDDEN, expanded ? "false" : "true");
1146
+ },
1147
+
1148
+ // Counting self-matches rather than the ancestors and subtrees kept for
1149
+ // context is the honest number. textContent only — the reader's query is
1150
+ // never used to build markup.
1151
+ //
1152
+ // A fallback result says so. Every item on screen then matched on its link and
1153
+ // not on any word the reader can see, and a bare "2 items match" would leave a
1154
+ // screen reader user hunting a highlight that is not there.
1155
+ announce(matches, byLink) {
1156
+ if (matches === 0) {
1157
+ this.setStatus("No items match");
1158
+ return;
1159
+ }
1160
+ if (byLink) {
1161
+ this.setStatus(`No title matches — ${matches} ${matches === 1 ? "page" : "pages"} matched by link`);
1162
+ return;
1163
+ }
1164
+ this.setStatus(matches === 1 ? "1 item matches" : `${matches} items match`);
1165
+ },
1166
+
1167
+ clearFilter() {
1168
+ this.nav.removeAttribute("data-filtering");
1169
+ for (const item of this.items) {
1170
+ item.li.removeAttribute("data-filter-hidden");
1171
+ this.restoreSubmenu(item);
1172
+ }
1173
+
1174
+ // `pending` too, not only the applied query: a whitespace-only query filters
1175
+ // nothing and lands here with a populated field and a hidden clear button,
1176
+ // and a pending query left standing would make the next click on a section
1177
+ // header reset — silently emptying a field the reader is still typing in.
1178
+ this.query = "";
1179
+ this.pending = "";
1180
+ this.clearButton.hidden = true;
1181
+ this.empty.hidden = true;
1182
+ this.setStatus("");
1183
+ },
1184
+ };
1185
+
1186
+ // Centralized resize manager to avoid multiple event listeners
1187
+ const ResizeManager = {
1188
+ listeners: [],
1189
+
1190
+ addListener(callback, delay = CONFIG.DEBOUNCE_DELAYS.MEDIUM) {
1191
+ const debouncedCallback = Utils.debounce(callback, delay);
1192
+ this.listeners.push(debouncedCallback);
1193
+ return debouncedCallback;
1194
+ },
1195
+
1196
+ init() {
1197
+ const debouncedResize = Utils.debounce(() => {
1198
+ this.listeners.forEach((listener) => {
1199
+ listener();
1200
+ });
1201
+ }, CONFIG.DEBOUNCE_DELAYS.MEDIUM);
1202
+
1203
+ window.addEventListener("resize", debouncedResize);
1204
+ window.addEventListener("load", debouncedResize);
1205
+ },
1206
+ };
1207
+
1208
+ // TOC manager
1209
+ const TOCManager = {
1210
+ init() {
1211
+ const tocSidebar = DOMCache.get("#leed-toc");
1212
+ if (!tocSidebar) return;
1213
+
1214
+ this.tocSidebar = tocSidebar;
1215
+ this.desktopTocContainer = DOMCache.get("#leed-toc");
1216
+ this.mobileTocContainer = DOMCache.get("#leed-mobile-toc-container");
1217
+ this.miniTocButton = DOMCache.get("#leed-mini-toc-button-text");
1218
+ this.miniTocCurrent = DOMCache.get("#leed-mini-toc-current");
1219
+ this.miniTocDropdown = DOMCache.get("#docs-mini-toc");
1220
+
1221
+ this.setupTOC();
1222
+ this.setupResponsiveBehavior();
1223
+ this.setupScrollTracking();
1224
+ this.setupMobileTocWatcher();
1225
+ },
1226
+
1227
+ setupTOC() {
1228
+ const tocHeading = this.tocSidebar.querySelector("#leed-toc-heading");
1229
+ const headingText = tocHeading ? tocHeading.textContent : "On this page";
1230
+
1231
+ if (this.miniTocButton && this.miniTocButton.textContent === "") {
1232
+ this.miniTocButton.textContent = headingText;
1233
+ }
1234
+
1235
+ this.originalTocParent = null;
1236
+ this.navTocElement = null;
1237
+ this.initializeIcons();
1238
+ },
1239
+
1240
+ initializeIcons() {
1241
+ const tocListItems = document.querySelectorAll(".leed-nav-toc li");
1242
+
1243
+ tocListItems.forEach((li) => {
1244
+ if (!li.querySelector(".current-item")) {
1245
+ const icon = document.createElement("i");
1246
+ icon.className = "current-item";
1247
+
1248
+ const anchor = li.querySelector("a");
1249
+ if (anchor) {
1250
+ li.insertBefore(icon, anchor);
1251
+ }
1252
+ }
1253
+ });
1254
+ },
1255
+
1256
+ setupResponsiveBehavior() {
1257
+ ResizeManager.addListener(() => {
1258
+ this.handleTocPosition();
1259
+ if (Utils.isDesktop() && window.Alpine?.store("sidebar")) {
1260
+ Alpine.store("sidebar").open = false;
1261
+ }
1262
+ });
1263
+ this.handleTocPosition();
1264
+ },
1265
+
1266
+ handleTocPosition() {
1267
+ if (!this.mobileTocContainer) return;
1268
+
1269
+ if (!this.navTocElement) {
1270
+ this.navTocElement = this.tocSidebar.querySelector(".leed-nav-toc");
1271
+ if (!this.navTocElement) return;
1272
+ this.originalTocParent = this.navTocElement.parentNode;
1273
+ }
1274
+
1275
+ if (Utils.isMobile() || Utils.isSmallerThanDesktop()) {
1276
+ if (this.navTocElement.parentNode !== this.mobileTocContainer) {
1277
+ this.mobileTocContainer.appendChild(this.navTocElement);
1278
+ }
1279
+ } else {
1280
+ if (this.navTocElement.parentNode !== this.originalTocParent) {
1281
+ this.originalTocParent.appendChild(this.navTocElement);
1282
+ }
1283
+ }
1284
+
1285
+ this.updateCurrentSection();
1286
+ },
1287
+
1288
+ setupScrollTracking() {
1289
+ const debouncedSetActiveLink = Utils.debounce(() => {
1290
+ this.setActiveLink();
1291
+ }, CONFIG.DEBOUNCE_DELAYS.FAST);
1292
+
1293
+ window.addEventListener("scroll", debouncedSetActiveLink);
1294
+
1295
+ const scrollableDiv = document.querySelector(".fixed-content-layout");
1296
+ if (scrollableDiv) {
1297
+ scrollableDiv.addEventListener("scroll", debouncedSetActiveLink);
1298
+ }
1299
+ this.setActiveLink();
1300
+ },
1301
+
1302
+ setActiveLink() {
1303
+ const tocLinks = document.querySelectorAll(CONFIG.SELECTORS.TOC_LINKS);
1304
+ const sections = Array.from(tocLinks)
1305
+ .map((link) => {
1306
+ const id = link.getAttribute("href")?.substring(1);
1307
+ return id ? document.getElementById(id) : null;
1308
+ })
1309
+ .filter(Boolean);
1310
+
1311
+ let currentSectionIndex = 0;
1312
+
1313
+ sections.forEach((section, index) => {
1314
+ if (!section) return;
1315
+
1316
+ const rect = section.getBoundingClientRect();
1317
+
1318
+ // Get header and TOC heights from HeaderManager
1319
+ const triggerOffset = HeaderManager.getHeaderHeight() + HeaderManager.getTocHeight();
1320
+
1321
+ if (rect.top <= triggerOffset) {
1322
+ currentSectionIndex = index;
1323
+ }
1324
+ });
1325
+
1326
+ tocLinks.forEach((link) => {
1327
+ AriaManager.removeCurrent(link);
1328
+
1329
+ const parentLi = link.closest("li");
1330
+ const icon = parentLi?.querySelector(".current-item");
1331
+ if (icon) {
1332
+ AriaManager.removeCurrent(icon);
1333
+ }
1334
+ });
1335
+
1336
+ if (tocLinks[currentSectionIndex]) {
1337
+ AriaManager.setCurrent(tocLinks[currentSectionIndex], "location");
1338
+
1339
+ const activeLi = tocLinks[currentSectionIndex].closest("li");
1340
+ const activeIcon = activeLi?.querySelector(".current-item");
1341
+ if (activeIcon) {
1342
+ AriaManager.setCurrent(activeIcon, "location");
1343
+ }
1344
+
1345
+ if (this.navTocElement && this.navTocElement.parentNode === this.desktopTocContainer) {
1346
+ this.scrollTocItemIntoView(activeLi, this.desktopTocContainer, "center");
1347
+ } else if (this.navTocElement && this.navTocElement.parentNode === this.mobileTocContainer) {
1348
+ // Check if mobile TOC is visible/open
1349
+ const isMobileTocVisible =
1350
+ this.mobileTocContainer.offsetParent !== null && this.mobileTocContainer.scrollHeight > 0;
1351
+
1352
+ if (isMobileTocVisible) {
1353
+ this.scrollTocItemIntoView(activeLi, this.mobileTocContainer, "start");
1354
+ } else {
1355
+ // Store the active item for when TOC is opened
1356
+ this.pendingMobileScrollItem = activeLi;
1357
+ }
1358
+ }
1359
+ }
1360
+
1361
+ this.updateCurrentSection();
1362
+ },
1363
+
1364
+ updateCurrentSection() {
1365
+ if (!this.miniTocCurrent) return;
1366
+
1367
+ const activeLink = document.querySelector(`${CONFIG.SELECTORS.TOC_LINKS}[${CONFIG.ARIA.CURRENT}="location"]`);
1368
+ if (activeLink) {
1369
+ this.miniTocCurrent.textContent = activeLink.textContent;
1370
+ } else {
1371
+ this.miniTocCurrent.textContent = "Overview";
1372
+ }
1373
+ },
1374
+
1375
+ scrollTocItemIntoView(activeLi, container, block) {
1376
+ if (!activeLi || !container) return;
1377
+
1378
+ // Find the actual scrollable element within the container
1379
+ let scrollableElement = container;
1380
+
1381
+ // Check if the container itself is scrollable
1382
+ if (container.scrollHeight <= container.clientHeight || container.scrollHeight === 0) {
1383
+ // Look for a scrollable child element
1384
+ const scrollableChildren = Array.from(container.querySelectorAll("*")).filter(
1385
+ (el) => el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== "visible",
1386
+ );
1387
+
1388
+ if (scrollableChildren.length > 0) {
1389
+ scrollableElement = scrollableChildren[0];
1390
+ } else {
1391
+ // For mobile, look for the docs-mini-toc element which is the actual scrollable container
1392
+ const docsMiniToc = document.getElementById("docs-mini-toc");
1393
+
1394
+ if (docsMiniToc && docsMiniToc.scrollHeight > docsMiniToc.clientHeight) {
1395
+ scrollableElement = docsMiniToc;
1396
+ } else {
1397
+ return;
1398
+ }
1399
+ }
1400
+ }
1401
+
1402
+ // Calculate the current scroll position within the scrollable element
1403
+ const currentScrollTop = scrollableElement.scrollTop;
1404
+
1405
+ // Calculate the item's position relative to the scrollable element
1406
+ const itemRect = activeLi.getBoundingClientRect();
1407
+ const containerRect = scrollableElement.getBoundingClientRect();
1408
+ const itemTop = itemRect.top - containerRect.top + scrollableElement.scrollTop;
1409
+ const itemHeight = activeLi.offsetHeight;
1410
+ const containerHeight = scrollableElement.clientHeight;
1411
+
1412
+ let targetScrollTop;
1413
+
1414
+ if (block === "center") {
1415
+ // Center the item in the container
1416
+ targetScrollTop = itemTop - containerHeight / 2 + itemHeight / 2;
1417
+ } else if (block === "start") {
1418
+ // Align item to the top of the container
1419
+ targetScrollTop = itemTop;
1420
+ } else {
1421
+ // Default to start
1422
+ targetScrollTop = itemTop;
1423
+ }
1424
+
1425
+ // Ensure we don't scroll beyond the container bounds
1426
+ const maxScrollTop = scrollableElement.scrollHeight - containerHeight;
1427
+ targetScrollTop = Math.max(0, Math.min(targetScrollTop, maxScrollTop));
1428
+
1429
+ // Only scroll if the item is not already visible and we need to scroll
1430
+ if (Math.abs(currentScrollTop - targetScrollTop) > 1) {
1431
+ scrollableElement.scrollTop = targetScrollTop;
1432
+ }
1433
+ },
1434
+
1435
+ // Method to handle mobile TOC opening - call this when mobile TOC becomes visible
1436
+ handleMobileTocOpen() {
1437
+ if (this.pendingMobileScrollItem && this.mobileTocContainer) {
1438
+ // Small delay to ensure the TOC is fully rendered
1439
+ setTimeout(() => {
1440
+ this.scrollTocItemIntoView(this.pendingMobileScrollItem, this.mobileTocContainer, "start");
1441
+ this.pendingMobileScrollItem = null;
1442
+ }, 100);
1443
+ }
1444
+ },
1445
+
1446
+ setupMobileTocWatcher() {
1447
+ // Watch for Alpine.js isOpen changes on the mobile TOC
1448
+ if (window.Alpine && this.mobileTocContainer) {
1449
+ // Find the Alpine component that controls the mobile TOC
1450
+ const alpineComponent = Alpine.$data(this.mobileTocContainer.closest("[x-data]"));
1451
+
1452
+ if (alpineComponent && alpineComponent.isOpen !== undefined) {
1453
+ // Watch for changes to isOpen
1454
+ Alpine.effect(() => {
1455
+ if (alpineComponent.isOpen) {
1456
+ this.handleMobileTocOpen();
1457
+ }
1458
+ });
1459
+ }
1460
+ }
1461
+ },
1462
+ };
1463
+
1464
+ // Header manager - handles header state, responsive behavior, and height calculations
1465
+ const HeaderManager = {
1466
+ init(params = {}) {
1467
+ this.hasScrollBar = params.scrollBarId?.length > 1 || false;
1468
+ this.topMenuOpen = false;
1469
+ this.headerHeight = 0;
1470
+ this.tocHeight = 0;
1471
+ this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
1472
+ this.isSmallest = Utils.isSmallest();
1473
+ this.headerSearchShow = false;
1474
+
1475
+ this.setupHeaderComponent();
1476
+ },
1477
+
1478
+ setupHeaderComponent() {
1479
+ // Calculate initial heights
1480
+ this.calculateHeaderHeight();
1481
+ this.calculateTocHeight();
1482
+
1483
+ // Setup responsive behavior
1484
+ ResizeManager.addListener(() => {
1485
+ this.handleResize();
1486
+ });
1487
+ },
1488
+
1489
+ calculateTocHeight() {
1490
+ const docsMiniBar = document.getElementById("docs-mini-bar");
1491
+ if (!docsMiniBar) {
1492
+ this.tocHeight = 0;
1493
+ return;
1494
+ }
1495
+
1496
+ this.tocHeight = docsMiniBar.offsetHeight;
1497
+ document.documentElement.style.setProperty("--height-toc", `${this.tocHeight}px`);
1498
+ },
1499
+
1500
+ calculateHeaderHeight() {
1501
+ const header = document.getElementById("docs-header-wrapper");
1502
+ if (!header) {
1503
+ this.headerHeight = 0;
1504
+ return;
1505
+ }
1506
+
1507
+ // --height-header is an OUTPUT of this function and never an input to it.
1508
+ //
1509
+ // Two different jobs had been collapsed onto that one name:
1510
+ // SEED the pre-JS estimate a site declares, e.g.
1511
+ // `@theme { --height-header: 6rem; }`. Tailwind v4 also mints a
1512
+ // `.h-header` utility (`height: var(--height-header)`) from it.
1513
+ // MEASURED what this function publishes after looking at the rendered bar,
1514
+ // and what every offset consumes: the docs shell's
1515
+ // `mt-(--height-header)`, `scroll-padding-top` (defaults.css),
1516
+ // the TOC's `top`/`max-h` (components/toc.css), the sidebar.
1517
+ //
1518
+ // A site that puts `h-header` on its fixed header wrapper — leed.ai did —
1519
+ // therefore SIZES that wrapper from the token. The wrapper is out of flow, so
1520
+ // the fallback below measured its children, i.e. the very box the token had
1521
+ // just sized: seed sizes box, box is measured, measurement rewrites seed.
1522
+ // And because that wrapper also carries a `transition: all 200ms`, its height
1523
+ // ANIMATES towards each new value, so the read lands mid-flight; whatever is
1524
+ // sampled becomes the next target, the transition retargets from where it
1525
+ // already is, and the pair sticks at an arbitrary fixed point somewhere
1526
+ // between the seed and the truth. Measured on leed.ai with a 96px seed and a
1527
+ // 73px header: 89px published on /docs/, 73px on a content page, 57px and
1528
+ // 96px in other sessions. The reader sees the whole reading column sitting
1529
+ // up to 39px too low, by a different amount on different loads.
1530
+ //
1531
+ // Clearing the token for the duration of the measurement breaks that loop for
1532
+ // ANY markup: while it is unset, `.h-header` — and anything else derived from
1533
+ // it — computes to `auto`, so every box reports its own content height and
1534
+ // the stale value cannot feed itself back in. The clear and the restore
1535
+ // happen in one synchronous task, so the browser never paints the in-between
1536
+ // state; `height: auto` is not an interpolable endpoint either, so the
1537
+ // transition jumps rather than animating through it.
1538
+ //
1539
+ // Do NOT reorder this to measure first and clear afterwards, and do not merge
1540
+ // the seed and the measured value back under a single name.
1541
+ const root = document.documentElement;
1542
+ const publishedValue = root.style.getPropertyValue("--height-header");
1543
+ root.style.setProperty("--height-header", "auto");
1544
+
1545
+ let height;
1546
+ try {
1547
+ // The wrapper is what the layouts position and what the page body offsets
1548
+ // against via --height-header. A site-supplied header (docs-header.hbs) may
1549
+ // position itself fixed or absolute, which takes it out of the wrapper's
1550
+ // flow and collapses offsetHeight to 0 — measure the children in that case,
1551
+ // or the whole page slides up underneath the header.
1552
+ height = header.offsetHeight;
1553
+ if (!height) {
1554
+ for (const child of header.children) {
1555
+ height = Math.max(height, child.getBoundingClientRect().height);
1556
+ }
1557
+ }
1558
+ } finally {
1559
+ // Put the page back exactly as it was before anything can paint, whatever
1560
+ // happened above. Restoring "" would leave an empty inline declaration
1561
+ // rather than falling through to the site's seed, hence removeProperty.
1562
+ if (publishedValue) {
1563
+ root.style.setProperty("--height-header", publishedValue);
1564
+ } else {
1565
+ root.style.removeProperty("--height-header");
1566
+ }
1567
+ }
1568
+
1569
+ this.headerHeight = height;
1570
+ document.documentElement.style.setProperty("--height-header", `${this.headerHeight}px`);
1571
+ },
1572
+
1573
+ handleResize() {
1574
+ this.calculateTocHeight();
1575
+ this.calculateHeaderHeight();
1576
+ this.topMenuOpen = false;
1577
+ this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
1578
+ this.isSmallest = Utils.isSmallest();
1579
+ },
1580
+
1581
+ // Public methods for external access
1582
+ setTopMenuOpen(open) {
1583
+ this.topMenuOpen = open;
1584
+ },
1585
+
1586
+ setHeaderSearchShow(show) {
1587
+ this.headerSearchShow = show;
1588
+ },
1589
+
1590
+ getHeaderHeight() {
1591
+ return this.headerHeight;
1592
+ },
1593
+
1594
+ getTocHeight() {
1595
+ return this.tocHeight;
1596
+ },
1597
+ };
1598
+
1599
+ // Anchor link manager
1600
+ const AnchorLinkManager = {
1601
+ init() {
1602
+ const contentArea = DOMCache.get("#leed-docs-copy");
1603
+ if (!contentArea) return;
1604
+
1605
+ const headings = contentArea.querySelectorAll("h2, h3, h4");
1606
+
1607
+ headings.forEach((heading) => {
1608
+ if (heading.classList.contains("no-anchor") || !heading.id) return;
1609
+
1610
+ this.createAnchorLink(heading);
1611
+ });
1612
+ },
1613
+
1614
+ createAnchorLink(heading) {
1615
+ // Nothing should run twice over the same heading: a second pass would nest
1616
+ // another wrapper and append a second anchor.
1617
+ if (heading.classList.contains(CONFIG.CLASSES.WITH_ANCHOR)) return;
1618
+
1619
+ const anchorLink = document.createElement("a");
1620
+ anchorLink.href = `#${heading.id}`;
1621
+ anchorLink.classList.add("docs-heading-anchor");
1622
+ anchorLink.setAttribute("aria-label", "Copy link to section");
1623
+
1624
+ const linkIcon = document.createElement("i");
1625
+ anchorLink.appendChild(linkIcon);
1626
+
1627
+ anchorLink.addEventListener("click", (e) => {
1628
+ e.preventDefault();
1629
+ e.stopPropagation();
1630
+
1631
+ const newUrl = `${window.location.pathname}${window.location.search}#${heading.id}`;
1632
+ window.history.pushState(null, "", newUrl);
1633
+
1634
+ navigator.clipboard
1635
+ .writeText(window.location.href)
1636
+ .then(() => {
1637
+ anchorLink.classList.add(CONFIG.CLASSES.COPIED);
1638
+ heading.classList.add(CONFIG.CLASSES.JUST_COPIED);
1639
+
1640
+ Notification.show(anchorLink, "Link copied!", "anchor-copied-notification");
1641
+
1642
+ setTimeout(() => {
1643
+ anchorLink.classList.remove(CONFIG.CLASSES.COPIED);
1644
+ heading.classList.remove(CONFIG.CLASSES.JUST_COPIED);
1645
+ }, CONFIG.NOTIFICATION_TIMEOUT);
1646
+ })
1647
+ .catch((err) => {
1648
+ console.error("Failed to copy link:", err);
1649
+ alert("Failed to copy link. Please try again.");
1650
+ });
1651
+ });
1652
+
1653
+ /**
1654
+ * Collect the heading's own content into a single element before the
1655
+ * heading becomes a flex row.
1656
+ *
1657
+ * `.docs-with-anchor` is `flex items-baseline gap-2`, and its rule grows
1658
+ * `> :not(.docs-heading-anchor)` so the text takes the width and wraps
1659
+ * normally. That assumes the heading has exactly one child, which is true
1660
+ * only of a heading made of plain text. Write `## Where the \`@font-face\`
1661
+ * rules come from` and the h2 holds three nodes — text, `<code>`, text.
1662
+ * Flex then lays out each as its own item: the `<code>` matches the
1663
+ * `:not()` and stretches, the two bare text runs do not, and an 8px gap
1664
+ * opens on either side of it. The sentence comes apart across the line.
1665
+ *
1666
+ * Wrapping first means there is one thing to grow no matter what the
1667
+ * heading contains, and the inline markup inside it is laid out as inline
1668
+ * content again rather than as flex children.
1669
+ */
1670
+ const headingText = document.createElement("span");
1671
+ headingText.className = "docs-heading-text";
1672
+ while (heading.firstChild) {
1673
+ headingText.appendChild(heading.firstChild);
1674
+ }
1675
+ heading.appendChild(headingText);
1676
+
1677
+ heading.classList.add(CONFIG.CLASSES.WITH_ANCHOR);
1678
+ heading.appendChild(anchorLink);
1679
+ },
1680
+ };
1681
+
1682
+ // Select manager for API documentation
1683
+ const SelectManager = {
1684
+ createResponseSelector() {
1685
+ return {
1686
+ selectedStatus: "",
1687
+ selectedContentType: "",
1688
+ availableContentTypes: [],
1689
+ statusOpen: false,
1690
+ contentTypeOpen: false,
1691
+ statusOptionsCount: 0,
1692
+ contentTypeOptionsCount: 0,
1693
+
1694
+ init() {
1695
+ this.$nextTick(() => {
1696
+ // Get status options from the custom dropdown options
1697
+ const statusOptions = this.$el.querySelectorAll(".api-item[data-value]");
1698
+ const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
1699
+ this.statusOptionsCount = availableStatuses.length;
1700
+
1701
+ const storageKey = "leed-tabs-responses";
1702
+ const savedTabIndex = localStorage.getItem(storageKey);
1703
+
1704
+ let initialStatus = "";
1705
+ if (savedTabIndex !== null && savedTabIndex !== undefined) {
1706
+ const arrayIndex = parseInt(savedTabIndex, 10);
1707
+ if (arrayIndex >= 0 && arrayIndex < availableStatuses.length) {
1708
+ initialStatus = availableStatuses[arrayIndex];
1709
+ } else if (availableStatuses.length > 0) {
1710
+ initialStatus = availableStatuses[0];
1711
+ }
1712
+ } else if (availableStatuses.length > 0) {
1713
+ initialStatus = availableStatuses[0];
1714
+ }
1715
+
1716
+ if (initialStatus && availableStatuses.includes(initialStatus)) {
1717
+ this.selectedStatus = initialStatus;
1718
+ this.updateContentTypes();
1719
+ }
1720
+ });
1721
+
1722
+ this.setupTabClickListener();
1723
+ this.$watch("selectedStatus", (value, oldValue) => {
1724
+ if (value && value !== oldValue) {
1725
+ this.updateContentTypes();
1726
+ this.handleStatusChange();
1727
+ }
1728
+ });
1729
+ },
1730
+
1731
+ updateContentTypes() {
1732
+ // Get all content type options and filter by selected status
1733
+ const allOptions = this.$el.querySelectorAll(".api-item[data-status-code]");
1734
+ this.availableContentTypes = Array.from(allOptions)
1735
+ .filter((option) => option.getAttribute("data-status-code") === this.selectedStatus)
1736
+ .map((option) => option.getAttribute("data-value"));
1737
+
1738
+ this.contentTypeOptionsCount = this.availableContentTypes.length;
1739
+ this.selectedContentType = this.availableContentTypes.length > 0 ? this.availableContentTypes[0] : "";
1740
+ },
1741
+
1742
+ setupTabClickListener() {
1743
+ setTimeout(() => {
1744
+ const tabs = document.querySelectorAll('[data-tabs-groupId="responses"] li[role="tab"]');
1745
+ tabs.forEach((tab) => {
1746
+ tab.addEventListener("click", (event) => {
1747
+ const tabIndex = tab.getAttribute("data-tab-index");
1748
+ if (tabIndex !== null) {
1749
+ const arrayIndex = parseInt(tabIndex, 10);
1750
+ const statusOptions = this.$el.querySelectorAll(".api-item[data-value]");
1751
+ const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
1752
+ if (arrayIndex >= 0 && arrayIndex < availableStatuses.length) {
1753
+ const targetStatus = availableStatuses[arrayIndex];
1754
+ this.selectedStatus = targetStatus;
1755
+ this.updateContentTypes();
1756
+ }
1757
+ }
1758
+ });
1759
+ });
1760
+ }, CONFIG.DEBOUNCE_DELAYS.SLOW);
1761
+ },
1762
+
1763
+ handleStatusChange() {
1764
+ const statusOptions = this.$el.querySelectorAll(".api-item[data-value]");
1765
+ const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
1766
+ const statusIndex = availableStatuses.indexOf(this.selectedStatus);
1767
+ if (statusIndex !== -1) {
1768
+ setTimeout(() => {
1769
+ const tabs = document.querySelectorAll('[data-tabs-groupId="responses"] li[role="tab"]');
1770
+ const targetTab = Array.from(tabs).find((tab) => {
1771
+ const tabIndexAttr = tab.getAttribute("data-tab-index");
1772
+ return tabIndexAttr && parseInt(tabIndexAttr, 10) === statusIndex;
1773
+ });
1774
+ if (targetTab) {
1775
+ if (typeof activateTabGroup === "function") {
1776
+ activateTabGroup(targetTab);
1777
+ } else {
1778
+ targetTab.click();
1779
+ }
1780
+ }
1781
+ }, 50);
1782
+ }
1783
+ },
1784
+ };
1785
+ },
1786
+
1787
+ createRequestBodySelector() {
1788
+ return {
1789
+ selectedContentType: "",
1790
+ contentTypes: [],
1791
+ contentTypeOpen: false,
1792
+ contentTypeOptionsCount: 0,
1793
+
1794
+ init() {
1795
+ // Get content types from the custom dropdown options
1796
+ const options = this.$el.querySelectorAll(".api-item[data-value]");
1797
+ this.contentTypes = Array.from(options).map((option) => option.getAttribute("data-value"));
1798
+ this.contentTypeOptionsCount = this.contentTypes.length;
1799
+ this.selectedContentType = this.contentTypes.length > 0 ? this.contentTypes[0] : "";
1800
+ },
1801
+ };
1802
+ },
1803
+
1804
+ createSecuritySelector() {
1805
+ return {
1806
+ selectedSecurity: "",
1807
+ securityOptions: [],
1808
+ securityOpen: false,
1809
+ securityOptionsCount: 0,
1810
+
1811
+ init() {
1812
+ this.$nextTick(() => {
1813
+ // Get security options from the custom dropdown options
1814
+ const options = this.$el.querySelectorAll(".api-item[data-value]");
1815
+ this.securityOptions = Array.from(options).map((option) => option.getAttribute("data-value"));
1816
+ this.securityOptionsCount = this.securityOptions.length;
1817
+ const savedSecurity = localStorage.getItem("api-security-selection");
1818
+
1819
+ if (savedSecurity && this.securityOptions.includes(savedSecurity)) {
1820
+ this.selectedSecurity = savedSecurity;
1821
+ } else if (this.securityOptions.length > 0) {
1822
+ this.selectedSecurity = this.securityOptions[0];
1823
+ }
1824
+
1825
+ if (this.selectedSecurity) {
1826
+ localStorage.setItem("api-security-selection", this.selectedSecurity);
1827
+ }
1828
+ });
1829
+ },
1830
+
1831
+ getSelectedSecurityName() {
1832
+ return this.selectedSecurity;
1833
+ },
1834
+
1835
+ showSecurity() {
1836
+ if (this.selectedSecurity) {
1837
+ localStorage.setItem("api-security-selection", this.selectedSecurity);
1838
+ }
1839
+ },
1840
+ };
1841
+ },
1842
+ };
1843
+
1844
+ // Sidebar manager
1845
+ const SidebarManager = {
1846
+ init() {
1847
+ Alpine.store("sidebar", {
1848
+ open: false,
1849
+ });
1850
+
1851
+ Alpine.data("sidebarNav", () => ({
1852
+ open: false,
1853
+ isSmallerThanDesktop: Utils.isSmallerThanDesktop(),
1854
+ // Tracked separately from `isSmallerThanDesktop`: the undebounced listener
1855
+ // in init() has already updated that by the time the debounced template
1856
+ // handler runs, so it cannot be compared against to detect a crossing.
1857
+ wasSmallerThanDesktop: Utils.isSmallerThanDesktop(),
1858
+
1859
+ init() {
1860
+ this.$watch("$store.sidebar.open", (value) => {
1861
+ this.open = value;
1862
+ });
1863
+
1864
+ window.addEventListener("resize", () => {
1865
+ // Deliberately does NOT touch wasSmallerThanDesktop: this listener is
1866
+ // undebounced, so syncing here would make the debounced handleResize()
1867
+ // always compare equal and never detect a crossing.
1868
+ this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
1869
+ });
1870
+ window.addEventListener("load", () => {
1871
+ this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
1872
+ // No resize fires for `load`, so handleResize() never runs to resync.
1873
+ // Without this the tracker keeps the pre-load side and the next real
1874
+ // crossing compares equal, skipping the close().
1875
+ this.wasSmallerThanDesktop = this.isSmallerThanDesktop;
1876
+ });
1877
+ },
1878
+
1879
+ toggle() {
1880
+ this.open = !this.open;
1881
+ this.$store.sidebar.open = this.open;
1882
+ },
1883
+
1884
+ close() {
1885
+ this.open = false;
1886
+ this.$store.sidebar.open = false;
1887
+ },
1888
+
1889
+ // Closing on every window resize also closed the drawer when a mobile
1890
+ // soft keyboard opened, which made the nav filter unusable there. Only an
1891
+ // actual breakpoint crossing — the handler's real intent — closes it.
1892
+ handleResize() {
1893
+ const isSmaller = Utils.isSmallerThanDesktop();
1894
+ this.isSmallerThanDesktop = isSmaller;
1895
+
1896
+ if (isSmaller === this.wasSmallerThanDesktop) {
1897
+ return;
1898
+ }
1899
+
1900
+ this.wasSmallerThanDesktop = isSmaller;
1901
+ this.close();
1902
+ },
1903
+
1904
+ isVisible() {
1905
+ return this.open || !this.isSmallerThanDesktop;
1906
+ },
1907
+ }));
1908
+ },
1909
+ };
1910
+
1911
+ // Alpine.js components initialization
1912
+ document.addEventListener("alpine:init", () => {
1913
+ Alpine.data("leedDocsNavigation", () => MenuManager.createComponent());
1914
+ Alpine.data("responseSelector", () => SelectManager.createResponseSelector());
1915
+ Alpine.data("requestBodySelector", () => SelectManager.createRequestBodySelector());
1916
+ Alpine.data("securitySelector", () => SelectManager.createSecuritySelector());
1917
+
1918
+ // Register the header component immediately when Alpine is available
1919
+ Alpine.data("documentationHeaderComponent", (params = {}) => ({
1920
+ hasScrollBar: params.scrollBarId?.length > 1 || false,
1921
+ topMenuOpen: false,
1922
+ headerHeight: 0,
1923
+ tocHeight: 0,
1924
+ isSmallerThanDesktop: Utils.isSmallerThanDesktop(),
1925
+ isSmallest: Utils.isSmallest(),
1926
+ headerSearchShow: false,
1927
+
1928
+ calculateTOCHeight() {
1929
+ HeaderManager.calculateTocHeight();
1930
+ this.tocHeight = HeaderManager.getTocHeight();
1931
+ },
1932
+
1933
+ calculateHeaderHeight() {
1934
+ HeaderManager.calculateHeaderHeight();
1935
+ this.headerHeight = HeaderManager.getHeaderHeight();
1936
+ },
1937
+
1938
+ handleResize() {
1939
+ HeaderManager.handleResize();
1940
+ this.topMenuOpen = HeaderManager.topMenuOpen;
1941
+ this.isSmallerThanDesktop = HeaderManager.isSmallerThanDesktop;
1942
+ this.isSmallest = HeaderManager.isSmallest;
1943
+ this.headerHeight = HeaderManager.getHeaderHeight();
1944
+ this.tocHeight = HeaderManager.getTocHeight();
1945
+ },
1946
+
1947
+ init() {
1948
+ this.handleResize();
1949
+ },
1950
+ }));
1951
+
1952
+ SidebarManager.init();
1953
+ });
1954
+
1955
+ // API Tab Dropdown Manager - converts tablists in .api-sidebar-content to dropdowns
1956
+ const ApiTabDropdownManager = {
1957
+ dropdowns: [],
1958
+
1959
+ init() {
1960
+ const apiSidebarContent = document.querySelector(".api-sidebar-content");
1961
+ if (!apiSidebarContent) return;
1962
+
1963
+ // Find all tab containers
1964
+ const tabContainers = apiSidebarContent.querySelectorAll(".leed-tabs__container");
1965
+
1966
+ tabContainers.forEach((container, index) => {
1967
+ const tablist = container.querySelector('ul[role="tablist"]');
1968
+ if (!tablist) return;
1969
+
1970
+ // First tablist always converts, others check width
1971
+ if (index === 0) {
1972
+ this.createDropdown(tablist, container);
1973
+ } else {
1974
+ // Check if tablist overflows (exceeds 80% of container width)
1975
+ if (this.shouldConvertToDropdown(tablist, container)) {
1976
+ this.createDropdown(tablist, container);
1977
+ }
1978
+ }
1979
+ });
1980
+
1981
+ // Close dropdowns when clicking outside
1982
+ document.addEventListener("click", (e) => {
1983
+ if (!e.target.closest(".api-tab-dropdown")) {
1984
+ this.closeAllDropdowns();
1985
+ }
1986
+ });
1987
+
1988
+ // Listen for tab changes to update dropdown display
1989
+ this.observeTabChanges();
1990
+
1991
+ // Recheck on resize for non-first tablists
1992
+ ResizeManager.addListener(() => {
1993
+ this.handleResize();
1994
+ });
1995
+ },
1996
+
1997
+ shouldConvertToDropdown(tablist, container) {
1998
+ // Temporarily show tablist if it's hidden to get accurate measurements
1999
+ const wasHidden = tablist.style.display === "none";
2000
+ const originalStyles = {
2001
+ display: tablist.style.display,
2002
+ visibility: tablist.style.visibility,
2003
+ position: tablist.style.position,
2004
+ overflow: tablist.style.overflow,
2005
+ width: tablist.style.width,
2006
+ maxWidth: tablist.style.maxWidth,
2007
+ };
2008
+
2009
+ if (wasHidden) {
2010
+ // Make it invisible but still in layout for measurement
2011
+ tablist.style.display = "";
2012
+ tablist.style.visibility = "hidden";
2013
+ tablist.style.position = "absolute";
2014
+ tablist.style.overflow = "visible";
2015
+ tablist.style.width = "auto";
2016
+ tablist.style.maxWidth = "none";
2017
+ } else {
2018
+ // Temporarily remove any width constraints to get natural width
2019
+ tablist.style.width = "auto";
2020
+ tablist.style.maxWidth = "none";
2021
+ }
2022
+
2023
+ // Force a reflow to ensure accurate measurements
2024
+ void tablist.offsetWidth;
2025
+
2026
+ // Get the natural width of the tablist (all tabs laid out without constraints)
2027
+ // Use scrollWidth which gives the full content width
2028
+ const tablistNaturalWidth = tablist.scrollWidth;
2029
+
2030
+ // Check if there's a wrapper (like .leed-tabs__container) that constrains the width
2031
+ const wrapper = tablist.closest(".leed-tabs__container");
2032
+ let availableWidth;
2033
+
2034
+ if (wrapper) {
2035
+ // Use wrapper's available width if it exists
2036
+ const wrapperRect = wrapper.getBoundingClientRect();
2037
+ availableWidth = wrapperRect.width;
2038
+ } else {
2039
+ // Use container's available width
2040
+ const containerRect = container.getBoundingClientRect();
2041
+ availableWidth = containerRect.width;
2042
+ }
2043
+
2044
+ const threshold = (80 / 100) * availableWidth;
2045
+
2046
+ // Restore original styles
2047
+ Object.keys(originalStyles).forEach((key) => {
2048
+ tablist.style[key] = originalStyles[key] || "";
2049
+ });
2050
+
2051
+ // Check if tablist natural width exceeds 80% of the available width
2052
+ const shouldConvert = tablistNaturalWidth > threshold;
2053
+
2054
+ return shouldConvert;
2055
+ },
2056
+
2057
+ createDropdown(tablist, container) {
2058
+ const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
2059
+ if (tabs.length === 0) return;
2060
+
2061
+ // Find the active tab
2062
+ const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0];
2063
+
2064
+ // Create dropdown wrapper
2065
+ const dropdown = document.createElement("div");
2066
+ dropdown.className = "api-tab-dropdown";
2067
+
2068
+ // Create button
2069
+ const button = document.createElement("button");
2070
+ button.className = "api-tab-dropdown-button";
2071
+ button.setAttribute("type", "button");
2072
+ button.setAttribute("aria-expanded", "false");
2073
+ button.setAttribute("aria-haspopup", "true");
2074
+ button.setAttribute("aria-controls", `api-tab-dropdown-menu-${this.dropdowns.length}`);
2075
+
2076
+ // Get button content from active tab
2077
+ const buttonContent = this.getTabContent(activeTab);
2078
+ button.innerHTML = buttonContent;
2079
+
2080
+ // Create menu
2081
+ const menu = document.createElement("div");
2082
+ menu.className = "api-tab-dropdown-menu";
2083
+ menu.id = `api-tab-dropdown-menu-${this.dropdowns.length}`;
2084
+ menu.setAttribute("role", "menu");
2085
+ menu.setAttribute("aria-hidden", "true");
2086
+
2087
+ // Create menu items from tabs
2088
+ tabs.forEach((tab) => {
2089
+ const menuItem = document.createElement("div");
2090
+ menuItem.className = "api-tab-dropdown-item";
2091
+ menuItem.setAttribute("role", "menuitem");
2092
+ menuItem.setAttribute("tabindex", "-1");
2093
+ menuItem.setAttribute("data-tab-id", tab.id);
2094
+ menuItem.setAttribute("aria-selected", tab.getAttribute("aria-selected") === "true" ? "true" : "false");
2095
+
2096
+ const tabContent = this.getTabContent(tab);
2097
+ menuItem.innerHTML = tabContent;
2098
+
2099
+ menuItem.addEventListener("click", () => {
2100
+ this.selectTab(tab, button, menu);
2101
+ });
2102
+
2103
+ menu.appendChild(menuItem);
2104
+ });
2105
+
2106
+ // Button click handler
2107
+ button.addEventListener("click", (e) => {
2108
+ e.stopPropagation();
2109
+ const isExpanded = button.getAttribute("aria-expanded") === "true";
2110
+ this.toggleDropdown(button, menu, !isExpanded);
2111
+ });
2112
+
2113
+ // Keyboard navigation
2114
+ button.addEventListener("keydown", (e) => {
2115
+ if (e.key === "Enter" || e.key === " ") {
2116
+ e.preventDefault();
2117
+ const isExpanded = button.getAttribute("aria-expanded") === "true";
2118
+ this.toggleDropdown(button, menu, !isExpanded);
2119
+ } else if (e.key === "Escape") {
2120
+ this.toggleDropdown(button, menu, false);
2121
+ }
2122
+ });
2123
+
2124
+ menu.addEventListener("keydown", (e) => {
2125
+ const items = Array.from(menu.querySelectorAll(".api-tab-dropdown-item"));
2126
+ const currentIndex = items.indexOf(e.target);
2127
+
2128
+ if (e.key === "ArrowDown") {
2129
+ e.preventDefault();
2130
+ const nextIndex = (currentIndex + 1) % items.length;
2131
+ items[nextIndex].focus();
2132
+ } else if (e.key === "ArrowUp") {
2133
+ e.preventDefault();
2134
+ const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
2135
+ items[prevIndex].focus();
2136
+ } else if (e.key === "Enter" || e.key === " ") {
2137
+ e.preventDefault();
2138
+ const tabId = e.target.getAttribute("data-tab-id");
2139
+ const tab = document.getElementById(tabId);
2140
+ if (tab) {
2141
+ this.selectTab(tab, button, menu);
2142
+ }
2143
+ } else if (e.key === "Escape") {
2144
+ this.toggleDropdown(button, menu, false);
2145
+ button.focus();
2146
+ }
2147
+ });
2148
+
2149
+ dropdown.appendChild(button);
2150
+ dropdown.appendChild(menu);
2151
+
2152
+ // Insert dropdown before the tablist
2153
+ tablist.parentNode.insertBefore(dropdown, tablist);
2154
+
2155
+ // Hide the original tablist by adding a class and setting display
2156
+ tablist.classList.add("converted-to-dropdown");
2157
+ tablist.style.display = "none";
2158
+
2159
+ // Store reference
2160
+ this.dropdowns.push({ dropdown, button, menu, tablist, tabs, container });
2161
+ },
2162
+
2163
+ getTabContent(tab) {
2164
+ // Clone the tab content (including icons, text, and any nested elements)
2165
+ const clone = tab.cloneNode(true);
2166
+ // Remove any classes that might interfere with styling
2167
+ clone.className = "";
2168
+ // Remove any attributes that might interfere
2169
+ clone.removeAttribute("role");
2170
+ clone.removeAttribute("aria-selected");
2171
+ clone.removeAttribute("aria-controls");
2172
+ clone.removeAttribute("tabindex");
2173
+ clone.removeAttribute("id");
2174
+ clone.removeAttribute("data-tab-index");
2175
+ // Return the innerHTML which includes all content dynamically
2176
+ return clone.innerHTML;
2177
+ },
2178
+
2179
+ toggleDropdown(button, menu, isOpen) {
2180
+ button.setAttribute("aria-expanded", isOpen.toString());
2181
+ menu.setAttribute("aria-hidden", (!isOpen).toString());
2182
+
2183
+ if (isOpen) {
2184
+ // Focus first item when opening
2185
+ const firstItem = menu.querySelector(".api-tab-dropdown-item");
2186
+ if (firstItem) {
2187
+ firstItem.setAttribute("tabindex", "0");
2188
+ firstItem.focus();
2189
+ // Set other items to -1
2190
+ menu.querySelectorAll(".api-tab-dropdown-item").forEach((item, index) => {
2191
+ if (item !== firstItem) {
2192
+ item.setAttribute("tabindex", "-1");
2193
+ }
2194
+ });
2195
+ }
2196
+ } else {
2197
+ // Reset tabindex when closing
2198
+ menu.querySelectorAll(".api-tab-dropdown-item").forEach((item) => {
2199
+ item.removeAttribute("tabindex");
2200
+ });
2201
+ }
2202
+ },
2203
+
2204
+ selectTab(tab, button, menu) {
2205
+ // Activate the tab using TabManager
2206
+ if (TabManager?.activateTabGroup) {
2207
+ TabManager.activateTabGroup(tab);
2208
+ } else {
2209
+ // Fallback: click the tab directly
2210
+ tab.click();
2211
+ }
2212
+
2213
+ // Update button content
2214
+ const buttonContent = this.getTabContent(tab);
2215
+ button.innerHTML = buttonContent;
2216
+
2217
+ // Update menu items
2218
+ menu.querySelectorAll(".api-tab-dropdown-item").forEach((item) => {
2219
+ const tabId = item.getAttribute("data-tab-id");
2220
+ if (tabId === tab.id) {
2221
+ item.setAttribute("aria-selected", "true");
2222
+ } else {
2223
+ item.setAttribute("aria-selected", "false");
2224
+ }
2225
+ });
2226
+
2227
+ // Sync with response selector if this is a responses tab
2228
+ this.syncWithResponseSelector(tab);
2229
+
2230
+ // Close dropdown
2231
+ this.toggleDropdown(button, menu, false);
2232
+ },
2233
+
2234
+ syncWithResponseSelector(tab) {
2235
+ // Check if this tab is part of the responses group
2236
+ const tabContainer = tab.closest(".leed-tabs__container");
2237
+ const groupId = tabContainer?.getAttribute("data-tabs-groupId");
2238
+
2239
+ if (groupId === "responses") {
2240
+ // Find the response selector component - try multiple selectors
2241
+ let responseSection = document.querySelector('.api-section[x-data="responseSelector()"]');
2242
+ if (!responseSection) {
2243
+ responseSection = document.querySelector('.api-section[x-data*="responseSelector"]');
2244
+ }
2245
+ if (!responseSection || !window.Alpine) return;
2246
+
2247
+ // Get the Alpine component instance
2248
+ const responseSelector = Alpine.$data(responseSection);
2249
+ if (!responseSelector) return;
2250
+
2251
+ // Get the tab index
2252
+ const tabIndex = tab.getAttribute("data-tab-index");
2253
+ if (tabIndex === null) return;
2254
+
2255
+ const arrayIndex = parseInt(tabIndex, 10);
2256
+
2257
+ // Get available statuses from the response selector
2258
+ // Look for status options (not content type options which have data-status-code)
2259
+ const statusOptions = responseSection.querySelectorAll(".api-item[data-value]:not([data-status-code])");
2260
+ const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
2261
+
2262
+ // Update selectedStatus if index is valid
2263
+ if (
2264
+ arrayIndex >= 0 &&
2265
+ arrayIndex < availableStatuses.length &&
2266
+ responseSelector.selectedStatus !== availableStatuses[arrayIndex]
2267
+ ) {
2268
+ responseSelector.selectedStatus = availableStatuses[arrayIndex];
2269
+ // updateContentTypes will be called automatically via the $watch
2270
+ }
2271
+ }
2272
+ },
2273
+
2274
+ closeAllDropdowns() {
2275
+ this.dropdowns.forEach(({ button, menu }) => {
2276
+ this.toggleDropdown(button, menu, false);
2277
+ });
2278
+ },
2279
+
2280
+ observeTabChanges() {
2281
+ // Use MutationObserver to watch for tab changes
2282
+ this.dropdowns.forEach(({ tablist, button, menu }) => {
2283
+ const observer = new MutationObserver(() => {
2284
+ const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
2285
+ const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true");
2286
+
2287
+ if (activeTab) {
2288
+ // Update button content
2289
+ const buttonContent = this.getTabContent(activeTab);
2290
+ button.innerHTML = buttonContent;
2291
+
2292
+ // Update menu items
2293
+ menu.querySelectorAll(".api-tab-dropdown-item").forEach((item) => {
2294
+ const tabId = item.getAttribute("data-tab-id");
2295
+ if (tabId === activeTab.id) {
2296
+ item.setAttribute("aria-selected", "true");
2297
+ } else {
2298
+ item.setAttribute("aria-selected", "false");
2299
+ }
2300
+ });
2301
+
2302
+ // Sync with response selector if this is a responses tab
2303
+ this.syncWithResponseSelector(activeTab);
2304
+ }
2305
+ });
2306
+
2307
+ observer.observe(tablist, {
2308
+ attributes: true,
2309
+ attributeFilter: ["aria-selected"],
2310
+ subtree: true,
2311
+ });
2312
+ });
2313
+ },
2314
+
2315
+ handleResize() {
2316
+ // Recheck all containers and convert/remove dropdowns as needed
2317
+ const apiSidebarContent = document.querySelector(".api-sidebar-content");
2318
+ if (!apiSidebarContent) return;
2319
+
2320
+ const tabContainers = apiSidebarContent.querySelectorAll(".leed-tabs__container");
2321
+
2322
+ tabContainers.forEach((container, index) => {
2323
+ const tablist = container.querySelector('ul[role="tablist"]');
2324
+ if (!tablist) return;
2325
+
2326
+ // Check if we already have a dropdown for this tablist
2327
+ const existingDropdown = this.dropdowns.find((d) => d.tablist === tablist && d.container === container);
2328
+
2329
+ // First tablist always converts, others check width
2330
+ let shouldConvert = false;
2331
+ if (index === 0) {
2332
+ shouldConvert = true;
2333
+ } else {
2334
+ shouldConvert = this.shouldConvertToDropdown(tablist, container);
2335
+ }
2336
+
2337
+ if (shouldConvert && !existingDropdown) {
2338
+ // Need to create dropdown
2339
+ this.createDropdown(tablist, container);
2340
+ } else if (!shouldConvert && existingDropdown && index !== 0) {
2341
+ // Need to remove dropdown and show tablist (but not for first tablist)
2342
+ if (existingDropdown.tablist) {
2343
+ existingDropdown.tablist.classList.remove("converted-to-dropdown");
2344
+ existingDropdown.tablist.style.display = "";
2345
+ }
2346
+ if (existingDropdown.dropdown?.parentNode) {
2347
+ existingDropdown.dropdown.remove();
2348
+ }
2349
+ this.dropdowns = this.dropdowns.filter((d) => d !== existingDropdown);
2350
+ }
2351
+ });
2352
+ },
2353
+ };
2354
+
2355
+ // Docs Tab Dropdown Manager - converts overflowing tablists in #leed-docs-copy to dropdowns
2356
+ const DocsTabDropdownManager = {
2357
+ dropdowns: [],
2358
+
2359
+ init() {
2360
+ const docsContent = document.getElementById("leed-docs-copy");
2361
+ if (!docsContent) return;
2362
+
2363
+ // Find all tab containers under #leed-docs-copy
2364
+ const tabContainers = docsContent.querySelectorAll(".leed-tabs__container");
2365
+
2366
+ tabContainers.forEach((container) => {
2367
+ // Skip containers that are inside .api-sidebar-content (handled by ApiTabDropdownManager)
2368
+ if (container.closest(".api-sidebar-content")) {
2369
+ return;
2370
+ }
2371
+
2372
+ const tablist = container.querySelector('ul[role="tablist"]');
2373
+ if (!tablist) return;
2374
+
2375
+ // Check if tablist overflows
2376
+ if (this.shouldConvertToDropdown(tablist, container)) {
2377
+ this.createDropdown(tablist, container);
2378
+ }
2379
+ });
2380
+
2381
+ // Close dropdowns when clicking outside
2382
+ document.addEventListener("click", (e) => {
2383
+ if (!e.target.closest(".docs-tab-dropdown")) {
2384
+ this.closeAllDropdowns();
2385
+ }
2386
+ });
2387
+
2388
+ // Listen for tab changes to update dropdown display
2389
+ this.observeTabChanges();
2390
+
2391
+ // Delay resize listener registration to avoid immediate resize event from ResizeManager.init()
2392
+ // which fires on window.load
2393
+ requestAnimationFrame(() => {
2394
+ requestAnimationFrame(() => {
2395
+ // Recheck on resize
2396
+ ResizeManager.addListener(() => {
2397
+ this.handleResize();
2398
+ });
2399
+ });
2400
+ });
2401
+ },
2402
+
2403
+ shouldConvertToDropdown(tablist, container) {
2404
+ // Temporarily show tablist if it's hidden to get accurate measurements
2405
+ const wasHidden = tablist.style.display === "none" || tablist.classList.contains("converted-to-dropdown");
2406
+ const hadClass = tablist.classList.contains("converted-to-dropdown");
2407
+ const originalStyles = {
2408
+ display: tablist.style.display,
2409
+ visibility: tablist.style.visibility,
2410
+ position: tablist.style.position,
2411
+ overflow: tablist.style.overflow,
2412
+ width: tablist.style.width,
2413
+ maxWidth: tablist.style.maxWidth,
2414
+ };
2415
+
2416
+ if (wasHidden) {
2417
+ // Temporarily remove the class if present
2418
+ if (hadClass) {
2419
+ tablist.classList.remove("converted-to-dropdown");
2420
+ }
2421
+ // Make it invisible but still in layout for measurement
2422
+ tablist.style.display = "";
2423
+ tablist.style.visibility = "hidden";
2424
+ tablist.style.position = "absolute";
2425
+ tablist.style.overflow = "visible";
2426
+ tablist.style.width = "auto";
2427
+ tablist.style.maxWidth = "none";
2428
+ } else {
2429
+ // Temporarily remove any width constraints to get natural width
2430
+ tablist.style.width = "auto";
2431
+ tablist.style.maxWidth = "none";
2432
+ }
2433
+
2434
+ // Force a reflow to ensure accurate measurements
2435
+ void tablist.offsetWidth;
2436
+
2437
+ // Get the natural width of the tablist (all tabs laid out without constraints)
2438
+ // Use scrollWidth which gives the full content width
2439
+ const tablistNaturalWidth = tablist.scrollWidth;
2440
+
2441
+ // Check if there's a wrapper (like .leed-tabs__container) that constrains the width
2442
+ const wrapper = tablist.closest(".leed-tabs__container");
2443
+ let availableWidth;
2444
+
2445
+ if (wrapper) {
2446
+ // Use wrapper's available width if it exists
2447
+ const wrapperRect = wrapper.getBoundingClientRect();
2448
+ availableWidth = wrapperRect.width;
2449
+ } else {
2450
+ // Use container's available width
2451
+ const containerRect = container.getBoundingClientRect();
2452
+ availableWidth = containerRect.width;
2453
+ }
2454
+
2455
+ const threshold = (80 / 100) * availableWidth;
2456
+
2457
+ // Restore original styles
2458
+ Object.keys(originalStyles).forEach((key) => {
2459
+ tablist.style[key] = originalStyles[key] || "";
2460
+ });
2461
+
2462
+ // Restore the class if it was present
2463
+ if (hadClass) {
2464
+ tablist.classList.add("converted-to-dropdown");
2465
+ }
2466
+
2467
+ // Check if tablist natural width exceeds 80% of the available width
2468
+ const shouldConvert = tablistNaturalWidth > threshold;
2469
+
2470
+ return shouldConvert;
2471
+ },
2472
+
2473
+ createDropdown(tablist, container) {
2474
+ const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
2475
+ if (tabs.length === 0) return;
2476
+
2477
+ // Find the active tab
2478
+ const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0];
2479
+
2480
+ // Create dropdown wrapper
2481
+ const dropdown = document.createElement("div");
2482
+ dropdown.className = "docs-tab-dropdown";
2483
+
2484
+ // Create button
2485
+ const button = document.createElement("button");
2486
+ button.className = "docs-tab-dropdown-button";
2487
+ button.setAttribute("type", "button");
2488
+ button.setAttribute("aria-expanded", "false");
2489
+ button.setAttribute("aria-haspopup", "true");
2490
+ button.setAttribute("aria-controls", `docs-tab-dropdown-menu-${this.dropdowns.length}`);
2491
+
2492
+ // Get button content from active tab
2493
+ const buttonContent = this.getTabContent(activeTab);
2494
+ button.innerHTML = buttonContent;
2495
+
2496
+ // Create menu
2497
+ const menu = document.createElement("div");
2498
+ menu.className = "docs-tab-dropdown-menu";
2499
+ menu.id = `docs-tab-dropdown-menu-${this.dropdowns.length}`;
2500
+ menu.setAttribute("role", "menu");
2501
+ menu.setAttribute("aria-hidden", "true");
2502
+
2503
+ // Create menu items from tabs
2504
+ tabs.forEach((tab) => {
2505
+ const menuItem = document.createElement("div");
2506
+ menuItem.className = "docs-tab-dropdown-item";
2507
+ menuItem.setAttribute("role", "menuitem");
2508
+ menuItem.setAttribute("tabindex", "-1");
2509
+ menuItem.setAttribute("data-tab-id", tab.id);
2510
+ menuItem.setAttribute("aria-selected", tab.getAttribute("aria-selected") === "true" ? "true" : "false");
2511
+
2512
+ const tabContent = this.getTabContent(tab);
2513
+ menuItem.innerHTML = tabContent;
2514
+
2515
+ menuItem.addEventListener("click", () => {
2516
+ this.selectTab(tab, button, menu);
2517
+ });
2518
+
2519
+ menu.appendChild(menuItem);
2520
+ });
2521
+
2522
+ // Button click handler
2523
+ button.addEventListener("click", (e) => {
2524
+ e.stopPropagation();
2525
+ const isExpanded = button.getAttribute("aria-expanded") === "true";
2526
+ this.toggleDropdown(button, menu, !isExpanded);
2527
+ });
2528
+
2529
+ // Keyboard navigation
2530
+ button.addEventListener("keydown", (e) => {
2531
+ if (e.key === "Enter" || e.key === " ") {
2532
+ e.preventDefault();
2533
+ const isExpanded = button.getAttribute("aria-expanded") === "true";
2534
+ this.toggleDropdown(button, menu, !isExpanded);
2535
+ } else if (e.key === "Escape") {
2536
+ this.toggleDropdown(button, menu, false);
2537
+ }
2538
+ });
2539
+
2540
+ menu.addEventListener("keydown", (e) => {
2541
+ const items = Array.from(menu.querySelectorAll(".docs-tab-dropdown-item"));
2542
+ const currentIndex = items.indexOf(e.target);
2543
+
2544
+ if (e.key === "ArrowDown") {
2545
+ e.preventDefault();
2546
+ const nextIndex = (currentIndex + 1) % items.length;
2547
+ items[nextIndex].focus();
2548
+ } else if (e.key === "ArrowUp") {
2549
+ e.preventDefault();
2550
+ const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
2551
+ items[prevIndex].focus();
2552
+ } else if (e.key === "Enter" || e.key === " ") {
2553
+ e.preventDefault();
2554
+ const tabId = e.target.getAttribute("data-tab-id");
2555
+ const tab = document.getElementById(tabId);
2556
+ if (tab) {
2557
+ this.selectTab(tab, button, menu);
2558
+ }
2559
+ } else if (e.key === "Escape") {
2560
+ this.toggleDropdown(button, menu, false);
2561
+ button.focus();
2562
+ }
2563
+ });
2564
+
2565
+ dropdown.appendChild(button);
2566
+ dropdown.appendChild(menu);
2567
+
2568
+ // Insert dropdown before the tablist
2569
+ tablist.parentNode.insertBefore(dropdown, tablist);
2570
+
2571
+ // Hide the original tablist by adding a class and setting display
2572
+ tablist.classList.add("converted-to-dropdown");
2573
+ tablist.style.display = "none";
2574
+
2575
+ // Store reference
2576
+ this.dropdowns.push({ dropdown, button, menu, tablist, tabs, container });
2577
+ },
2578
+
2579
+ getTabContent(tab) {
2580
+ // Clone the tab content (including icons, text, and any nested elements)
2581
+ const clone = tab.cloneNode(true);
2582
+ // Remove any classes that might interfere with styling
2583
+ clone.className = "";
2584
+ // Remove any attributes that might interfere
2585
+ clone.removeAttribute("role");
2586
+ clone.removeAttribute("aria-selected");
2587
+ clone.removeAttribute("aria-controls");
2588
+ clone.removeAttribute("tabindex");
2589
+ clone.removeAttribute("id");
2590
+ clone.removeAttribute("data-tab-index");
2591
+ // Return the innerHTML which includes all content dynamically
2592
+ return clone.innerHTML;
2593
+ },
2594
+
2595
+ toggleDropdown(button, menu, isOpen) {
2596
+ button.setAttribute("aria-expanded", isOpen.toString());
2597
+ menu.setAttribute("aria-hidden", (!isOpen).toString());
2598
+
2599
+ if (isOpen) {
2600
+ // Focus first item when opening
2601
+ const firstItem = menu.querySelector(".docs-tab-dropdown-item");
2602
+ if (firstItem) {
2603
+ firstItem.setAttribute("tabindex", "0");
2604
+ firstItem.focus();
2605
+ // Set other items to -1
2606
+ menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
2607
+ if (item !== firstItem) {
2608
+ item.setAttribute("tabindex", "-1");
2609
+ }
2610
+ });
2611
+ }
2612
+ } else {
2613
+ // Reset tabindex when closing
2614
+ menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
2615
+ item.removeAttribute("tabindex");
2616
+ });
2617
+ }
2618
+ },
2619
+
2620
+ selectTab(tab, button, menu) {
2621
+ // Activate the tab using TabManager
2622
+ if (TabManager?.activateTabGroup) {
2623
+ TabManager.activateTabGroup(tab);
2624
+ } else {
2625
+ // Fallback: click the tab directly
2626
+ tab.click();
2627
+ }
2628
+
2629
+ // Update button content
2630
+ const buttonContent = this.getTabContent(tab);
2631
+ button.innerHTML = buttonContent;
2632
+
2633
+ // Update menu items
2634
+ menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
2635
+ const tabId = item.getAttribute("data-tab-id");
2636
+ if (tabId === tab.id) {
2637
+ item.setAttribute("aria-selected", "true");
2638
+ } else {
2639
+ item.setAttribute("aria-selected", "false");
2640
+ }
2641
+ });
2642
+
2643
+ // Close dropdown
2644
+ this.toggleDropdown(button, menu, false);
2645
+ },
2646
+
2647
+ closeAllDropdowns() {
2648
+ this.dropdowns.forEach(({ button, menu }) => {
2649
+ this.toggleDropdown(button, menu, false);
2650
+ });
2651
+ },
2652
+
2653
+ observeTabChanges() {
2654
+ // Use MutationObserver to watch for tab changes
2655
+ this.dropdowns.forEach(({ tablist, button, menu }) => {
2656
+ const observer = new MutationObserver(() => {
2657
+ const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
2658
+ const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true");
2659
+
2660
+ if (activeTab) {
2661
+ // Update button content
2662
+ const buttonContent = this.getTabContent(activeTab);
2663
+ button.innerHTML = buttonContent;
2664
+
2665
+ // Update menu items
2666
+ menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
2667
+ const tabId = item.getAttribute("data-tab-id");
2668
+ if (tabId === activeTab.id) {
2669
+ item.setAttribute("aria-selected", "true");
2670
+ } else {
2671
+ item.setAttribute("aria-selected", "false");
2672
+ }
2673
+ });
2674
+ }
2675
+ });
2676
+
2677
+ observer.observe(tablist, {
2678
+ attributes: true,
2679
+ attributeFilter: ["aria-selected"],
2680
+ subtree: true,
2681
+ });
2682
+ });
2683
+ },
2684
+
2685
+ handleResize() {
2686
+ // Recheck all containers and convert/remove dropdowns as needed
2687
+ const docsContent = document.getElementById("leed-docs-copy");
2688
+ if (!docsContent) return;
2689
+
2690
+ const tabContainers = docsContent.querySelectorAll(".leed-tabs__container");
2691
+
2692
+ tabContainers.forEach((container) => {
2693
+ // Skip containers that are inside .api-sidebar-content (handled by ApiTabDropdownManager)
2694
+ if (container.closest(".api-sidebar-content")) {
2695
+ return;
2696
+ }
2697
+
2698
+ const tablist = container.querySelector('ul[role="tablist"]');
2699
+ if (!tablist) return;
2700
+
2701
+ // Validate container has a valid width before making decisions
2702
+ const containerRect = container.getBoundingClientRect();
2703
+ if (containerRect.width === 0) {
2704
+ // Container not yet laid out, skip this check
2705
+ return;
2706
+ }
2707
+
2708
+ // Check if we already have a dropdown for this tablist
2709
+ const existingDropdown = this.dropdowns.find((d) => d.tablist === tablist && d.container === container);
2710
+
2711
+ const shouldConvert = this.shouldConvertToDropdown(tablist, container);
2712
+
2713
+ if (shouldConvert && !existingDropdown) {
2714
+ // Need to create dropdown
2715
+ this.createDropdown(tablist, container);
2716
+ } else if (!shouldConvert && existingDropdown) {
2717
+ // Only remove dropdown if container has valid dimensions
2718
+ // This prevents premature removal during initial layout
2719
+ if (containerRect.width > 0 && containerRect.height > 0) {
2720
+ // Need to remove dropdown and show tablist
2721
+ if (existingDropdown.tablist) {
2722
+ existingDropdown.tablist.classList.remove("converted-to-dropdown");
2723
+ existingDropdown.tablist.style.display = "";
2724
+ }
2725
+ if (existingDropdown.dropdown?.parentNode) {
2726
+ existingDropdown.dropdown.remove();
2727
+ }
2728
+ this.dropdowns = this.dropdowns.filter((d) => d !== existingDropdown);
2729
+ }
2730
+ }
2731
+ });
2732
+ },
2733
+ };
2734
+
2735
+ /**
2736
+ * Tables scroll rather than crush.
2737
+ *
2738
+ * A reference table has a width below which it stops being readable, and a
2739
+ * narrow column takes it straight through: the flag column wrapping `--env`
2740
+ * over two lines is a column of noise where the whole point is a value you can
2741
+ * scan.
2742
+ *
2743
+ * CSS alone cannot fix it. `overflow-x` on a `<table>` only applies with
2744
+ * `display: block`, and that makes the inner table box shrink to the block's
2745
+ * width, so the cells crush and nothing ever overflows. The element that
2746
+ * scrolls has to be a real wrapper, and markdown gives the renderer none — so
2747
+ * one is added here, the same way `setupCopyButtons` wraps a `<pre>`.
2748
+ *
2749
+ * Scoped to `#leed-docs-copy`, the article body that doc-article.hbs and
2750
+ * api-article.hbs both emit, so a table used for layout elsewhere in the
2751
+ * chrome is left alone.
2752
+ *
2753
+ * With JavaScript off the table is exactly what it was before: readable, just
2754
+ * narrow. Nothing is hidden by this, which is the right way to fail.
2755
+ */
2756
+ const TableManager = {
2757
+ init() {
2758
+ document.querySelectorAll("#leed-docs-copy table").forEach((table) => {
2759
+ if (table.parentElement?.classList.contains("table-scroll")) return;
2760
+ const wrapper = document.createElement("div");
2761
+ wrapper.className = "table-scroll";
2762
+ table.parentNode.insertBefore(wrapper, table);
2763
+ wrapper.appendChild(table);
2764
+ });
2765
+ },
2766
+ };
2767
+
2768
+ // Main initialization
2769
+ document.addEventListener("DOMContentLoaded", () => {
2770
+ ResizeManager.init();
2771
+ MermaidManager.init();
2772
+ CodeBlockManager.init();
2773
+ TableManager.init();
2774
+ TabManager.init();
2775
+ HeaderManager.init();
2776
+ TOCManager.init();
2777
+ NavFilterManager.init();
2778
+ AnchorLinkManager.init();
2779
+ ApiTabDropdownManager.init();
2780
+ DocsTabDropdownManager.init();
2781
+ });