@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,133 @@
1
+ "use client";
2
+
3
+ /** @jsxRuntime automatic */
4
+ /** @jsxImportSource preact */
5
+
6
+ import type { VNode } from "preact";
7
+ import { useMemo, useState } from "preact/hooks";
8
+
9
+ import type { HeadingItem } from "./types.js";
10
+ import { SmartBreak } from "../smart-break/index.js";
11
+ import { cx } from "./cx.js";
12
+
13
+ export interface MobileTocProps {
14
+ headings: readonly HeadingItem[];
15
+ title?: string;
16
+ }
17
+
18
+ /**
19
+ * Collapsible TOC for narrow viewports (`xl:hidden`) — a Preact island
20
+ * component. Closed by default; tapping the header toggles, and tapping
21
+ * any entry closes the panel after navigation.
22
+ *
23
+ * Renders the `<div class="xl:hidden …">` panel directly. **The caller
24
+ * is responsible for wrapping this in `<Island when="load">`** so the
25
+ * SSG output emits the `data-zfb-island="MobileToc"` hydration marker
26
+ * around the panel. `<DocLayoutWithDefaults>` does this for you;
27
+ * consumers who render `<MobileToc>` outside the default layout (e.g.
28
+ * via the `mobileTocOverride` prop or in a custom layout) must apply
29
+ * the wrapper themselves — otherwise the open/close toggle never
30
+ * hydrates on the client and the panel stays in its initial closed
31
+ * state.
32
+ *
33
+ * Wave 13 (zudolab/zudo-doc#1355): previously this module exported a
34
+ * `MobileToc` wrapper that called `Island(...)` itself; on hydration
35
+ * the runtime ran `hydrate(<MobileToc/>, dataIslandDiv)` and the
36
+ * wrapper re-emitted *another* `<div data-zfb-island="MobileToc">…</div>`
37
+ * inside the existing one, leaving two `xl:hidden …` panels in the
38
+ * post-hydration DOM. See `./toc.tsx` for the full diagnosis. Moving
39
+ * the `<Island>` wrapper to the call site lets the bundle hydrate the
40
+ * bare panel against the existing DOM in-place.
41
+ *
42
+ * Like `Toc`, this always includes the `title` text in the SSG HTML
43
+ * even when no headings qualify. When no headings are present, a
44
+ * CSS-hidden container carries the title string so no-JS users and
45
+ * crawlers can still find the locale label in the static markup. When
46
+ * headings are present the full interactive toggle UI is rendered.
47
+ */
48
+ export function MobileToc({
49
+ headings,
50
+ title = "On this page",
51
+ }: MobileTocProps): VNode {
52
+ const filtered = useMemo(
53
+ () => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
54
+ [headings],
55
+ );
56
+ const [open, setOpen] = useState(false);
57
+
58
+ // No qualifying headings: emit a CSS-hidden container that still carries
59
+ // the locale title text. The `hidden` class sets display:none visually,
60
+ // but the text node remains in the serialized HTML so no-JS users and
61
+ // crawlers can still see the locale label ("On this page" / "目次").
62
+ // aria-hidden prevents screen readers from announcing the invisible label.
63
+ if (filtered.length === 0) {
64
+ return (
65
+ <div className="hidden" aria-hidden="true">
66
+ {title}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <div className="xl:hidden border border-muted mb-vsp-lg">
73
+ <button
74
+ type="button"
75
+ onClick={() => setOpen((prev) => !prev)}
76
+ aria-expanded={open}
77
+ className="flex w-full items-center justify-between px-hsp-lg py-vsp-xs text-small font-medium text-fg"
78
+ >
79
+ <span>{title}</span>
80
+ <svg
81
+ xmlns="http://www.w3.org/2000/svg"
82
+ aria-hidden="true"
83
+ className={cx(
84
+ "h-icon-sm w-icon-sm text-muted transition-transform duration-150",
85
+ open && "rotate-180",
86
+ )}
87
+ fill="none"
88
+ viewBox="0 0 24 24"
89
+ stroke="currentColor"
90
+ strokeWidth={2}
91
+ >
92
+ <path
93
+ strokeLinecap="round"
94
+ strokeLinejoin="round"
95
+ d="M19 9l-7 7-7-7"
96
+ />
97
+ </svg>
98
+ </button>
99
+ {/* Items list is always in the SSG HTML so anchor links are visible to
100
+ crawlers and JS-off users. Visibility is toggled by the `hidden` CSS
101
+ class — when `open` is false the list is display:none but the <a>
102
+ elements remain in the static markup, satisfying the a11y requirement
103
+ for keyboard accessibility after hydration. */}
104
+ <ul
105
+ className={cx(
106
+ "border-t border-muted px-hsp-lg py-vsp-xs space-y-vsp-2xs",
107
+ !open && "hidden",
108
+ )}
109
+ aria-hidden={!open}
110
+ >
111
+ {filtered.map((heading, index) => (
112
+ <li
113
+ key={`${heading.slug}-${index}`}
114
+ className={cx(
115
+ heading.depth === 3 && "ml-hsp-lg",
116
+ heading.depth === 4 && "ml-hsp-2xl",
117
+ )}
118
+ >
119
+ <a
120
+ href={`#${heading.slug}`}
121
+ onClick={() => setOpen(false)}
122
+ className="block py-vsp-2xs text-small text-muted hover:text-fg hover:underline focus-visible:underline"
123
+ >
124
+ <SmartBreak>{heading.text}</SmartBreak>
125
+ </a>
126
+ </li>
127
+ ))}
128
+ </ul>
129
+ </div>
130
+ );
131
+ }
132
+ // See `./toc.tsx` for rationale on the explicit displayName pin.
133
+ MobileToc.displayName = "MobileToc";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Locale-to-TOC-title mapping for the v2 package.
3
+ *
4
+ * Mirrors the `toc.title` key values from `src/config/i18n.ts` (and the
5
+ * create-zudo-doc template equivalent). This copy lives inside the v2
6
+ * package so `DocLayoutWithDefaults` can derive the correct default title
7
+ * from the `lang` HTML attribute without importing the host project's i18n
8
+ * module — keeping the package self-contained and host-agnostic.
9
+ *
10
+ * When adding a new locale to zudo-doc's supported set, add the
11
+ * corresponding entry here too.
12
+ */
13
+ const TOC_TITLES: Record<string, string> = {
14
+ en: "On this page",
15
+ ja: "目次",
16
+ de: "Auf dieser Seite",
17
+ };
18
+
19
+ const DEFAULT_TOC_TITLE = "On this page";
20
+
21
+ /**
22
+ * Return the TOC section label for the given BCP-47 language tag.
23
+ *
24
+ * Accepts full tags ("en-US", "ja-JP") by checking the primary subtag
25
+ * first, then falls back to the full tag, then falls back to the English
26
+ * default so callers always receive a non-empty string.
27
+ */
28
+ export function getTocTitle(lang?: string): string {
29
+ if (!lang) return DEFAULT_TOC_TITLE;
30
+ // "en-US" → try "en" first, then "en-US", then fallback
31
+ const primary = lang.split("-")[0]!;
32
+ return TOC_TITLES[primary] ?? TOC_TITLES[lang] ?? DEFAULT_TOC_TITLE;
33
+ }
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ /** @jsxRuntime automatic */
4
+ /** @jsxImportSource preact */
5
+
6
+ import type { VNode } from "preact";
7
+ import { useMemo } from "preact/hooks";
8
+
9
+ import { useActiveHeading } from "./use-active-heading.js";
10
+ import type { HeadingItem } from "./types.js";
11
+ import { SmartBreak } from "../smart-break/index.js";
12
+ import { cx } from "./cx.js";
13
+
14
+ export interface TocProps {
15
+ headings: readonly HeadingItem[];
16
+ /**
17
+ * Section label used by MobileToc. The desktop Toc no longer renders
18
+ * a visible heading (issue #1655 / T1) but the prop is kept so callers
19
+ * can share a single title value for both desktop and mobile surfaces.
20
+ * Defaults to "On this page" (English).
21
+ */
22
+ title?: string;
23
+ }
24
+
25
+ /**
26
+ * Desktop right-rail Table of Contents — a Preact island component.
27
+ *
28
+ * Renders the `<nav aria-label="Table of contents">` directly. **The
29
+ * caller is responsible for wrapping this in `<Island when="load">`**
30
+ * so the SSG output emits the `data-zfb-island="Toc"` hydration marker
31
+ * around the nav. `<DocLayoutWithDefaults>` does this for you; consumers
32
+ * who render `<Toc>` outside the default layout (e.g. via the
33
+ * `tocOverride` prop or in a custom layout) must apply the wrapper
34
+ * themselves — otherwise the active-section scroll-spy never hydrates
35
+ * on the client and the nav stays static.
36
+ *
37
+ * Wave 13 ("smoke-toc duplicate-nav regression", zudolab/zudo-doc#1355):
38
+ * before this refactor, this module exported a `Toc` wrapper that itself
39
+ * called `Island(...)` internally and an inner `TocInner` component that
40
+ * carried `displayName = "Toc"`. The zfb island scanner picked the
41
+ * exported `Toc` (the wrapper) as the hydration target, so on the
42
+ * client `mountIslands` ran `hydrate(<Toc/>, dataIslandDiv)` where the
43
+ * vnode itself rendered to *another* `<div data-zfb-island="Toc">…<nav>…</nav></div>`
44
+ * — Preact appended the new wrapper-div as a child of the existing
45
+ * data-zfb-island element instead of in-place hydrating the inner nav,
46
+ * leaving two `<nav aria-label="Table of contents">` elements in the
47
+ * post-hydration DOM. The host's Wave 12 prop-serialisation pin
48
+ * (`data-props`) made hydration succeed where it previously crashed
49
+ * silently on the missing `headings` prop, which is why the duplicate
50
+ * only became visible in CI smoke runs after Wave 12. Moving the
51
+ * `<Island>` wrapper to the call site lets the bundle hydrate the bare
52
+ * `<nav>` against the existing DOM in-place, eliminating the duplicate.
53
+ *
54
+ * Renders only depth 2–4 headings (h2/h3/h4), matching zudo-doc's
55
+ * historical behavior — the page title (h1) is rendered separately by
56
+ * the layout and h5+ are deemed too granular for the TOC.
57
+ *
58
+ * The `title` prop is kept on the signature (MobileToc and callers still
59
+ * use it) but the desktop Toc no longer renders a visible heading — the
60
+ * `<nav aria-label="Table of contents">` landmark provides the section
61
+ * semantics for screen readers. Issue #1655 / T1.
62
+ */
63
+ export function Toc({ headings, title = "On this page" }: TocProps): VNode {
64
+ const filtered = useMemo(
65
+ () => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
66
+ [headings],
67
+ );
68
+ const { activeId, activate } = useActiveHeading(filtered);
69
+
70
+ return (
71
+ <nav
72
+ aria-label="Table of contents"
73
+ className={cx(
74
+ "hidden xl:flex flex-col",
75
+ "w-[280px] shrink-0",
76
+ "sticky top-[3.5rem] self-start z-sidebar",
77
+ "pt-vsp-xl lg:pt-vsp-2xl",
78
+ "h-[calc(100vh-3.5rem)]",
79
+ )}
80
+ >
81
+ {filtered.length > 0 && (
82
+ <ul className="border-l border-muted pl-hsp-lg overflow-y-auto">
83
+ {filtered.map((heading, index) => {
84
+ const isActive = heading.slug === activeId;
85
+ return (
86
+ <li
87
+ key={`${heading.slug}-${index}`}
88
+ className={cx(
89
+ heading.depth === 3 && "ml-hsp-lg",
90
+ heading.depth === 4 && "ml-hsp-2xl",
91
+ )}
92
+ >
93
+ <a
94
+ href={`#${heading.slug}`}
95
+ onClick={() => activate(heading.slug)}
96
+ aria-current={isActive ? "true" : undefined}
97
+ className={cx(
98
+ "block py-vsp-2xs text-small leading-snug transition-colors",
99
+ isActive
100
+ ? "bg-fg text-bg font-medium"
101
+ : "text-muted hover:underline focus:underline",
102
+ )}
103
+ >
104
+ <SmartBreak>{heading.text}</SmartBreak>
105
+ </a>
106
+ </li>
107
+ );
108
+ })}
109
+ </ul>
110
+ )}
111
+ </nav>
112
+ );
113
+ }
114
+ // Pin the marker name to "Toc" explicitly so the SSR pass `<Island>` wrapper
115
+ // resolves a stable component identity even after esbuild minification has
116
+ // renamed the function. captureComponentName reads displayName first, then
117
+ // falls back to function.name; setting both means the SSR marker stays
118
+ // "Toc" no matter which path the runtime takes.
119
+ Toc.displayName = "Toc";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Heading metadata consumed by the TOC primitives.
3
+ *
4
+ * Shape mirrors the `headings` array exported from MDX modules by the
5
+ * underlying engine — `depth` (1–6), DOM `slug` (matching the rendered
6
+ * heading element's id), and the rendered `text`. Keep field names and
7
+ * types byte-aligned with the engine so a page module's `headings`
8
+ * export drops in directly.
9
+ */
10
+ export interface HeadingItem {
11
+ readonly depth: number;
12
+ readonly slug: string;
13
+ readonly text: string;
14
+ }
@@ -0,0 +1,162 @@
1
+ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
2
+ import type { HeadingItem } from "./types.js";
3
+
4
+ /**
5
+ * Pixel offset from the viewport top used as the "above the fold"
6
+ * threshold. A heading whose top is above this line is treated as
7
+ * having scrolled past — matches the original zudo-doc behavior so the
8
+ * scroll-spy keeps the same feel after the migration.
9
+ */
10
+ const SCROLL_MARGIN_TOP = 80;
11
+ const DEBOUNCE_MS = 200;
12
+
13
+ /**
14
+ * Pure helper that picks the active heading id given the heading slug
15
+ * order and a slug→element map. Extracted from the hook so it is unit-
16
+ * testable without DOM event plumbing.
17
+ *
18
+ * Algorithm: find the first heading whose top is at or below the
19
+ * SCROLL_MARGIN_TOP threshold ("first visible"). If none, the user has
20
+ * scrolled past every heading — return the last one. If the very first
21
+ * heading is the first visible AND its top is above the viewport
22
+ * midline, treat it as already active. Otherwise activate the
23
+ * predecessor of the first visible heading.
24
+ */
25
+ export function getActiveHeadingId(
26
+ headingIds: readonly string[],
27
+ elementMap: ReadonlyMap<string, HTMLElement>,
28
+ viewportHeight: number,
29
+ ): string | null {
30
+ if (headingIds.length === 0) return null;
31
+
32
+ let firstVisibleIndex = -1;
33
+ for (let i = 0; i < headingIds.length; i++) {
34
+ const el = elementMap.get(headingIds[i]!);
35
+ if (!el) continue;
36
+ const { top } = el.getBoundingClientRect();
37
+ if (top >= SCROLL_MARGIN_TOP) {
38
+ firstVisibleIndex = i;
39
+ break;
40
+ }
41
+ }
42
+
43
+ if (firstVisibleIndex === -1) {
44
+ return headingIds[headingIds.length - 1] ?? null;
45
+ }
46
+
47
+ if (firstVisibleIndex === 0) {
48
+ const el = elementMap.get(headingIds[0]!);
49
+ if (el) {
50
+ const { top } = el.getBoundingClientRect();
51
+ if (top < viewportHeight / 2) {
52
+ return headingIds[0] ?? null;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ const el = elementMap.get(headingIds[firstVisibleIndex]!);
59
+ if (el) {
60
+ const { top } = el.getBoundingClientRect();
61
+ if (top < viewportHeight / 2) {
62
+ return headingIds[firstVisibleIndex] ?? null;
63
+ }
64
+ }
65
+
66
+ return headingIds[firstVisibleIndex - 1] ?? null;
67
+ }
68
+
69
+ export interface UseActiveHeadingResult {
70
+ activeId: string | null;
71
+ activate: (id: string) => void;
72
+ }
73
+
74
+ /**
75
+ * Scroll-spy hook. Tracks which heading slug should be highlighted
76
+ * based on the current scroll position. Provides an `activate(id)`
77
+ * imperatively-callable helper used by click handlers — sets the
78
+ * active id immediately and suppresses the scroll-driven update until
79
+ * the smooth-scroll completes (`scrollend`, with a timeout fallback
80
+ * for older Safari).
81
+ */
82
+ export function useActiveHeading(
83
+ headings: readonly HeadingItem[],
84
+ ): UseActiveHeadingResult {
85
+ const [activeId, setActiveId] = useState<string | null>(null);
86
+ const headingIdsRef = useRef<readonly string[]>([]);
87
+ const elementMapRef = useRef<Map<string, HTMLElement>>(new Map());
88
+ const suppressedRef = useRef(false);
89
+
90
+ const activate = useCallback((id: string) => {
91
+ setActiveId(id);
92
+ suppressedRef.current = true;
93
+ // Safety timeout: unsuppress if no scroll event fires (target already in view)
94
+ setTimeout(() => {
95
+ suppressedRef.current = false;
96
+ }, 2000);
97
+ }, []);
98
+
99
+ useEffect(() => {
100
+ const ids = headings.map((h) => h.slug);
101
+ headingIdsRef.current = ids;
102
+
103
+ const map = new Map<string, HTMLElement>();
104
+ for (const id of ids) {
105
+ const el = document.getElementById(id);
106
+ if (el) map.set(id, el);
107
+ }
108
+ elementMapRef.current = map;
109
+
110
+ let timerId: ReturnType<typeof setTimeout> | null = null;
111
+
112
+ function update() {
113
+ timerId = null;
114
+ setActiveId(
115
+ getActiveHeadingId(
116
+ headingIdsRef.current,
117
+ elementMapRef.current,
118
+ window.innerHeight,
119
+ ),
120
+ );
121
+ }
122
+
123
+ let fallbackTimerId: ReturnType<typeof setTimeout> | null = null;
124
+
125
+ function onScroll() {
126
+ if (suppressedRef.current) {
127
+ // Fallback for browsers without scrollend (Safari < 18)
128
+ if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
129
+ fallbackTimerId = setTimeout(onScrollEnd, 1500);
130
+ return;
131
+ }
132
+ if (timerId !== null) clearTimeout(timerId);
133
+ timerId = setTimeout(update, DEBOUNCE_MS);
134
+ }
135
+
136
+ function onScrollEnd() {
137
+ suppressedRef.current = false;
138
+ if (fallbackTimerId !== null) {
139
+ clearTimeout(fallbackTimerId);
140
+ fallbackTimerId = null;
141
+ }
142
+ if (timerId !== null) clearTimeout(timerId);
143
+ update();
144
+ }
145
+
146
+ update();
147
+
148
+ window.addEventListener("scroll", onScroll, { passive: true });
149
+ window.addEventListener("resize", onScroll, { passive: true });
150
+ window.addEventListener("scrollend", onScrollEnd, { passive: true });
151
+
152
+ return () => {
153
+ window.removeEventListener("scroll", onScroll);
154
+ window.removeEventListener("resize", onScroll);
155
+ window.removeEventListener("scrollend", onScrollEnd);
156
+ if (timerId !== null) clearTimeout(timerId);
157
+ if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
158
+ };
159
+ }, [headings]);
160
+
161
+ return { activeId, activate };
162
+ }