@takazudo/zudo-doc 0.2.21 → 1.0.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 (373) hide show
  1. package/bin/gen-z-index.mjs +164 -0
  2. package/bin/tags-audit-runner.ts +126 -0
  3. package/bin/tags-audit.mjs +64 -0
  4. package/bin/zudo-doc.mjs +83 -0
  5. package/dist/ai-chat-modal/index.d.ts +8 -0
  6. package/dist/ai-chat-modal/index.js +243 -0
  7. package/dist/body-foot-util/body-foot-util-area.d.ts +4 -8
  8. package/dist/body-foot-util/edit-link.d.ts +4 -8
  9. package/dist/body-foot-util/index.d.ts +4 -3
  10. package/dist/breadcrumb/breadcrumb.d.ts +5 -9
  11. package/dist/breadcrumb/find-path.d.ts +2 -5
  12. package/dist/breadcrumb/index.d.ts +4 -4
  13. package/dist/breadcrumb/types.d.ts +3 -5
  14. package/dist/category-nav/index.d.ts +86 -0
  15. package/dist/category-nav/index.js +44 -0
  16. package/dist/category-tree-nav/index.d.ts +65 -0
  17. package/dist/category-tree-nav/index.js +24 -0
  18. package/dist/code-group/index.d.ts +10 -0
  19. package/dist/code-group/index.js +37 -0
  20. package/dist/code-syntax/code-block-enhancer-script.d.ts +1 -3
  21. package/dist/code-syntax/code-block-enhancer.d.ts +3 -6
  22. package/dist/code-syntax/index.d.ts +9 -8
  23. package/dist/code-syntax/mermaid-init-script.d.ts +3 -5
  24. package/dist/code-syntax/mermaid-init.d.ts +4 -7
  25. package/dist/code-syntax/tabs-init-script.d.ts +1 -3
  26. package/dist/code-syntax/tabs-init.d.ts +3 -6
  27. package/dist/code-syntax/tabs.d.ts +4 -7
  28. package/dist/color-scheme-utils.d.ts +125 -0
  29. package/dist/color-scheme-utils.js +151 -0
  30. package/dist/compose-meta-title/index.d.ts +7 -0
  31. package/dist/compose-meta-title/index.js +10 -0
  32. package/dist/content/component-map.d.ts +13 -17
  33. package/dist/content/content-blockquote.d.ts +3 -6
  34. package/dist/content/content-code.d.ts +3 -6
  35. package/dist/content/content-code.js +1 -1
  36. package/dist/content/content-link.d.ts +3 -6
  37. package/dist/content/content-link.js +1 -1
  38. package/dist/content/content-ol.d.ts +3 -6
  39. package/dist/content/content-paragraph.d.ts +3 -6
  40. package/dist/content/content-strong.d.ts +3 -6
  41. package/dist/content/content-table.d.ts +3 -6
  42. package/dist/content/content-ul.d.ts +3 -6
  43. package/dist/content/heading-h2.d.ts +3 -6
  44. package/dist/content/heading-h3.d.ts +3 -6
  45. package/dist/content/heading-h4.d.ts +3 -6
  46. package/dist/content/index.d.ts +29 -13
  47. package/dist/content-admonition/index.d.ts +15 -0
  48. package/dist/content-admonition/index.js +14 -0
  49. package/dist/design-token-panel-bootstrap.d.ts +38 -0
  50. package/dist/design-token-panel-bootstrap.js +29 -0
  51. package/dist/desktop-sidebar-toggle-island/index.d.ts +5 -0
  52. package/dist/desktop-sidebar-toggle-island/index.js +62 -0
  53. package/dist/details/details.d.ts +3 -7
  54. package/dist/details/index.d.ts +2 -2
  55. package/dist/doc-body-end/index.d.ts +21 -0
  56. package/dist/doc-body-end/index.js +15 -0
  57. package/dist/doc-content-header/index.d.ts +69 -0
  58. package/dist/doc-content-header/index.js +49 -0
  59. package/dist/doc-history/index.d.ts +10 -0
  60. package/dist/doc-history/index.js +407 -0
  61. package/dist/doc-history-area/index.d.ts +84 -0
  62. package/dist/doc-history-area/index.js +76 -0
  63. package/dist/doc-metainfo-area/index.d.ts +46 -0
  64. package/dist/doc-metainfo-area/index.js +41 -0
  65. package/dist/doc-page-props/index.d.ts +93 -0
  66. package/dist/doc-page-props/index.js +0 -0
  67. package/dist/doc-page-renderer/index.d.ts +128 -0
  68. package/dist/doc-page-renderer/index.js +103 -0
  69. package/dist/doc-page-shell/index.d.ts +136 -0
  70. package/dist/doc-page-shell/index.js +127 -0
  71. package/dist/doc-pager/index.d.ts +25 -0
  72. package/dist/doc-pager/index.js +41 -0
  73. package/dist/doc-route-entries/index.d.ts +125 -0
  74. package/dist/doc-route-entries/index.js +76 -0
  75. package/dist/doc-route-paths/index.d.ts +54 -0
  76. package/dist/doc-route-paths/index.js +61 -0
  77. package/dist/doc-tags-area/index.d.ts +38 -0
  78. package/dist/doc-tags-area/index.js +30 -0
  79. package/dist/doclayout/anchors.d.ts +7 -9
  80. package/dist/doclayout/doc-layout-with-defaults.d.ts +16 -12
  81. package/dist/doclayout/doc-layout.d.ts +5 -9
  82. package/dist/doclayout/index.d.ts +6 -6
  83. package/dist/eject/index.d.ts +33 -0
  84. package/dist/eject/index.js +229 -0
  85. package/dist/extract-headings/index.d.ts +43 -0
  86. package/dist/extract-headings/index.js +111 -0
  87. package/dist/factory-context/index.d.ts +80 -0
  88. package/dist/factory-context/index.js +0 -0
  89. package/dist/features.css +882 -0
  90. package/dist/footer/footer.d.ts +4 -8
  91. package/dist/footer/index.d.ts +13 -3
  92. package/dist/footer/types.d.ts +4 -6
  93. package/dist/footer-with-defaults/index.d.ts +63 -0
  94. package/dist/footer-with-defaults/index.js +128 -0
  95. package/dist/frontmatter-preview-data/index.d.ts +26 -0
  96. package/dist/frontmatter-preview-data/index.js +23 -0
  97. package/dist/github-helpers/index.d.ts +17 -0
  98. package/dist/github-helpers/index.js +16 -0
  99. package/dist/head/doc-head.d.ts +3 -6
  100. package/dist/head/index.d.ts +9 -5
  101. package/dist/head/og-tags.d.ts +4 -7
  102. package/dist/head/twitter-card.d.ts +3 -6
  103. package/dist/head/types.d.ts +1 -3
  104. package/dist/head-with-defaults/index.d.ts +49 -0
  105. package/dist/head-with-defaults/index.js +62 -0
  106. package/dist/header/header.d.ts +6 -10
  107. package/dist/header/index.d.ts +16 -6
  108. package/dist/header/nav-active.d.ts +5 -7
  109. package/dist/header/nav-overflow-script.d.ts +1 -3
  110. package/dist/header/right-items.d.ts +3 -6
  111. package/dist/header/types.d.ts +10 -12
  112. package/dist/header-with-defaults/index.d.ts +77 -0
  113. package/dist/header-with-defaults/index.js +163 -0
  114. package/dist/html-preview-wrapper/dedent.d.ts +1 -3
  115. package/dist/html-preview-wrapper/highlighted-code.d.ts +3 -7
  116. package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +6 -10
  117. package/dist/html-preview-wrapper/html-preview.d.ts +5 -9
  118. package/dist/html-preview-wrapper/index.d.ts +8 -5
  119. package/dist/html-preview-wrapper/preflight.d.ts +1 -3
  120. package/dist/html-preview-wrapper/preview-base.d.ts +4 -8
  121. package/dist/i18n-version/index.d.ts +7 -5
  122. package/dist/i18n-version/language-switcher.d.ts +5 -8
  123. package/dist/i18n-version/types.d.ts +3 -5
  124. package/dist/i18n-version/version-banner.d.ts +4 -8
  125. package/dist/i18n-version/version-switcher.d.ts +7 -10
  126. package/dist/icons/index.d.ts +9 -13
  127. package/dist/image-enlarge/index.d.ts +13 -0
  128. package/dist/image-enlarge/index.js +176 -0
  129. package/dist/index.d.ts +26 -2
  130. package/dist/inline-version-switcher/index.d.ts +31 -0
  131. package/dist/inline-version-switcher/index.js +44 -0
  132. package/dist/integrations/claude-resources/escape-for-mdx.d.ts +1 -3
  133. package/dist/integrations/claude-resources/generate.d.ts +2 -4
  134. package/dist/integrations/claude-resources/index.d.ts +7 -10
  135. package/dist/integrations/doc-history/index.d.ts +21 -24
  136. package/dist/integrations/doc-history/pre-build.d.ts +7 -9
  137. package/dist/integrations/llms-txt/dev-middleware.d.ts +7 -11
  138. package/dist/integrations/llms-txt/emit.d.ts +2 -7
  139. package/dist/integrations/llms-txt/generate.d.ts +3 -8
  140. package/dist/integrations/llms-txt/index.d.ts +33 -8
  141. package/dist/integrations/llms-txt/load.d.ts +3 -7
  142. package/dist/integrations/llms-txt/strip.d.ts +2 -6
  143. package/dist/integrations/llms-txt/types.d.ts +9 -13
  144. package/dist/integrations/search-index/build-emitter.d.ts +4 -7
  145. package/dist/integrations/search-index/collect.d.ts +2 -5
  146. package/dist/integrations/search-index/dev-middleware.d.ts +5 -8
  147. package/dist/integrations/search-index/index.d.ts +7 -5
  148. package/dist/integrations/search-index/types.d.ts +5 -7
  149. package/dist/island-types/index.d.ts +44 -0
  150. package/dist/island-types/index.js +12 -0
  151. package/dist/locale-merge/index.d.ts +114 -0
  152. package/dist/locale-merge/index.js +27 -0
  153. package/dist/math-block/index.d.ts +18 -0
  154. package/dist/math-block/index.js +29 -0
  155. package/dist/md-utils/index.d.ts +6 -8
  156. package/dist/md-utils/index.js +2 -1
  157. package/dist/mdx-components/index.d.ts +46 -0
  158. package/dist/mdx-components/index.js +161 -0
  159. package/dist/mermaid-enlarge/index.d.ts +11 -0
  160. package/dist/mermaid-enlarge/index.js +290 -0
  161. package/dist/metainfo/doc-metainfo.d.ts +5 -9
  162. package/dist/metainfo/doc-tags.d.ts +7 -11
  163. package/dist/metainfo/frontmatter-preview.d.ts +8 -12
  164. package/dist/metainfo/index.d.ts +6 -4
  165. package/dist/nav-data-prep/index.d.ts +72 -0
  166. package/dist/nav-data-prep/index.js +34 -0
  167. package/dist/nav-indexing/category-nav.d.ts +4 -8
  168. package/dist/nav-indexing/category-tree-nav.d.ts +4 -8
  169. package/dist/nav-indexing/doc-card-grid.d.ts +4 -8
  170. package/dist/nav-indexing/docs-sitemap.d.ts +4 -8
  171. package/dist/nav-indexing/index.d.ts +37 -10
  172. package/dist/nav-indexing/nav-card-grid.d.ts +4 -8
  173. package/dist/nav-indexing/site-tree-nav-demo.d.ts +4 -8
  174. package/dist/nav-indexing/tag-nav.d.ts +6 -10
  175. package/dist/nav-indexing/types.d.ts +6 -8
  176. package/dist/nav-indexing/versions-page-content.d.ts +4 -8
  177. package/dist/nav-scope/index.d.ts +41 -0
  178. package/dist/nav-scope/index.js +41 -0
  179. package/dist/nav-source-cache/index.d.ts +13 -0
  180. package/dist/nav-source-cache/index.js +28 -0
  181. package/dist/nav-source-docs/index.d.ts +169 -0
  182. package/dist/nav-source-docs/index.js +123 -0
  183. package/dist/page-loading/index.d.ts +1 -2
  184. package/dist/page-loading/page-loading-overlay.d.ts +4 -8
  185. package/dist/plugins/claude-resources.d.ts +3 -0
  186. package/dist/plugins/claude-resources.js +27 -0
  187. package/dist/plugins/connect-adapter.d.ts +31 -0
  188. package/dist/plugins/connect-adapter.js +78 -0
  189. package/dist/plugins/doc-history.d.ts +3 -0
  190. package/dist/plugins/doc-history.js +54 -0
  191. package/dist/plugins/llms-txt.d.ts +3 -0
  192. package/dist/plugins/llms-txt.js +48 -0
  193. package/dist/plugins/plugin-utils.d.ts +17 -0
  194. package/dist/plugins/plugin-utils.js +11 -0
  195. package/dist/plugins/routes.d.ts +2 -0
  196. package/dist/plugins/routes.js +100 -0
  197. package/dist/plugins/search-index.d.ts +3 -0
  198. package/dist/plugins/search-index.js +24 -0
  199. package/dist/preset.d.ts +198 -0
  200. package/dist/preset.js +185 -0
  201. package/dist/render-markdown/index.d.ts +5 -0
  202. package/dist/render-markdown/index.js +115 -0
  203. package/dist/robots.d.ts +36 -0
  204. package/dist/robots.js +17 -0
  205. package/dist/route-enumerators/index.d.ts +204 -0
  206. package/dist/route-enumerators/index.js +156 -0
  207. package/dist/routes/404.d.ts +7 -0
  208. package/dist/routes/404.js +47 -0
  209. package/dist/routes/_chrome.d.ts +32 -0
  210. package/dist/routes/_chrome.js +365 -0
  211. package/dist/routes/_context.d.ts +56 -0
  212. package/dist/routes/_context.js +222 -0
  213. package/dist/routes/_docs-helpers.d.ts +43 -0
  214. package/dist/routes/_docs-helpers.js +173 -0
  215. package/dist/routes/_virtual.d.js +0 -0
  216. package/dist/routes/api-ai-chat.d.ts +5 -0
  217. package/dist/routes/api-ai-chat.js +24 -0
  218. package/dist/routes/docs-slug.d.ts +21 -0
  219. package/dist/routes/docs-slug.js +26 -0
  220. package/dist/routes/docs-tags-index.d.ts +7 -0
  221. package/dist/routes/docs-tags-index.js +11 -0
  222. package/dist/routes/docs-tags-tag.d.ts +23 -0
  223. package/dist/routes/docs-tags-tag.js +19 -0
  224. package/dist/routes/docs-versions.d.ts +7 -0
  225. package/dist/routes/docs-versions.js +11 -0
  226. package/dist/routes/index.d.ts +7 -0
  227. package/dist/routes/index.js +117 -0
  228. package/dist/routes/locale-docs-slug.d.ts +27 -0
  229. package/dist/routes/locale-docs-slug.js +40 -0
  230. package/dist/routes/locale-docs-tags-index.d.ts +18 -0
  231. package/dist/routes/locale-docs-tags-index.js +15 -0
  232. package/dist/routes/locale-docs-tags-tag.d.ts +25 -0
  233. package/dist/routes/locale-docs-tags-tag.js +22 -0
  234. package/dist/routes/locale-docs-versions.d.ts +18 -0
  235. package/dist/routes/locale-docs-versions.js +15 -0
  236. package/dist/routes/locale-index.d.ts +24 -0
  237. package/dist/routes/locale-index.js +132 -0
  238. package/dist/routes/robots.txt.d.ts +3 -0
  239. package/dist/routes/robots.txt.js +8 -0
  240. package/dist/routes/sitemap.xml.d.ts +5 -0
  241. package/dist/routes/sitemap.xml.js +31 -0
  242. package/dist/routes/v-docs-slug.d.ts +27 -0
  243. package/dist/routes/v-docs-slug.js +34 -0
  244. package/dist/routes/v-locale-docs-slug.d.ts +31 -0
  245. package/dist/routes/v-locale-docs-slug.js +50 -0
  246. package/dist/safelist.css +1 -1
  247. package/dist/search-widget/index.d.ts +38 -0
  248. package/dist/search-widget/index.js +169 -0
  249. package/dist/search-widget-script/index.d.ts +18 -0
  250. package/dist/search-widget-script/index.js +465 -0
  251. package/dist/settings.d.ts +270 -0
  252. package/dist/settings.js +0 -0
  253. package/dist/sidebar/index.d.ts +21 -3
  254. package/dist/sidebar/sidebar.d.ts +5 -9
  255. package/dist/sidebar/types.d.ts +4 -6
  256. package/dist/sidebar-prepaint/index.d.ts +21 -0
  257. package/dist/sidebar-prepaint/index.js +23 -0
  258. package/dist/sidebar-resizer/index.d.ts +2 -6
  259. package/dist/sidebar-resizer/sidebar-resizer-init.d.ts +6 -10
  260. package/dist/sidebar-toggle-island/index.d.ts +13 -0
  261. package/dist/sidebar-toggle-island/index.js +122 -0
  262. package/dist/sidebar-tree/build-tree.d.ts +4 -8
  263. package/dist/sidebar-tree/category-meta.d.ts +3 -6
  264. package/dist/sidebar-tree/index.d.ts +18 -3
  265. package/dist/sidebar-tree/types.d.ts +6 -8
  266. package/dist/sidebar-tree-island/index.d.ts +13 -0
  267. package/dist/sidebar-tree-island/index.js +454 -0
  268. package/dist/sidebar-utils/index.d.ts +64 -0
  269. package/dist/sidebar-utils/index.js +88 -0
  270. package/dist/sidebar-with-defaults/index.d.ts +38 -0
  271. package/dist/sidebar-with-defaults/index.js +45 -0
  272. package/dist/site-tree-nav/index.d.ts +71 -0
  273. package/dist/site-tree-nav/index.js +36 -0
  274. package/dist/site-tree-nav-island/index.d.ts +11 -0
  275. package/dist/site-tree-nav-island/index.js +176 -0
  276. package/dist/slug/index.d.ts +4 -0
  277. package/dist/slug/index.js +19 -0
  278. package/dist/smart-break/index.d.ts +55 -0
  279. package/dist/{toc/smart-break.js → smart-break/index.js} +30 -3
  280. package/dist/tab-item/index.d.ts +2 -2
  281. package/dist/tab-item/tab-item.d.ts +6 -7
  282. package/dist/tag-helpers/index.d.ts +40 -0
  283. package/dist/tag-helpers/index.js +54 -0
  284. package/dist/tag-pages/index.d.ts +114 -0
  285. package/dist/tag-pages/index.js +146 -0
  286. package/dist/tags-audit.d.ts +107 -0
  287. package/dist/tags-audit.js +321 -0
  288. package/dist/theme/color-scheme-provider.d.ts +4 -8
  289. package/dist/theme/color-tweak-export-modal.d.ts +4 -7
  290. package/dist/theme/design-token-serde.d.ts +14 -17
  291. package/dist/theme/design-token-types.d.ts +4 -6
  292. package/dist/theme/iframe-bridge.d.ts +12 -14
  293. package/dist/theme/index.d.ts +7 -9
  294. package/dist/theme/theme-toggle.d.ts +3 -8
  295. package/dist/theme-toggle/color-scheme-sync.d.ts +5 -7
  296. package/dist/theme-toggle/index.d.ts +4 -8
  297. package/dist/toc/cx.d.ts +2 -3
  298. package/dist/toc/index.d.ts +8 -6
  299. package/dist/toc/mobile-toc.d.ts +5 -9
  300. package/dist/toc/mobile-toc.js +1 -1
  301. package/dist/toc/toc-title.d.ts +1 -3
  302. package/dist/toc/toc.d.ts +5 -9
  303. package/dist/toc/toc.js +1 -1
  304. package/dist/toc/types.d.ts +1 -3
  305. package/dist/toc/use-active-heading.d.ts +4 -7
  306. package/dist/transitions/index.d.ts +1 -1
  307. package/dist/transitions/page-events.d.ts +4 -6
  308. package/dist/tree-nav-shared/index.d.ts +22 -0
  309. package/dist/tree-nav-shared/index.js +66 -0
  310. package/dist/url-helpers/index.d.ts +37 -0
  311. package/dist/url-helpers/index.js +106 -0
  312. package/dist/url-normalizer/index.d.ts +39 -1
  313. package/dist/url-normalizer/normalize.d.ts +3 -5
  314. package/dist/use-modal-dialog/index.d.ts +70 -0
  315. package/dist/use-modal-dialog/index.js +73 -0
  316. package/dist/versions-page/index.d.ts +55 -0
  317. package/dist/versions-page/index.js +73 -0
  318. package/eject/breadcrumb/breadcrumb.tsx +194 -0
  319. package/eject/breadcrumb/find-path.ts +26 -0
  320. package/eject/breadcrumb/index.ts +4 -0
  321. package/eject/breadcrumb/types.ts +32 -0
  322. package/eject/code-group/index.tsx +78 -0
  323. package/eject/content-admonition/index.tsx +50 -0
  324. package/eject/details/details.tsx +34 -0
  325. package/eject/details/index.ts +4 -0
  326. package/eject/doc-pager/index.tsx +91 -0
  327. package/eject/footer/footer.tsx +167 -0
  328. package/eject/footer/index.ts +19 -0
  329. package/eject/footer/types.ts +60 -0
  330. package/eject/header/header.tsx +725 -0
  331. package/eject/header/index.ts +36 -0
  332. package/eject/header/nav-active.ts +122 -0
  333. package/eject/header/nav-overflow-script.ts +302 -0
  334. package/eject/header/right-items.ts +78 -0
  335. package/eject/header/types.ts +69 -0
  336. package/eject/page-loading/index.ts +10 -0
  337. package/eject/page-loading/page-loading-overlay.tsx +103 -0
  338. package/eject/sidebar/index.ts +27 -0
  339. package/eject/sidebar/sidebar.tsx +137 -0
  340. package/eject/sidebar/types.ts +73 -0
  341. package/eject/tab-item/index.ts +7 -0
  342. package/eject/tab-item/tab-item.tsx +70 -0
  343. package/eject/theme-toggle/color-scheme-sync.ts +73 -0
  344. package/eject/theme-toggle/index.tsx +120 -0
  345. package/eject/toc/cx.ts +38 -0
  346. package/eject/toc/index.ts +11 -0
  347. package/eject/toc/mobile-toc.tsx +133 -0
  348. package/eject/toc/toc-title.ts +33 -0
  349. package/eject/toc/toc.tsx +119 -0
  350. package/eject/toc/types.ts +14 -0
  351. package/eject/toc/use-active-heading.ts +162 -0
  352. package/package.json +371 -11
  353. package/routes-src/404.tsx +49 -0
  354. package/routes-src/_chrome.tsx +470 -0
  355. package/routes-src/_context.ts +314 -0
  356. package/routes-src/_docs-helpers.ts +284 -0
  357. package/routes-src/_virtual.d.ts +15 -0
  358. package/routes-src/api-ai-chat.tsx +46 -0
  359. package/routes-src/docs-slug.tsx +43 -0
  360. package/routes-src/docs-tags-index.tsx +14 -0
  361. package/routes-src/docs-tags-tag.tsx +29 -0
  362. package/routes-src/docs-versions.tsx +15 -0
  363. package/routes-src/index.tsx +134 -0
  364. package/routes-src/locale-docs-slug.tsx +69 -0
  365. package/routes-src/locale-docs-tags-index.tsx +23 -0
  366. package/routes-src/locale-docs-tags-tag.tsx +39 -0
  367. package/routes-src/locale-docs-versions.tsx +23 -0
  368. package/routes-src/locale-index.tsx +150 -0
  369. package/routes-src/robots.txt.tsx +13 -0
  370. package/routes-src/sitemap.xml.tsx +48 -0
  371. package/routes-src/v-docs-slug.tsx +64 -0
  372. package/routes-src/v-locale-docs-slug.tsx +85 -0
  373. package/dist/toc/smart-break.d.ts +0 -27
@@ -0,0 +1,106 @@
1
+ function makeUrlHelpers(settings, i18n) {
2
+ const { defaultLocale, locales, getLocaleLabel } = i18n;
3
+ const normalizedBase = settings.base.replace(/\/+$/, "");
4
+ function applyTrailingSlash(url) {
5
+ if (!settings.trailingSlash) return url;
6
+ if (url.endsWith("/")) return url;
7
+ const suffixIdx = url.search(/[?#]/);
8
+ const pathPart = suffixIdx >= 0 ? url.slice(0, suffixIdx) : url;
9
+ const suffix = suffixIdx >= 0 ? url.slice(suffixIdx) : "";
10
+ if (pathPart.endsWith("/")) return url;
11
+ const lastSegment = pathPart.split("/").pop() ?? "";
12
+ if (/\.[a-zA-Z]\w*$/.test(lastSegment)) return url;
13
+ return pathPart + "/" + suffix;
14
+ }
15
+ function withBase(path) {
16
+ const raw = normalizedBase === "" ? path : `${normalizedBase}${path.startsWith("/") ? path : `/${path}`}`;
17
+ return applyTrailingSlash(raw);
18
+ }
19
+ function stripBase(path) {
20
+ if (normalizedBase === "") return path;
21
+ if (path === normalizedBase) return "/";
22
+ return path.startsWith(`${normalizedBase}/`) ? path.slice(normalizedBase.length) : path;
23
+ }
24
+ function absoluteUrl(pageUrl) {
25
+ return settings.siteUrl ? settings.siteUrl.replace(/\/$/, "") + pageUrl : void 0;
26
+ }
27
+ function docsUrl(slug, lang = defaultLocale) {
28
+ const path = lang === defaultLocale ? `/docs/${slug}` : `/${lang}/docs/${slug}`;
29
+ return withBase(path);
30
+ }
31
+ function isExternal(href) {
32
+ return href.startsWith("http://") || href.startsWith("https://");
33
+ }
34
+ function resolveHref(href) {
35
+ return isExternal(href) ? href : withBase(href);
36
+ }
37
+ function navHref(path, lang, currentVersion) {
38
+ const isNonDefaultLocale = lang != null && lang !== defaultLocale;
39
+ const versionPrefix = currentVersion ? `/v/${currentVersion}` : "";
40
+ return withBase(
41
+ isNonDefaultLocale ? `${versionPrefix}/${lang}${path}` : `${versionPrefix}${path}`
42
+ );
43
+ }
44
+ function splitVersionPrefix(path) {
45
+ const m = path.match(/^(\/v\/[^/]+)(\/.*|$)/);
46
+ return m ? { versionPrefix: m[1] ?? "", rest: m[2] ?? "/" } : { versionPrefix: "", rest: path };
47
+ }
48
+ function getPathForLocale(path, currentLang, targetLang) {
49
+ const { versionPrefix, rest } = splitVersionPrefix(stripBase(path));
50
+ let relativePath = rest;
51
+ if (currentLang !== defaultLocale) {
52
+ relativePath = relativePath.replace(new RegExp(`^/${currentLang}(?:/|$)`), "/");
53
+ }
54
+ if (targetLang !== defaultLocale) {
55
+ relativePath = `/${targetLang}${relativePath}`;
56
+ }
57
+ return withBase(`${versionPrefix}${relativePath}`);
58
+ }
59
+ function isDefaultLocaleOnlyPath(path) {
60
+ const stripped = stripBase(path);
61
+ const normalized = stripped.endsWith("/") ? stripped : `${stripped}/`;
62
+ return settings.defaultLocaleOnlyPrefixes.some((prefix) => normalized.startsWith(prefix));
63
+ }
64
+ function buildLocaleLinks(currentPath, currentLang) {
65
+ let defaultLocalePath = splitVersionPrefix(stripBase(currentPath)).rest;
66
+ if (currentLang !== defaultLocale) {
67
+ defaultLocalePath = defaultLocalePath.replace(new RegExp(`^/${currentLang}(?:/|$)`), "/");
68
+ }
69
+ if (isDefaultLocaleOnlyPath(defaultLocalePath)) {
70
+ return [{
71
+ code: currentLang,
72
+ label: getLocaleLabel(currentLang),
73
+ href: getPathForLocale(currentPath, currentLang, currentLang),
74
+ active: true
75
+ }];
76
+ }
77
+ return locales.map((code) => ({
78
+ code,
79
+ label: getLocaleLabel(code),
80
+ href: getPathForLocale(currentPath, currentLang, code),
81
+ active: code === currentLang
82
+ }));
83
+ }
84
+ function versionedDocsUrl(slug, versionSlug, lang = defaultLocale) {
85
+ const path = lang === defaultLocale ? `/v/${versionSlug}/docs/${slug}` : `/v/${versionSlug}/${lang}/docs/${slug}`;
86
+ return withBase(path);
87
+ }
88
+ return {
89
+ normalizedBase,
90
+ applyTrailingSlash,
91
+ withBase,
92
+ stripBase,
93
+ absoluteUrl,
94
+ docsUrl,
95
+ isExternal,
96
+ resolveHref,
97
+ navHref,
98
+ getPathForLocale,
99
+ buildLocaleLinks,
100
+ isDefaultLocaleOnlyPath,
101
+ versionedDocsUrl
102
+ };
103
+ }
104
+ export {
105
+ makeUrlHelpers
106
+ };
@@ -1 +1,39 @@
1
- export { buildUrl, normalizePathname, shouldSkipNormalization } from './normalize.js';
1
+ /**
2
+ * Public entry for `@takazudo/zudo-doc/url-normalizer`.
3
+ *
4
+ * Build-time URL normalizer that ensures every emitted route URL ends with
5
+ * a trailing slash, matching the `trailingSlash: "always"` policy of the
6
+ * legacy Astro build.
7
+ *
8
+ * ## Why build-time only?
9
+ *
10
+ * zfb is a purely static site builder — there is no server or dev middleware.
11
+ * The Astro `src/middleware-handler.ts` ran at request time; the equivalent
12
+ * guarantee is now split:
13
+ *
14
+ * - **Build time** (this module): `buildUrl()` constructs URLs that always
15
+ * end with `/`, and `normalizePathname()` corrects any bare pathname passed
16
+ * in from external data.
17
+ * - **Runtime**: the deployment host (e.g. Cloudflare Pages `_redirects`) is
18
+ * responsible for redirecting requests that arrive without a trailing slash.
19
+ *
20
+ * See the "Trailing-Slash Policy" concept doc in the showcase docs for the
21
+ * full design rationale and an example `_redirects` rule.
22
+ *
23
+ * ## Usage
24
+ *
25
+ * ```ts
26
+ * import {
27
+ * buildUrl,
28
+ * normalizePathname,
29
+ * shouldSkipNormalization,
30
+ * } from "@takazudo/zudo-doc/url-normalizer";
31
+ *
32
+ * // In a paths() implementation:
33
+ * const url = buildUrl("docs", slug); // "/docs/my-slug/"
34
+ *
35
+ * // In an integration that receives raw pathnames:
36
+ * const canonical = normalizePathname(rawPathname);
37
+ * ```
38
+ */
39
+ export { buildUrl, normalizePathname, shouldSkipNormalization } from "./normalize.js";
@@ -58,7 +58,7 @@
58
58
  * buildUrl() // "/"
59
59
  * buildUrl("", "docs", "") // "/docs/"
60
60
  */
61
- declare function buildUrl(...segments: string[]): string;
61
+ export declare function buildUrl(...segments: string[]): string;
62
62
  /**
63
63
  * Return `true` when `pathname` should NOT receive a trailing slash.
64
64
  *
@@ -78,7 +78,7 @@ declare function buildUrl(...segments: string[]): string;
78
78
  * shouldSkipNormalization("/docs/guide") // false (needs trailing slash)
79
79
  * shouldSkipNormalization("/docs/v2.0") // false (number after dot → not an extension)
80
80
  */
81
- declare function shouldSkipNormalization(pathname: string): boolean;
81
+ export declare function shouldSkipNormalization(pathname: string): boolean;
82
82
  /**
83
83
  * Return `pathname` with a trailing slash appended if needed.
84
84
  *
@@ -99,6 +99,4 @@ declare function shouldSkipNormalization(pathname: string): boolean;
99
99
  * normalizePathname("/docs/guide/") // "/docs/guide/" (no-op)
100
100
  * normalizePathname("/assets/app.js") // "/assets/app.js" (skipped)
101
101
  */
102
- declare function normalizePathname(pathname: string): string;
103
-
104
- export { buildUrl, normalizePathname, shouldSkipNormalization };
102
+ export declare function normalizePathname(pathname: string): string;
@@ -0,0 +1,70 @@
1
+ interface UseModalDialogOptions {
2
+ /** True when the dialog should be open. */
3
+ isOpen: boolean;
4
+ /** Called when the dialog is closed for any reason (Escape, backdrop, navigation). */
5
+ onClose: () => void;
6
+ /**
7
+ * DOM event name that triggers a close-and-reset. Pass the host's SPA
8
+ * after-navigate event name (e.g. `AFTER_NAVIGATE_EVENT`); pass `undefined`
9
+ * to skip navigation-close wiring entirely.
10
+ */
11
+ navigateEvent?: string;
12
+ /**
13
+ * When `true`, enable backdrop-click-to-close. The caller must forward the
14
+ * click handler returned by this hook to the `<dialog>` element's `onClick`.
15
+ * Defaults to `false` so opt-in only.
16
+ */
17
+ backdropClickClose?: boolean;
18
+ /**
19
+ * When `true`, enable focus management:
20
+ * - On open: capture `document.activeElement` as the return target (unless
21
+ * `returnFocusRef` is provided), then move focus to the first focusable
22
+ * element inside the dialog (or the dialog itself as a fallback).
23
+ * - On close: restore focus to the captured element.
24
+ *
25
+ * When a caller already manages focus internally (e.g. ai-chat-modal focuses
26
+ * its input in its own effect), pass `restoreFocusOnly: true` instead to get
27
+ * trigger-capture + restore without the "move into dialog" step.
28
+ */
29
+ manageFocus?: boolean;
30
+ /**
31
+ * Like `manageFocus` but skips the "focus first element in dialog" step —
32
+ * use when the caller has its own focus-on-open logic and only needs
33
+ * trigger-capture + focus restore on close.
34
+ */
35
+ restoreFocusOnly?: boolean;
36
+ /**
37
+ * Optional ref holding the element to return focus to on close. When
38
+ * provided and non-null, this takes precedence over the internally captured
39
+ * `document.activeElement`. Useful when the trigger element is conditionally
40
+ * rendered and therefore removed from the DOM before the open effect fires
41
+ * (e.g. the doc-history trigger button, which is unmounted when `isOpen`
42
+ * is true). The caller should set `returnFocusRef.current` to the trigger
43
+ * element in its click handler, before calling `setState`.
44
+ */
45
+ returnFocusRef?: React.RefObject<HTMLElement | null>;
46
+ }
47
+ interface UseModalDialogResult {
48
+ dialogRef: React.RefObject<HTMLDialogElement>;
49
+ /**
50
+ * Pass this to `<dialog onClick={handleBackdropClick}>`.
51
+ * Only wired when `backdropClickClose` is true; otherwise a no-op.
52
+ */
53
+ handleBackdropClick: (e: React.MouseEvent<HTMLDialogElement>) => void;
54
+ }
55
+ /**
56
+ * Shared hook for `<dialog>`-based modal components.
57
+ *
58
+ * Usage:
59
+ * ```tsx
60
+ * const { dialogRef, handleBackdropClick } = useModalDialog({
61
+ * isOpen,
62
+ * onClose: handleClose,
63
+ * navigateEvent: AFTER_NAVIGATE_EVENT,
64
+ * backdropClickClose: true,
65
+ * });
66
+ * return <dialog ref={dialogRef} onClick={handleBackdropClick} ... />;
67
+ * ```
68
+ */
69
+ export declare function useModalDialog({ isOpen, onClose, navigateEvent, backdropClickClose, manageFocus, restoreFocusOnly, returnFocusRef, }: UseModalDialogOptions): UseModalDialogResult;
70
+ export {};
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "preact/compat";
3
+ function useModalDialog({
4
+ isOpen,
5
+ onClose,
6
+ navigateEvent,
7
+ backdropClickClose = false,
8
+ manageFocus = false,
9
+ restoreFocusOnly = false,
10
+ returnFocusRef
11
+ }) {
12
+ const dialogRef = useRef(null);
13
+ const internalTriggerRef = useRef(null);
14
+ useEffect(() => {
15
+ const dialog = dialogRef.current;
16
+ if (!dialog) return;
17
+ if (isOpen && !dialog.open) {
18
+ if ((manageFocus || restoreFocusOnly) && !returnFocusRef) {
19
+ internalTriggerRef.current = document.activeElement;
20
+ }
21
+ dialog.showModal();
22
+ if (manageFocus && !restoreFocusOnly) {
23
+ const FOCUSABLE = 'button:not([disabled]),a[href],[tabindex]:not([tabindex="-1"]),input:not([disabled]),select:not([disabled]),textarea:not([disabled])';
24
+ const first = dialog.querySelector(FOCUSABLE);
25
+ if (first) {
26
+ first.focus();
27
+ } else {
28
+ dialog.focus();
29
+ }
30
+ }
31
+ } else if (!isOpen && dialog.open) {
32
+ dialog.close();
33
+ }
34
+ }, [isOpen, manageFocus, restoreFocusOnly, returnFocusRef]);
35
+ useEffect(() => {
36
+ const dialog = dialogRef.current;
37
+ if (!dialog) return;
38
+ function onDialogClose() {
39
+ if (manageFocus || restoreFocusOnly) {
40
+ const trigger = returnFocusRef ? returnFocusRef.current : internalTriggerRef.current;
41
+ if (trigger instanceof HTMLElement) {
42
+ trigger.focus();
43
+ }
44
+ internalTriggerRef.current = null;
45
+ }
46
+ if (isOpen) onClose();
47
+ }
48
+ dialog.addEventListener("close", onDialogClose);
49
+ return () => dialog.removeEventListener("close", onDialogClose);
50
+ }, [isOpen, onClose, manageFocus, restoreFocusOnly, returnFocusRef]);
51
+ useEffect(() => {
52
+ if (!navigateEvent) return;
53
+ function handleNavigation() {
54
+ const dialog = dialogRef.current;
55
+ if (dialog?.open) {
56
+ dialog.close();
57
+ onClose();
58
+ }
59
+ }
60
+ document.addEventListener(navigateEvent, handleNavigation);
61
+ return () => document.removeEventListener(navigateEvent, handleNavigation);
62
+ }, [navigateEvent, onClose]);
63
+ function handleBackdropClick(e) {
64
+ if (!backdropClickClose) return;
65
+ const dialog = dialogRef.current;
66
+ if (!dialog) return;
67
+ if (e.target === dialog) dialog.close();
68
+ }
69
+ return { dialogRef, handleBackdropClick };
70
+ }
71
+ export {
72
+ useModalDialog
73
+ };
@@ -0,0 +1,55 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { ComponentChildren, JSX } from "preact";
4
+ /** Minimal version config entry needed by the versions page. */
5
+ export interface VersionsPageVersionEntry {
6
+ slug: string;
7
+ label?: string;
8
+ banner?: "unmaintained" | "unreleased" | false;
9
+ }
10
+ /** Settings subset read by the versions page factory. */
11
+ export interface VersionsPageSettings {
12
+ noindex?: boolean;
13
+ dynamicPageTransition?: boolean;
14
+ versions: VersionsPageVersionEntry[] | false;
15
+ base: string;
16
+ }
17
+ /** Host-supplied component bindings injected into the versions page factory. */
18
+ export interface VersionsPageComponents {
19
+ HeadWithDefaults: (props: {
20
+ title: string;
21
+ }) => JSX.Element;
22
+ HeaderWithDefaults: (props: {
23
+ lang?: string;
24
+ currentPath?: string;
25
+ }) => JSX.Element;
26
+ FooterWithDefaults: (props: {
27
+ lang?: string;
28
+ }) => JSX.Element;
29
+ BodyEndIslands: (props: {
30
+ basePath: string;
31
+ }) => JSX.Element;
32
+ }
33
+ /** Injected dependencies for {@link createVersionsPageView}. */
34
+ export interface VersionsPageDeps {
35
+ settings: VersionsPageSettings;
36
+ /** Default locale code (e.g. `"en"`). */
37
+ defaultLocale: string;
38
+ /** Translate a UI string key for a locale. */
39
+ t: (key: string, locale: string) => string;
40
+ /** Prefix a path with the configured base directory. */
41
+ withBase: (path: string) => string;
42
+ /** Compose the meta `<title>` value. */
43
+ composeMetaTitle: (title: string) => string;
44
+ components: VersionsPageComponents;
45
+ }
46
+ /** Props for the VersionsPageView component. */
47
+ export interface VersionsPageViewProps {
48
+ locale: string;
49
+ children?: ComponentChildren;
50
+ }
51
+ /**
52
+ * Create a `VersionsPageView` component bound to the host's settings and
53
+ * component dependencies.
54
+ */
55
+ export declare function createVersionsPageView(deps: VersionsPageDeps): (props: VersionsPageViewProps) => JSX.Element;
@@ -0,0 +1,73 @@
1
+ import { Fragment, jsx } from "preact/jsx-runtime";
2
+ import { DocLayoutWithDefaults } from "../doclayout/index.js";
3
+ import { VersionsPageContent } from "../nav-indexing/index.js";
4
+ function createVersionsPageView(deps) {
5
+ const {
6
+ settings,
7
+ defaultLocale,
8
+ t,
9
+ withBase,
10
+ composeMetaTitle,
11
+ components: {
12
+ HeadWithDefaults,
13
+ HeaderWithDefaults,
14
+ FooterWithDefaults,
15
+ BodyEndIslands
16
+ }
17
+ } = deps;
18
+ function VersionsPageView({ locale }) {
19
+ const isDefault = locale === defaultLocale;
20
+ const prefix = isDefault ? "" : `/${locale}`;
21
+ const pageTitle = t("version.page.title", locale);
22
+ const labels = {
23
+ pageTitle,
24
+ latestTitle: t("version.page.latest.title", locale),
25
+ latestDescription: t("version.page.latest.description", locale),
26
+ latestLink: t("version.page.latest.link", locale),
27
+ pastTitle: t("version.page.past.title", locale),
28
+ pastDescription: t("version.page.past.description", locale),
29
+ unmaintained: t("version.page.unmaintained", locale),
30
+ unreleased: t("version.page.unreleased", locale),
31
+ versionCol: t("version.switcher.label", locale),
32
+ statusCol: t("version.page.status", locale),
33
+ docsCol: t("version.page.docs", locale)
34
+ };
35
+ const latestHref = withBase(`${prefix}/docs/getting-started`);
36
+ const versions = settings.versions ? settings.versions.map((v) => ({
37
+ slug: v.slug,
38
+ label: v.label ?? v.slug,
39
+ // Version prefix comes BEFORE the locale — the only routed shape is
40
+ // pages/v/[version]/{locale}/docs/...; /{locale}/v/... has no route.
41
+ docsHref: withBase(`/v/${v.slug}${prefix}/docs/getting-started/`),
42
+ banner: v.banner
43
+ })) : [];
44
+ return /* @__PURE__ */ jsx(
45
+ DocLayoutWithDefaults,
46
+ {
47
+ title: composeMetaTitle(pageTitle),
48
+ head: /* @__PURE__ */ jsx(HeadWithDefaults, { title: pageTitle }),
49
+ lang: locale,
50
+ noindex: settings.noindex,
51
+ hideSidebar: true,
52
+ hideToc: true,
53
+ sidebarOverride: /* @__PURE__ */ jsx(Fragment, {}),
54
+ headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: withBase(`${prefix}/docs/versions`) }),
55
+ footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }),
56
+ bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
57
+ enableClientRouter: settings.dynamicPageTransition,
58
+ children: /* @__PURE__ */ jsx(
59
+ VersionsPageContent,
60
+ {
61
+ latestHref,
62
+ versions,
63
+ labels
64
+ }
65
+ )
66
+ }
67
+ );
68
+ }
69
+ return VersionsPageView;
70
+ }
71
+ export {
72
+ createVersionsPageView
73
+ };
@@ -0,0 +1,194 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+
4
+ import type { ComponentChildren, VNode } from "preact";
5
+ import { findPath } from "./find-path.js";
6
+ import type { BreadcrumbItem, SidebarNode } from "./types.js";
7
+ import { ChevronRight } from "../icons/index.js";
8
+
9
+ /**
10
+ * Convert a sidebar tree + the current page's id into the ordered list
11
+ * of crumbs the renderer walks. The first crumb is the home rung
12
+ * (icon-only, label is the empty string), followed by each ancestor in
13
+ * the tree, ending with the current page (no `href` so the renderer
14
+ * paints it as plain text).
15
+ *
16
+ * Mirrors the legacy buildBreadcrumbs() behaviour from
17
+ * src/utils/docs.ts so the rendered output stays parity-equivalent
18
+ * with the original Astro breadcrumb component.
19
+ */
20
+ export function buildBreadcrumbItems(
21
+ tree: SidebarNode[],
22
+ currentId: string,
23
+ homeHref: string,
24
+ ): BreadcrumbItem[] {
25
+ const items: BreadcrumbItem[] = [{ label: "", href: homeHref }];
26
+ const path = findPath(tree, currentId);
27
+ for (let i = 0; i < path.length; i++) {
28
+ const node = path[i];
29
+ if (!node) continue;
30
+ const isLast = i === path.length - 1;
31
+ items.push({
32
+ label: node.label,
33
+ href: isLast ? undefined : node.href,
34
+ });
35
+ }
36
+ return items;
37
+ }
38
+
39
+ interface SmartLabelProps {
40
+ label: string;
41
+ }
42
+
43
+ /**
44
+ * Inline replacement for the Astro template's `set:html` /
45
+ * smartBreakToHtml call. Keeps the breadcrumb self-contained inside the
46
+ * v2 package — the legacy smart-break util lives in the host project's
47
+ * src/utils/, which v2 must not reach into.
48
+ *
49
+ * Inserts a Preact <wbr/> after each delimiter character when the label
50
+ * looks "path-like" (URLs, slash-separated paths, etc.). Prose labels
51
+ * pass through unchanged.
52
+ */
53
+ function SmartLabel({ label }: SmartLabelProps): VNode | string {
54
+ if (!isPathLike(label)) return label;
55
+ const parts = label.split(DELIM_SPLIT);
56
+ const nodes: (string | VNode)[] = [];
57
+ for (let i = 0; i < parts.length; i++) {
58
+ const part = parts[i];
59
+ if (!part) continue;
60
+ nodes.push(part);
61
+ if (i % 2 === 1) nodes.push(<wbr key={`wbr-${i}`} />);
62
+ }
63
+ return <>{nodes}</>;
64
+ }
65
+
66
+ const DELIM_SPLIT = /([/\\\-_.:?#&=])/;
67
+
68
+ /** Heuristic copied from src/utils/smart-break.tsx — kept inline so v2
69
+ * has no upward dependency. Returns true when delimiter-aware
70
+ * line-break hints are useful (paths, URLs), false for prose. */
71
+ function isPathLike(text: string): boolean {
72
+ if (!text) return false;
73
+ if (text.includes("://")) return true;
74
+ if (/^\.{0,2}\//.test(text)) return true;
75
+ if (/^[A-Za-z]:[\\/]/.test(text)) return true;
76
+ const forwardMatches = text.match(/[A-Za-z0-9]\/[A-Za-z0-9]/g);
77
+ if (forwardMatches && forwardMatches.length >= 2) return true;
78
+ const backMatches = text.match(/[A-Za-z0-9]\\[A-Za-z0-9]/g);
79
+ if (backMatches && backMatches.length >= 2) return true;
80
+ const hasDomainDot = /[A-Za-z0-9]\.[A-Za-z0-9]/.test(text);
81
+ const hasSlash = /[\\/]/.test(text);
82
+ if (hasDomainDot && hasSlash) return true;
83
+ return false;
84
+ }
85
+
86
+ function ChevronIcon(): VNode {
87
+ return <ChevronRight className="h-icon-xs w-icon-xs text-muted shrink-0" />;
88
+ }
89
+
90
+ function HomeIcon(): VNode {
91
+ return (
92
+ <svg
93
+ xmlns="http://www.w3.org/2000/svg"
94
+ class="h-[1.575rem] w-[1.575rem] shrink-0"
95
+ fill="none"
96
+ viewBox="0 0 24 24"
97
+ stroke="currentColor"
98
+ stroke-width="2"
99
+ aria-hidden="true"
100
+ >
101
+ <path
102
+ stroke-linecap="round"
103
+ stroke-linejoin="round"
104
+ d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1h-2z"
105
+ />
106
+ </svg>
107
+ );
108
+ }
109
+
110
+ export interface BreadcrumbProps {
111
+ /** Pre-built items, or omit and pass `tree` + `currentId` instead. */
112
+ items?: BreadcrumbItem[];
113
+ /** Sidebar tree to derive the trail from. Required when `items` is omitted. */
114
+ tree?: SidebarNode[];
115
+ /** Id of the current page (matches SidebarNode.id). Required when `items` is omitted. */
116
+ currentId?: string;
117
+ /** Href for the leading home rung. Defaults to "/". */
118
+ homeHref?: string;
119
+ /**
120
+ * Optional slot rendered at the right end of the breadcrumb row.
121
+ * When provided, the breadcrumb nav and this slot are wrapped in a
122
+ * flex row that matches the original Astro doc-layout's combined
123
+ * breadcrumb + version-switcher row:
124
+ *
125
+ * class="mb-vsp-sm flex flex-col items-start gap-vsp-xs
126
+ * sm:flex-row sm:items-center sm:justify-between [&_nav]:mb-0"
127
+ *
128
+ * The `[&_nav]:mb-0` rule strips the nav's own `mb-vsp-md` so the
129
+ * wrapper margin controls spacing instead. Use this to place the
130
+ * VersionSwitcher pill inline at the right of the breadcrumb row,
131
+ * matching the reference site's layout on category index pages.
132
+ */
133
+ rightSlot?: ComponentChildren;
134
+ }
135
+
136
+ /**
137
+ * Breadcrumb trail component.
138
+ *
139
+ * Two usage shapes:
140
+ * 1. Pass pre-built `items` (existing call-sites that already
141
+ * compute crumbs upstream).
142
+ * 2. Pass `tree` + `currentId` and the component derives the trail.
143
+ *
144
+ * Returns null when no items resolve, matching the documented
145
+ * `items.length > 0 &&` guard.
146
+ */
147
+ export function Breadcrumb(props: BreadcrumbProps): VNode | null {
148
+ const items =
149
+ props.items ??
150
+ (props.tree && props.currentId !== undefined
151
+ ? buildBreadcrumbItems(props.tree, props.currentId, props.homeHref ?? "/")
152
+ : []);
153
+
154
+ if (items.length === 0) return null;
155
+
156
+ const nav = (
157
+ <nav class="mb-vsp-md text-small" aria-label="Breadcrumb">
158
+ <ol class="flex flex-wrap items-center gap-x-hsp-xs">
159
+ {items.map((item, i) => (
160
+ <li key={`crumb-${i}`} class="flex items-center gap-x-hsp-xs">
161
+ {i > 0 && <ChevronIcon />}
162
+ {item.href ? (
163
+ <a
164
+ href={item.href}
165
+ class="text-muted underline hover:text-fg flex items-center gap-x-hsp-2xs"
166
+ >
167
+ {i === 0 && <HomeIcon />}
168
+ <SmartLabel label={item.label} />
169
+ </a>
170
+ ) : (
171
+ <span class="text-fg">
172
+ <SmartLabel label={item.label} />
173
+ </span>
174
+ )}
175
+ </li>
176
+ ))}
177
+ </ol>
178
+ </nav>
179
+ );
180
+
181
+ if (props.rightSlot === undefined) return nav;
182
+
183
+ // When a rightSlot is provided, wrap both elements in a flex row that
184
+ // matches the original Astro doc-layout's breadcrumb + version-switcher
185
+ // row. The `[&_nav]:mb-0` rule cancels the nav's own bottom margin so
186
+ // the wrapper margin controls spacing instead (matching the Astro
187
+ // reference which used `mb-vsp-sm` on the outer div).
188
+ return (
189
+ <div class="mb-vsp-sm flex flex-col items-start gap-vsp-xs sm:flex-row sm:items-center sm:justify-between [&_nav]:mb-0">
190
+ {nav}
191
+ {props.rightSlot}
192
+ </div>
193
+ );
194
+ }
@@ -0,0 +1,26 @@
1
+ import type { BreadcrumbNode, SidebarNode } from "./types.js";
2
+
3
+ /**
4
+ * Walk the sidebar tree and return the chain of nodes from the root
5
+ * level down to (and including) the node whose `id` matches `targetId`.
6
+ *
7
+ * Returns an empty array when no node matches. Pure: never mutates the
8
+ * input tree, never reads anything outside of it.
9
+ *
10
+ * Used by the breadcrumb component to derive the current page's
11
+ * ancestry. The home/root crumb is the renderer's responsibility — this
12
+ * helper deals strictly with the tree itself.
13
+ */
14
+ export function findPath<T extends BreadcrumbNode = SidebarNode>(
15
+ nodes: readonly T[],
16
+ targetId: string,
17
+ ): T[] {
18
+ for (const node of nodes) {
19
+ if (node.id === targetId) return [node];
20
+ if (node.children && node.children.length > 0) {
21
+ const sub = findPath(node.children as readonly T[], targetId);
22
+ if (sub.length > 0) return [node, ...sub];
23
+ }
24
+ }
25
+ return [];
26
+ }
@@ -0,0 +1,4 @@
1
+ export { Breadcrumb, buildBreadcrumbItems } from "./breadcrumb.js";
2
+ export type { BreadcrumbProps } from "./breadcrumb.js";
3
+ export { findPath } from "./find-path.js";
4
+ export type { BreadcrumbItem, SidebarNode } from "./types.js";