@takazudo/zudo-doc 5.28.2 → 6.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 (456) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +49 -16
  3. package/bin/gen-z-index.mjs +18 -18
  4. package/dist/agent-docs/identity.d.ts +4 -0
  5. package/dist/agent-docs/identity.js +24 -0
  6. package/dist/agent-docs/index.d.ts +5 -0
  7. package/dist/agent-docs/index.js +5 -0
  8. package/dist/agent-docs/limits.d.ts +11 -0
  9. package/dist/agent-docs/limits.js +24 -0
  10. package/dist/agent-docs/search.d.ts +14 -0
  11. package/dist/agent-docs/search.js +82 -0
  12. package/dist/agent-docs/tokenize.d.ts +2 -0
  13. package/dist/agent-docs/tokenize.js +22 -0
  14. package/dist/agent-docs/types.d.ts +68 -0
  15. package/dist/agent-docs/types.js +0 -0
  16. package/dist/ai-chat-modal/index.d.ts +1 -4
  17. package/dist/ai-chat-modal/index.js +183 -123
  18. package/dist/asset-components/asset-card.d.ts +1 -2
  19. package/dist/asset-components/asset-card.js +12 -12
  20. package/dist/asset-components/asset-code.d.ts +1 -2
  21. package/dist/asset-components/asset-code.js +10 -10
  22. package/dist/asset-components/shared.d.ts +3 -4
  23. package/dist/asset-components/shared.js +4 -4
  24. package/dist/asset-index-page/body.d.ts +5 -6
  25. package/dist/asset-index-page/body.js +6 -6
  26. package/dist/asset-index-page/index.d.ts +1 -2
  27. package/dist/asset-index-page/index.js +1 -1
  28. package/dist/asset-page/body.d.ts +5 -9
  29. package/dist/asset-page/body.js +3 -3
  30. package/dist/asset-page/components.d.ts +16 -17
  31. package/dist/asset-page/components.js +10 -9
  32. package/dist/asset-page/index.d.ts +1 -2
  33. package/dist/asset-page/index.js +1 -1
  34. package/dist/auto-logo/index.d.ts +1 -2
  35. package/dist/auto-logo/index.js +2 -2
  36. package/dist/body-foot-util/body-foot-util-area.d.ts +3 -4
  37. package/dist/body-foot-util/body-foot-util-area.js +1 -2
  38. package/dist/body-foot-util/edit-link.d.ts +2 -3
  39. package/dist/body-foot-util/edit-link.js +1 -2
  40. package/dist/breadcrumb/breadcrumb.d.ts +6 -6
  41. package/dist/breadcrumb/breadcrumb.js +3 -4
  42. package/dist/category-nav/index.d.ts +1 -2
  43. package/dist/category-nav/index.js +11 -5
  44. package/dist/category-tree-nav/index.d.ts +1 -2
  45. package/dist/category-tree-nav/index.js +3 -3
  46. package/dist/chrome/derive.d.ts +18 -27
  47. package/dist/chrome/derive.js +47 -29
  48. package/dist/chrome/index.d.ts +5 -5
  49. package/dist/chrome/index.js +1 -1
  50. package/dist/chrome/island-data.d.ts +4 -0
  51. package/dist/chrome/island-data.js +52 -0
  52. package/dist/chrome/primary-slots.d.ts +2 -3
  53. package/dist/chrome-bindings.d.ts +28 -22
  54. package/dist/code-group/index.d.ts +3 -4
  55. package/dist/code-group/index.js +3 -7
  56. package/dist/code-syntax/code-block-enhancer.d.ts +1 -2
  57. package/dist/code-syntax/code-block-enhancer.js +2 -2
  58. package/dist/code-syntax/mermaid-init.d.ts +1 -2
  59. package/dist/code-syntax/mermaid-init.js +5 -2
  60. package/dist/code-syntax/tabs-init-script.js +1 -1
  61. package/dist/code-syntax/tabs-init.d.ts +1 -2
  62. package/dist/code-syntax/tabs-init.js +2 -2
  63. package/dist/code-syntax/tabs.d.ts +6 -6
  64. package/dist/code-syntax/tabs.js +8 -8
  65. package/dist/compiled.css +2933 -2943
  66. package/dist/config-assertions/index.d.ts +13 -0
  67. package/dist/config-assertions/index.js +14 -0
  68. package/dist/config.d.ts +36 -17
  69. package/dist/config.js +23 -95
  70. package/dist/content/content-blockquote.d.ts +2 -3
  71. package/dist/content/content-blockquote.js +3 -3
  72. package/dist/content/content-code.d.ts +3 -4
  73. package/dist/content/content-code.js +5 -5
  74. package/dist/content/content-link.d.ts +3 -4
  75. package/dist/content/content-link.js +10 -10
  76. package/dist/content/content-ol.d.ts +2 -3
  77. package/dist/content/content-ol.js +5 -4
  78. package/dist/content/content-paragraph.d.ts +2 -3
  79. package/dist/content/content-paragraph.js +1 -1
  80. package/dist/content/content-strong.d.ts +2 -3
  81. package/dist/content/content-strong.js +3 -3
  82. package/dist/content/content-table.d.ts +2 -3
  83. package/dist/content/content-table.js +4 -4
  84. package/dist/content/content-ul.d.ts +2 -3
  85. package/dist/content/content-ul.js +4 -4
  86. package/dist/content/heading-h2.d.ts +2 -3
  87. package/dist/content/heading-h2.js +4 -4
  88. package/dist/content/heading-h3.d.ts +2 -3
  89. package/dist/content/heading-h3.js +4 -4
  90. package/dist/content/heading-h4.d.ts +2 -3
  91. package/dist/content/heading-h4.js +4 -4
  92. package/dist/content-admonition/index.d.ts +3 -4
  93. package/dist/content-admonition/index.js +2 -2
  94. package/dist/content.css +35 -33
  95. package/dist/design-token-panel-bootstrap-controller.d.ts +178 -0
  96. package/dist/design-token-panel-bootstrap-controller.js +359 -0
  97. package/dist/design-token-panel-bootstrap-island.d.ts +5 -0
  98. package/dist/design-token-panel-bootstrap-island.js +19 -0
  99. package/dist/design-token-panel-bootstrap.d.ts +5 -195
  100. package/dist/design-token-panel-bootstrap.js +3 -288
  101. package/dist/desktop-sidebar-toggle-island/index.d.ts +2 -7
  102. package/dist/desktop-sidebar-toggle-island/index.js +2 -58
  103. package/dist/desktop-sidebar-toggle-island/island.d.ts +4 -0
  104. package/dist/desktop-sidebar-toggle-island/island.js +43 -0
  105. package/dist/desktop-sidebar-toggle-island/storage.d.ts +4 -0
  106. package/dist/desktop-sidebar-toggle-island/storage.js +29 -0
  107. package/dist/desktop-toc-toggle-island/index.d.ts +2 -7
  108. package/dist/desktop-toc-toggle-island/index.js +2 -58
  109. package/dist/desktop-toc-toggle-island/island.d.ts +4 -0
  110. package/dist/desktop-toc-toggle-island/island.js +43 -0
  111. package/dist/desktop-toc-toggle-island/storage.d.ts +4 -0
  112. package/dist/desktop-toc-toggle-island/storage.js +29 -0
  113. package/dist/details/details.d.ts +3 -4
  114. package/dist/details/details.js +1 -1
  115. package/dist/doc-body-end/index.d.ts +1 -2
  116. package/dist/doc-body-end/index.js +1 -1
  117. package/dist/doc-body-end-islands/design-token-panel-island.d.ts +2 -2
  118. package/dist/doc-body-end-islands/design-token-panel-island.js +3 -7
  119. package/dist/doc-body-end-islands/index.d.ts +4 -21
  120. package/dist/doc-body-end-islands/index.js +5 -3
  121. package/dist/doc-body-end-islands/theme-pack-switcher-island.d.ts +3 -13
  122. package/dist/doc-body-end-islands/theme-pack-switcher-island.js +11 -13
  123. package/dist/doc-content-header/index.d.ts +1 -2
  124. package/dist/doc-content-header/index.js +1 -1
  125. package/dist/doc-history/index.d.ts +1 -1
  126. package/dist/doc-history/index.js +166 -141
  127. package/dist/doc-history-area/index.d.ts +6 -4
  128. package/dist/doc-history-area/index.js +17 -16
  129. package/dist/doc-metainfo-area/index.d.ts +2 -3
  130. package/dist/doc-metainfo-area/index.js +1 -1
  131. package/dist/doc-page-renderer/index.d.ts +13 -7
  132. package/dist/doc-page-renderer/index.js +10 -1
  133. package/dist/doc-page-shell/index.d.ts +20 -7
  134. package/dist/doc-page-shell/index.js +17 -6
  135. package/dist/doc-pager/index.d.ts +1 -2
  136. package/dist/doc-pager/index.js +3 -3
  137. package/dist/doc-tags-area/index.d.ts +2 -3
  138. package/dist/doc-tags-area/index.js +1 -1
  139. package/dist/doclayout/doc-layout-with-defaults.d.ts +9 -9
  140. package/dist/doclayout/doc-layout-with-defaults.js +5 -15
  141. package/dist/doclayout/doc-layout.d.ts +26 -27
  142. package/dist/doclayout/doc-layout.js +23 -14
  143. package/dist/docs-schema/index.d.ts +4 -0
  144. package/dist/docs-schema/index.js +1 -0
  145. package/dist/extract-headings/index.js +43 -12
  146. package/dist/factory-context/index.d.ts +22 -19
  147. package/dist/features.css +70 -8
  148. package/dist/find-in-page/find-bar.d.ts +4 -2
  149. package/dist/find-in-page/find-bar.js +58 -69
  150. package/dist/find-in-page/index.d.ts +2 -8
  151. package/dist/find-in-page/index.js +33 -30
  152. package/dist/footer/footer.d.ts +3 -4
  153. package/dist/footer/footer.js +3 -3
  154. package/dist/footer-with-defaults/index.d.ts +2 -3
  155. package/dist/footer-with-defaults/index.js +1 -1
  156. package/dist/frontmatter-preview-defaults/index.js +1 -0
  157. package/dist/head/doc-head.d.ts +1 -1
  158. package/dist/head/doc-head.js +24 -32
  159. package/dist/head/og-tags.d.ts +1 -1
  160. package/dist/head/og-tags.js +8 -7
  161. package/dist/head/twitter-card.d.ts +1 -1
  162. package/dist/head/twitter-card.js +1 -1
  163. package/dist/head-with-defaults/index.d.ts +7 -2
  164. package/dist/head-with-defaults/index.js +46 -32
  165. package/dist/header/header.d.ts +8 -8
  166. package/dist/header/header.js +9 -11
  167. package/dist/header/types.d.ts +7 -7
  168. package/dist/header-with-defaults/index.d.ts +1 -2
  169. package/dist/header-with-defaults/index.js +18 -8
  170. package/dist/home-intro/index.d.ts +2 -5
  171. package/dist/home-intro/index.js +7 -5
  172. package/dist/home-intro/prepare.js +4 -3
  173. package/dist/home-page/index.d.ts +3 -3
  174. package/dist/home-page/index.js +19 -17
  175. package/dist/html-preview-wrapper/highlighted-code.d.ts +3 -4
  176. package/dist/html-preview-wrapper/highlighted-code.js +19 -17
  177. package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +4 -25
  178. package/dist/html-preview-wrapper/html-preview-wrapper.js +2 -87
  179. package/dist/html-preview-wrapper/html-preview.d.ts +3 -10
  180. package/dist/html-preview-wrapper/html-preview.js +13 -37
  181. package/dist/html-preview-wrapper/index.d.ts +2 -1
  182. package/dist/html-preview-wrapper/index.js +1 -1
  183. package/dist/html-preview-wrapper/preview-base.d.ts +3 -5
  184. package/dist/html-preview-wrapper/preview-base.js +56 -59
  185. package/dist/html-preview-wrapper/wrapper.d.ts +20 -0
  186. package/dist/html-preview-wrapper/wrapper.js +37 -0
  187. package/dist/hydration-pending.d.ts +3 -5
  188. package/dist/hydration-pending.js +7 -5
  189. package/dist/i18n-version/language-switcher.d.ts +5 -32
  190. package/dist/i18n-version/language-switcher.js +5 -106
  191. package/dist/i18n-version/switcher-generated-scripts.d.ts +2 -0
  192. package/dist/i18n-version/switcher-generated-scripts.js +107 -0
  193. package/dist/i18n-version/switcher-url-state.d.ts +76 -0
  194. package/dist/i18n-version/switcher-url-state.js +103 -0
  195. package/dist/i18n-version/version-banner.d.ts +2 -3
  196. package/dist/i18n-version/version-banner.js +1 -1
  197. package/dist/i18n-version/version-switcher.d.ts +5 -50
  198. package/dist/i18n-version/version-switcher.js +5 -157
  199. package/dist/icons/index.d.ts +18 -19
  200. package/dist/icons/index.js +17 -33
  201. package/dist/image-enlarge/index.d.ts +1 -10
  202. package/dist/image-enlarge/index.js +80 -55
  203. package/dist/image-enlarge/public.d.ts +2 -0
  204. package/dist/image-enlarge/public.js +6 -0
  205. package/dist/image-enlarge/ssr-fallback.d.ts +2 -0
  206. package/dist/image-enlarge/ssr-fallback.js +8 -0
  207. package/dist/index.d.ts +11 -4
  208. package/dist/inline-version-switcher/index.d.ts +1 -2
  209. package/dist/inline-version-switcher/index.js +1 -1
  210. package/dist/island-types/index.d.ts +2 -2
  211. package/dist/island-types/index.js +1 -5
  212. package/dist/math-block/index.d.ts +4 -7
  213. package/dist/math-block/index.js +5 -5
  214. package/dist/mcp/handler.d.ts +8 -0
  215. package/dist/mcp/handler.js +129 -0
  216. package/dist/mcp/index.d.ts +4 -0
  217. package/dist/mcp/index.js +7 -0
  218. package/dist/mcp/loader.d.ts +19 -0
  219. package/dist/mcp/loader.js +202 -0
  220. package/dist/mdx-components/index.js +40 -108
  221. package/dist/mermaid-enlarge/index.d.ts +1 -8
  222. package/dist/mermaid-enlarge/index.js +174 -158
  223. package/dist/mermaid-enlarge/public.d.ts +2 -0
  224. package/dist/mermaid-enlarge/public.js +6 -0
  225. package/dist/mermaid-enlarge/ssr-fallback.d.ts +2 -0
  226. package/dist/mermaid-enlarge/ssr-fallback.js +8 -0
  227. package/dist/metainfo/doc-metainfo.d.ts +2 -3
  228. package/dist/metainfo/doc-metainfo.js +1 -4
  229. package/dist/metainfo/doc-tags.d.ts +2 -3
  230. package/dist/metainfo/doc-tags.js +1 -1
  231. package/dist/metainfo/frontmatter-preview.d.ts +3 -4
  232. package/dist/metainfo/frontmatter-preview.js +1 -1
  233. package/dist/nav-data-prep/index.js +6 -4
  234. package/dist/nav-indexing/category-nav.d.ts +2 -3
  235. package/dist/nav-indexing/category-nav.js +1 -2
  236. package/dist/nav-indexing/category-tree-nav.d.ts +2 -3
  237. package/dist/nav-indexing/category-tree-nav.js +1 -1
  238. package/dist/nav-indexing/doc-card-grid.d.ts +2 -3
  239. package/dist/nav-indexing/doc-card-grid.js +1 -2
  240. package/dist/nav-indexing/docs-sitemap.d.ts +2 -3
  241. package/dist/nav-indexing/docs-sitemap.js +2 -2
  242. package/dist/nav-indexing/index.d.ts +1 -1
  243. package/dist/nav-indexing/nav-card-grid.d.ts +2 -3
  244. package/dist/nav-indexing/nav-card-grid.js +1 -2
  245. package/dist/nav-indexing/note-tray-index-parts/card-list.d.ts +1 -2
  246. package/dist/nav-indexing/note-tray-index-parts/card-list.js +4 -4
  247. package/dist/nav-indexing/note-tray-index-parts/date-line.d.ts +2 -3
  248. package/dist/nav-indexing/note-tray-index-parts/date-line.js +1 -1
  249. package/dist/nav-indexing/note-tray-index-parts/index-list.d.ts +1 -2
  250. package/dist/nav-indexing/note-tray-index-parts/index-list.js +2 -2
  251. package/dist/nav-indexing/note-tray-index-parts/timeline.d.ts +1 -2
  252. package/dist/nav-indexing/note-tray-index-parts/timeline.js +2 -2
  253. package/dist/nav-indexing/note-tray-index.d.ts +1 -2
  254. package/dist/nav-indexing/note-tray-index.js +1 -1
  255. package/dist/nav-indexing/site-tree-nav-demo.d.ts +2 -3
  256. package/dist/nav-indexing/site-tree-nav-demo.js +2 -2
  257. package/dist/nav-indexing/tag-nav.d.ts +1 -2
  258. package/dist/nav-indexing/tag-nav.js +1 -1
  259. package/dist/nav-indexing/types.d.ts +2 -2
  260. package/dist/nav-indexing/versions-page-content.d.ts +2 -3
  261. package/dist/nav-indexing/versions-page-content.js +8 -8
  262. package/dist/note-tray-index/index.d.ts +1 -2
  263. package/dist/note-tray-index/index.js +1 -1
  264. package/dist/page-loading/page-loading-overlay.d.ts +1 -2
  265. package/dist/page-loading/page-loading-overlay.js +3 -5
  266. package/dist/plugins/agent-export.d.ts +3 -0
  267. package/dist/plugins/agent-export.js +34 -0
  268. package/dist/plugins/internal/agent-export/chunk.d.ts +5 -0
  269. package/dist/plugins/internal/agent-export/chunk.js +90 -0
  270. package/dist/plugins/internal/agent-export/index.d.ts +24 -0
  271. package/dist/plugins/internal/agent-export/index.js +123 -0
  272. package/dist/plugins/internal/agent-export/normalize.d.ts +5 -0
  273. package/dist/plugins/internal/agent-export/normalize.js +73 -0
  274. package/dist/plugins/internal/llms-txt/dev-middleware.d.ts +6 -0
  275. package/dist/plugins/internal/llms-txt/dev-middleware.js +8 -3
  276. package/dist/plugins/internal/llms-txt/emit.js +23 -6
  277. package/dist/plugins/internal/llms-txt/generate.d.ts +7 -1
  278. package/dist/plugins/internal/llms-txt/generate.js +9 -2
  279. package/dist/plugins/internal/llms-txt/load.d.ts +2 -0
  280. package/dist/plugins/internal/llms-txt/load.js +20 -4
  281. package/dist/plugins/internal/llms-txt/types.d.ts +10 -0
  282. package/dist/plugins/routes.js +4 -0
  283. package/dist/preset.d.ts +19 -9
  284. package/dist/preset.js +20 -1
  285. package/dist/route-context-payload/index.js +1 -1
  286. package/dist/routes/404.d.ts +2 -3
  287. package/dist/routes/404.js +1 -1
  288. package/dist/routes/_chrome.d.ts +5 -5
  289. package/dist/routes/_chrome.js +7 -2
  290. package/dist/routes/_context.d.ts +4 -4
  291. package/dist/routes/_design-token-panel-bootstrap.d.ts +1 -4
  292. package/dist/routes/_design-token-panel-bootstrap.js +9 -1
  293. package/dist/routes/docs-slug.d.ts +2 -3
  294. package/dist/routes/docs-tags-index.d.ts +2 -3
  295. package/dist/routes/docs-tags-index.js +1 -1
  296. package/dist/routes/docs-tags-tag.d.ts +2 -3
  297. package/dist/routes/docs-tags-tag.js +1 -1
  298. package/dist/routes/docs-versions.d.ts +2 -3
  299. package/dist/routes/docs-versions.js +1 -1
  300. package/dist/routes/files-path.d.ts +2 -3
  301. package/dist/routes/files-path.js +1 -1
  302. package/dist/routes/index.d.ts +2 -3
  303. package/dist/routes/index.js +1 -1
  304. package/dist/routes/locale-docs-slug.d.ts +2 -3
  305. package/dist/routes/locale-docs-tags-index.d.ts +2 -3
  306. package/dist/routes/locale-docs-tags-index.js +1 -1
  307. package/dist/routes/locale-docs-tags-tag.d.ts +2 -3
  308. package/dist/routes/locale-docs-tags-tag.js +1 -1
  309. package/dist/routes/locale-docs-versions.d.ts +2 -3
  310. package/dist/routes/locale-docs-versions.js +1 -1
  311. package/dist/routes/locale-files-path.d.ts +2 -3
  312. package/dist/routes/locale-files-path.js +1 -1
  313. package/dist/routes/locale-index.d.ts +2 -3
  314. package/dist/routes/locale-index.js +1 -1
  315. package/dist/routes/mcp.d.ts +5 -0
  316. package/dist/routes/mcp.js +25 -0
  317. package/dist/routes/v-docs-slug.d.ts +2 -3
  318. package/dist/routes/v-locale-docs-slug.d.ts +2 -3
  319. package/dist/search-widget/index.d.ts +1 -2
  320. package/dist/search-widget/index.js +4 -7
  321. package/dist/search-widget-script/generated-script.js +12 -5
  322. package/dist/settings-defaults.d.ts +2 -0
  323. package/dist/settings-defaults.js +93 -0
  324. package/dist/settings.d.ts +8 -2
  325. package/dist/sidebar/index.d.ts +1 -1
  326. package/dist/sidebar/sidebar.d.ts +12 -44
  327. package/dist/sidebar/sidebar.js +4 -3
  328. package/dist/sidebar/types.d.ts +14 -0
  329. package/dist/sidebar-prepaint/index.d.ts +1 -2
  330. package/dist/sidebar-prepaint/index.js +4 -6
  331. package/dist/sidebar-resizer/sidebar-resizer-init.d.ts +2 -3
  332. package/dist/sidebar-resizer/sidebar-resizer-init.js +3 -3
  333. package/dist/sidebar-toggle-island/index.d.ts +8 -11
  334. package/dist/sidebar-toggle-island/index.js +75 -62
  335. package/dist/sidebar-tree/build-tree.js +5 -6
  336. package/dist/sidebar-tree/types.d.ts +2 -0
  337. package/dist/sidebar-tree-island/index.d.ts +4 -19
  338. package/dist/sidebar-tree-island/index.js +782 -436
  339. package/dist/sidebar-tree-island/scope-state.d.ts +38 -0
  340. package/dist/sidebar-tree-island/scope-state.js +151 -0
  341. package/dist/sidebar-tree-island/sidebar-scroll-preserve.js +6 -2
  342. package/dist/sidebar-utils/index.d.ts +17 -19
  343. package/dist/sidebar-utils/index.js +100 -3
  344. package/dist/sidebar-utils/scope.d.ts +11 -0
  345. package/dist/sidebar-utils/scope.js +67 -0
  346. package/dist/sidebar-with-defaults/index.d.ts +1 -2
  347. package/dist/sidebar-with-defaults/index.js +19 -18
  348. package/dist/site-schema/nav-tree.js +15 -15
  349. package/dist/site-schema/types.d.ts +1 -0
  350. package/dist/site-tree-nav/index.d.ts +2 -3
  351. package/dist/site-tree-nav/index.js +11 -12
  352. package/dist/site-tree-nav/normalize-island-props.d.ts +16 -0
  353. package/dist/site-tree-nav/normalize-island-props.js +122 -0
  354. package/dist/site-tree-nav-island/index.d.ts +3 -3
  355. package/dist/site-tree-nav-island/index.js +243 -144
  356. package/dist/smart-break/index.d.ts +3 -4
  357. package/dist/smart-break/index.js +1 -1
  358. package/dist/tab-item/tab-item.d.ts +3 -4
  359. package/dist/tab-item/tab-item.js +1 -1
  360. package/dist/tag-pages/index.d.ts +3 -3
  361. package/dist/tag-pages/index.js +10 -10
  362. package/dist/theme/color-scheme-provider.d.ts +3 -3
  363. package/dist/theme/color-scheme-provider.js +3 -3
  364. package/dist/theme/theme-pack-provider.d.ts +1 -1
  365. package/dist/theme/theme-pack-provider.js +3 -3
  366. package/dist/theme/theme-toggle.d.ts +2 -3
  367. package/dist/theme/theme-toggle.js +1 -1
  368. package/dist/theme-pack-dialog/index.d.ts +1 -1
  369. package/dist/theme-pack-dialog/index.js +69 -75
  370. package/dist/theme-pack-dialog/theme-pack-card.d.ts +3 -4
  371. package/dist/theme-pack-dialog/theme-pack-card.js +19 -17
  372. package/dist/theme-pack-switcher/index.d.ts +4 -5
  373. package/dist/theme-pack-switcher/index.js +52 -49
  374. package/dist/theme-toggle/index.d.ts +1 -1
  375. package/dist/theme-toggle/index.js +154 -167
  376. package/dist/theme.css +47 -121
  377. package/dist/toc/mobile-toc.d.ts +3 -5
  378. package/dist/toc/mobile-toc.js +29 -27
  379. package/dist/toc/toc.d.ts +3 -4
  380. package/dist/toc/toc.js +18 -18
  381. package/dist/toc/use-active-heading.d.ts +5 -4
  382. package/dist/toc/use-active-heading.js +36 -30
  383. package/dist/toc-prepaint/index.d.ts +1 -2
  384. package/dist/toc-prepaint/index.js +4 -6
  385. package/dist/transitions/nested-island-props-refresh.d.ts +3 -16
  386. package/dist/transitions/nested-island-props-refresh.js +133 -151
  387. package/dist/transitions/page-events.d.ts +8 -13
  388. package/dist/tree-nav-shared/index.d.ts +4 -5
  389. package/dist/tree-nav-shared/index.js +6 -7
  390. package/dist/use-modal-dialog/index.d.ts +9 -63
  391. package/dist/use-modal-dialog/index.js +41 -59
  392. package/dist/versions-page/index.d.ts +3 -3
  393. package/dist/versions-page/index.js +5 -5
  394. package/dist/wind/index.d.ts +5 -0
  395. package/dist/wind/index.js +169 -0
  396. package/dist/wind.json +498 -0
  397. package/dist/z-index-defaults/index.d.ts +4 -4
  398. package/dist/zdtp-loader.d.ts +7 -0
  399. package/dist/zdtp-loader.js +8 -0
  400. package/eject/asset-components/asset-card.tsx +14 -16
  401. package/eject/asset-components/asset-code.tsx +10 -12
  402. package/eject/asset-components/shared.tsx +5 -7
  403. package/eject/breadcrumb/breadcrumb.tsx +15 -16
  404. package/eject/code-group/index.tsx +4 -10
  405. package/eject/content-admonition/index.tsx +5 -6
  406. package/eject/desktop-sidebar-toggle-island/index.ts +4 -0
  407. package/eject/desktop-sidebar-toggle-island/island.tsx +50 -0
  408. package/eject/desktop-sidebar-toggle-island/storage.ts +27 -0
  409. package/eject/details/details.tsx +3 -5
  410. package/eject/doc-history/index.tsx +193 -179
  411. package/eject/doc-pager/index.tsx +3 -4
  412. package/eject/footer/footer.tsx +7 -9
  413. package/eject/header/header.tsx +42 -54
  414. package/eject/header/nav-overflow-script.ts +1 -1
  415. package/eject/header/types.ts +12 -13
  416. package/eject/image-enlarge/index.tsx +84 -69
  417. package/eject/image-enlarge/public.ts +2 -0
  418. package/eject/image-enlarge/ssr-fallback.tsx +7 -0
  419. package/eject/page-loading/page-loading-overlay.tsx +8 -15
  420. package/eject/sidebar/index.ts +1 -0
  421. package/eject/sidebar/sidebar.tsx +16 -97
  422. package/eject/sidebar/types.ts +15 -0
  423. package/eject/sidebar-toggle-island/index.tsx +103 -102
  424. package/eject/sidebar-tree-island/index.tsx +827 -615
  425. package/eject/sidebar-tree-island/scope-state.ts +154 -0
  426. package/eject/sidebar-tree-island/sidebar-scroll-preserve.ts +8 -2
  427. package/eject/site-tree-nav-island/index.tsx +349 -222
  428. package/eject/tab-item/tab-item.tsx +2 -4
  429. package/eject/theme-toggle/hydration-pending.ts +6 -10
  430. package/eject/theme-toggle/index.tsx +133 -173
  431. package/eject/toc/mobile-toc.tsx +27 -31
  432. package/eject/toc/toc.tsx +21 -24
  433. package/eject/toc/use-active-heading.ts +43 -38
  434. package/package.json +40 -22
  435. package/routes-src/404.tsx +2 -3
  436. package/routes-src/_chrome.tsx +7 -2
  437. package/routes-src/_design-token-panel-bootstrap.tsx +1 -63
  438. package/routes-src/docs-slug.tsx +2 -3
  439. package/routes-src/docs-tags-index.tsx +2 -3
  440. package/routes-src/docs-tags-tag.tsx +2 -3
  441. package/routes-src/docs-versions.tsx +2 -3
  442. package/routes-src/files-path.tsx +2 -4
  443. package/routes-src/index.tsx +2 -3
  444. package/routes-src/locale-docs-slug.tsx +2 -3
  445. package/routes-src/locale-docs-tags-index.tsx +2 -3
  446. package/routes-src/locale-docs-tags-tag.tsx +2 -3
  447. package/routes-src/locale-docs-versions.tsx +2 -3
  448. package/routes-src/locale-files-path.tsx +2 -4
  449. package/routes-src/locale-index.tsx +2 -3
  450. package/routes-src/mcp.tsx +25 -0
  451. package/routes-src/v-docs-slug.tsx +2 -3
  452. package/routes-src/v-locale-docs-slug.tsx +2 -3
  453. package/tsconfig.base.json +1 -1
  454. package/dist/safelist.css +0 -2
  455. package/dist/theme-no-reset.css +0 -379
  456. package/eject/desktop-sidebar-toggle-island/index.tsx +0 -110
@@ -1,25 +1,39 @@
1
1
  "use client";
2
2
 
3
- /** @jsxRuntime automatic */
4
- /** @jsxImportSource preact */
5
- // Use preact hook entrypoints directly — the "react" → "preact/compat" alias
6
- // lets us consume React-typed components in this Preact app.
7
- import { useState, useCallback, useEffect, useMemo, useRef } from "preact/hooks";
8
- import { memo } from "preact/compat";
9
- import type { SidebarNavNode, SidebarRootMenuItem, SidebarLocaleLink } from "../sidebar/types.js";
3
+ import {
4
+ computed,
5
+ For,
6
+ getScope,
7
+ Show,
8
+ signal,
9
+ type ReadonlySignal,
10
+ type Ref,
11
+ } from "@takazudo/zfb/zudo-react";
12
+ import type {
13
+ SidebarNavNode,
14
+ SidebarNavigationContext,
15
+ SidebarRootMenuItem,
16
+ SidebarLocaleLink,
17
+ } from "../sidebar/types.js";
10
18
  import type { ResolvedDateFormats } from "../settings.js";
11
- import { INDENT, BASE_PAD, connectorLeft, ConnectorLines, CategoryLinkIcon } from "../tree-nav-shared/index.js";
19
+ import {
20
+ INDENT,
21
+ BASE_PAD,
22
+ connectorLeft,
23
+ ConnectorLines,
24
+ CategoryLinkIcon,
25
+ } from "../tree-nav-shared/index.js";
12
26
  import { ChevronRight, ChevronLeft, Search } from "../icons/index.js";
13
- // BARE ThemeToggle — renders inside the SidebarToggle island, so it must
14
- // NOT bring its own island wrapper.
15
27
  import { ThemeToggle, type ThemeToggleLabels } from "../theme-toggle/index.js";
16
28
  import { smartBreakToHtml } from "../smart-break/index.js";
17
- // After zudolab/zudo-doc#1335 the host components also pull lifecycle event
18
- // names from the v2 transitions module rather than hard-coding literals.
19
- import { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT } from "../transitions/index.js";
29
+ import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
30
+ import { useSidebarScope, type ScopeControls } from "./scope-state.js";
20
31
  import { filterTree } from "../sidebar-filter/index.js";
21
32
  import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
22
- import { CURRENT_PATH_DATASET_KEY, readCurrentPath } from "../current-path/index.js";
33
+ import {
34
+ CURRENT_PATH_DATASET_KEY,
35
+ readCurrentPath,
36
+ } from "../current-path/index.js";
23
37
  import { ensureSidebarScrollPreserve } from "./sidebar-scroll-preserve.js";
24
38
  import {
25
39
  formatYearLabel,
@@ -31,25 +45,31 @@ import {
31
45
  } from "../note-tray-model/index.js";
32
46
  import { formatMonthDay } from "../format-date/index.js";
33
47
 
34
- // The persisted aside can transiently tear down and re-mount its SidebarTree
35
- // effect during a body swap. Keep navigation snapshot ownership at browser
36
- // module/document lifetime so that island lifecycle cannot discard it between
37
- // before-preparation and after-swap. SSR evaluation is a safe no-op.
38
- ensureSidebarScrollPreserve();
48
+ const STORAGE_KEY = "zd-sidebar-open";
49
+ const GROUPED_TRAY_ITEM_DEPTH = 3;
39
50
 
40
- function ToggleChevron({ isExpanded, className }: { isExpanded: boolean; className?: string }) {
51
+ function ToggleChevron({
52
+ isExpanded,
53
+ className,
54
+ }: {
55
+ isExpanded: ReadonlySignal<boolean>;
56
+ className?: ReadonlySignal<string> | string;
57
+ }) {
41
58
  return (
42
- <ChevronRight
43
- className={`h-[0.625rem] w-[0.625rem] shrink-0 transition-transform duration-150 ${isExpanded ? "rotate-90" : ""} ${className ?? ""}`}
44
- />
59
+ <For
60
+ each={computed(() => [
61
+ `h-[0.625rem] w-[0.625rem] shrink-0 transition-transform duration-150 ${isExpanded.value ? "rotate-90" : ""} ${typeof className === "string" ? className : (className?.value ?? "")}`,
62
+ ])}
63
+ by={(value) => value}
64
+ >
65
+ {(value) => <ChevronRight class={value.value} />}
66
+ </For>
45
67
  );
46
68
  }
47
69
 
48
- const STORAGE_KEY = "zd-sidebar-open";
49
- const GROUPED_TRAY_ITEM_DEPTH = 3;
50
-
51
70
  function padLeft(depth: number, forCategory: boolean): string {
52
- if (depth === 0) return `calc(${BASE_PAD} + ${forCategory ? "0.15rem" : "0rem"})`;
71
+ if (depth === 0)
72
+ return `calc(${BASE_PAD} + ${forCategory ? "0.15rem" : "0rem"})`;
53
73
  return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
54
74
  }
55
75
 
@@ -58,349 +78,450 @@ function getOpenSet(): Set<string> {
58
78
  const raw = sessionStorage.getItem(STORAGE_KEY);
59
79
  if (!raw) return new Set();
60
80
  const parsed: unknown = JSON.parse(raw);
61
- return Array.isArray(parsed) ? new Set(parsed.filter((v): v is string => typeof v === "string")) : new Set();
81
+ return Array.isArray(parsed)
82
+ ? new Set(
83
+ parsed.filter((value): value is string => typeof value === "string"),
84
+ )
85
+ : new Set();
62
86
  } catch {
63
87
  return new Set();
64
88
  }
65
89
  }
66
90
 
67
- function saveOpenSet(set: Set<string>) {
91
+ function saveOpenSet(set: Set<string>): void {
68
92
  try {
69
93
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify([...set]));
70
94
  } catch {
71
- // ignore
95
+ /* Storage can be unavailable. */
72
96
  }
73
97
  }
74
98
 
75
- /**
76
- * Derive the active slug from an explicit current-route input. Resolution
77
- * order is owned by {@link readCurrentPath}: the `pathname` argument, then the
78
- * `data-zd-current-path` override, then `window.location.pathname`. Used as a
79
- * hydration-time fallback when the parent island does not forward
80
- * `currentSlug` through its prop boundary, and at every View Transition to
81
- * keep the highlight in sync.
82
- *
83
- * Returns `undefined` both when no pathname is resolvable AND when the
84
- * resolved pathname matches no route (e.g. the literal "srcdoc") — callers
85
- * must treat `undefined` as "no update," preserving whatever active slug is
86
- * already set rather than clearing it.
87
- */
88
- function deriveActiveSlug(nodes: SidebarNavNode[], pathname?: string): string | undefined {
89
- // An empty-string `pathname` must fall through to the live sources instead
90
- // of silently disabling derivation — SidebarWithDefaults defaults its own
91
- // currentPath to "", so "" is the natural absent shape.
99
+ function deriveActiveSlug(
100
+ nodes: SidebarNavNode[],
101
+ pathname?: string,
102
+ ): string | undefined {
92
103
  const resolved = readCurrentPath(CURRENT_PATH_DATASET_KEY, pathname);
93
- if (!resolved) return undefined;
94
- return findActiveSlug(nodes, normalizePath(resolved));
104
+ return resolved ? findActiveSlug(nodes, normalizePath(resolved)) : undefined;
95
105
  }
96
106
 
97
- /**
98
- * Track the current active slug, updating on View Transition navigations.
99
- *
100
- * The initial-state initialiser prefers the SSR-supplied `initial` prop, but
101
- * falls back to `deriveActiveSlug` (explicit `currentPath` input, then the
102
- * override/location fallbacks) when the prop is missing.
103
- */
104
- function useActiveSlug(nodes: SidebarNavNode[], initial?: string, currentPath?: string): string | undefined {
105
- const [slug, setSlug] = useState<string | undefined>(() =>
106
- initial !== undefined ? initial : deriveActiveSlug(nodes, currentPath),
107
- );
108
-
109
- useEffect(() => {
107
+ function useActiveSlug(
108
+ nodes: SidebarNavNode[],
109
+ initial?: string,
110
+ currentPath?: string,
111
+ ) {
112
+ const slug = signal<string | undefined>(initial);
113
+ getScope().onActivate(() => {
110
114
  const update = (pathname?: string) => {
111
115
  const found = deriveActiveSlug(nodes, pathname);
112
- if (found !== undefined) setSlug(found);
116
+ if (found !== undefined) slug.value = found;
113
117
  };
114
- // The static `currentPath` prop is an SSR-serialized value for the page the
115
- // island first hydrated on — valid at hydration time only. Post-navigation
116
- // updates must re-read the LIVE sources (dataset override, then
117
- // location.pathname); recomputing from the frozen prop would pin the
118
- // highlight to the first-loaded page across client-router navigations.
119
- const onNavigate = () => update();
120
118
  update(currentPath);
119
+ const onNavigate = () => update();
121
120
  document.addEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
122
121
  return () => document.removeEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
123
- }, [nodes, currentPath]);
124
-
122
+ });
125
123
  return slug;
126
124
  }
127
125
 
128
- function RootMenuItemEntry({ item }: { item: SidebarRootMenuItem }) {
129
- const [expanded, setExpanded] = useState(false);
130
- const hasChildren = item.children && item.children.length > 0;
131
-
126
+ function RootMenuItemEntry({
127
+ item,
128
+ }: {
129
+ item: ReadonlySignal<SidebarRootMenuItem>;
130
+ }) {
131
+ const expanded = signal(false);
132
+ const hasChildren = computed(() => !!item.value.children?.length);
132
133
  return (
133
- <div className="border-t border-muted">
134
- <div className="flex items-center">
134
+ <div class="border-t border-muted">
135
+ <div class="flex items-center">
135
136
  <a
136
- href={item.href}
137
- className="flex flex-1 items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-small font-semibold text-fg hover:text-accent hover:underline break-words"
137
+ href={computed(() => item.value.href ?? "")}
138
+ class="flex flex-1 items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-small font-semibold text-fg hover:text-accent hover:underline break-words"
138
139
  >
139
- <CategoryLinkIcon className="w-[14px]" />
140
- <span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(item.label) }} />
140
+ <CategoryLinkIcon class="w-[14px]" />
141
+ <span rawHtml={computed(() => smartBreakToHtml(item.value.label))} />
141
142
  </a>
142
- {hasChildren && (
143
- <button
144
- type="button"
145
- onClick={() => setExpanded((prev) => !prev)}
146
- className="flex items-center justify-center px-hsp-sm py-vsp-xs text-muted hover:text-fg"
147
- aria-expanded={expanded}
148
- aria-label={expanded ? `Collapse ${item.label}` : `Expand ${item.label}`}
149
- >
150
- <ToggleChevron isExpanded={expanded} className="text-muted" />
151
- </button>
152
- )}
143
+ <Show when={hasChildren}>
144
+ {() => (
145
+ <button
146
+ type="button"
147
+ on:click={() => {
148
+ expanded.value = !expanded.value;
149
+ }}
150
+ class="flex items-center justify-center px-hsp-sm py-vsp-xs text-muted hover:text-fg"
151
+ aria-expanded={computed(() =>
152
+ expanded.value ? "true" : "false",
153
+ )}
154
+ aria-label={computed(
155
+ () =>
156
+ `${expanded.value ? "Collapse" : "Expand"} ${item.value.label}`,
157
+ )}
158
+ >
159
+ <ToggleChevron isExpanded={expanded} className="text-muted" />
160
+ </button>
161
+ )}
162
+ </Show>
153
163
  </div>
154
- {hasChildren && expanded && (
155
- <div className="pb-vsp-xs">
156
- {item.children!.map((child) => (
157
- <a
158
- key={child.href}
159
- href={child.href}
160
- className="block pl-hsp-xl pr-hsp-sm py-vsp-2xs text-small text-muted hover:text-accent hover:underline break-words"
164
+ <Show when={computed(() => hasChildren.value && expanded.value)}>
165
+ {() => (
166
+ <div class="pb-vsp-xs">
167
+ <For
168
+ each={computed(() => item.value.children ?? [])}
169
+ by={(child) => child.href}
161
170
  >
162
- <span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(child.label) }} />
163
- </a>
164
- ))}
165
- </div>
166
- )}
171
+ {(child) => (
172
+ <a
173
+ href={computed(() => child.value.href)}
174
+ class="block pl-hsp-xl pr-hsp-sm py-vsp-2xs text-small text-muted hover:text-accent hover:underline break-words"
175
+ >
176
+ <span
177
+ rawHtml={computed(() =>
178
+ smartBreakToHtml(child.value.label),
179
+ )}
180
+ />
181
+ </a>
182
+ )}
183
+ </For>
184
+ </div>
185
+ )}
186
+ </Show>
167
187
  </div>
168
188
  );
169
189
  }
170
190
 
171
191
  export interface SidebarTreeProps {
172
192
  nodes: SidebarNavNode[];
193
+ navigation?: SidebarNavigationContext;
173
194
  currentSlug?: string;
174
- /**
175
- * Explicit current-route override, checked before the
176
- * `data-zd-current-path` dataset override and `window.location.pathname`
177
- * when deriving the active slug on hydration and at every View Transition.
178
- * See `deriveActiveSlug` (zudolab/zudo-doc#3398).
179
- */
195
+ /** Route override used on activation before dataset and location. */
180
196
  currentPath?: string;
181
197
  rootMenuItems?: SidebarRootMenuItem[];
182
198
  backToMenuLabel?: string;
183
- /** Display locale; falls back to the active locale link, then English. */
184
199
  locale?: string;
185
200
  localeLinks?: SidebarLocaleLink[];
186
201
  themeDefaultMode?: "light" | "dark";
187
202
  themeLabels?: ThemeToggleLabels;
188
203
  themeRespectSystem?: boolean;
189
- /**
190
- * Per-role date patterns already resolved for this page's locale, serialized
191
- * into the island's `data-props` by the SSR wrappers (`sidebar-with-defaults`
192
- * for the desktop sidebar, `header-with-defaults` -> `SidebarToggle` for the
193
- * mobile drawer). Optional and absent-safe: omitted means every role behaves
194
- * as `"locale"` — today's `Intl` output (#4075).
195
- *
196
- * The patterns are resolved against the real page locale at SSR,
197
- * so they stay correct on a single-locale site where `localeLinks` is empty.
198
- */
199
204
  dateFormats?: ResolvedDateFormats;
200
205
  }
201
206
 
202
- function SidebarFooter({ links, themeDefaultMode, themeLabels, themeRespectSystem }: { links?: SidebarLocaleLink[]; themeDefaultMode?: "light" | "dark"; themeLabels?: ThemeToggleLabels; themeRespectSystem?: boolean }) {
207
+ function SidebarFooter({
208
+ links,
209
+ themeDefaultMode,
210
+ themeLabels,
211
+ themeRespectSystem,
212
+ }: {
213
+ links?: SidebarLocaleLink[];
214
+ themeDefaultMode?: "light" | "dark";
215
+ themeLabels?: ThemeToggleLabels;
216
+ themeRespectSystem?: boolean;
217
+ }) {
203
218
  if (!links && !themeDefaultMode) return null;
204
219
  return (
205
- // pb-[50vh] provides scroll room so the footer doesn't sit at the very bottom of the viewport
206
- <div className="lg:hidden flex items-center gap-hsp-md border-t border-muted px-hsp-sm py-vsp-xs pb-[50vh] text-small">
207
- {themeDefaultMode && (
208
- <ThemeToggle defaultMode={themeDefaultMode} labels={themeLabels} respectPrefersColorScheme={themeRespectSystem} pendingUntilHydrated={true} />
209
- )}
210
- {links && links.map((link, i) => (
211
- <span key={link.href} className="flex items-center gap-hsp-xs">
212
- {i > 0 && <span className="text-muted">/</span>}
213
- {link.active ? (
214
- <span aria-current="true" className="font-medium text-fg">{link.label}</span>
215
- ) : (
216
- <a href={link.href} lang={link.code} className="text-muted hover:text-fg">
217
- {link.label}
218
- </a>
219
- )}
220
- </span>
221
- ))}
220
+ <div class="lg:hidden flex items-center gap-hsp-md border-t border-muted px-hsp-sm py-vsp-xs pb-[50vh] text-small">
221
+ {themeDefaultMode ? (
222
+ <ThemeToggle
223
+ defaultMode={themeDefaultMode}
224
+ labels={themeLabels}
225
+ respectPrefersColorScheme={themeRespectSystem}
226
+ pendingUntilHydrated={true}
227
+ />
228
+ ) : null}
229
+ <For each={computed(() => links ?? [])} by={(link) => link.href}>
230
+ {(link, index) => (
231
+ <span class="flex items-center gap-hsp-xs">
232
+ <Show when={computed(() => index.value > 0)}>
233
+ {() => <span class="text-muted">/</span>}
234
+ </Show>
235
+ <Show
236
+ when={computed(() => link.value.active)}
237
+ fallback={() => (
238
+ <a
239
+ href={computed(() => link.value.href)}
240
+ lang={computed(() => link.value.code)}
241
+ class="text-muted hover:text-fg"
242
+ >
243
+ {computed(() => link.value.label)}
244
+ </a>
245
+ )}
246
+ >
247
+ {() => (
248
+ <span aria-current="true" class="font-medium text-fg">
249
+ {computed(() => link.value.label)}
250
+ </span>
251
+ )}
252
+ </Show>
253
+ </span>
254
+ )}
255
+ </For>
222
256
  </div>
223
257
  );
224
258
  }
225
259
 
226
- export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMenuLabel, locale: localeProp, localeLinks, themeDefaultMode, themeLabels, themeRespectSystem, dateFormats }: SidebarTreeProps) {
227
- const activeSlug = useActiveSlug(nodes, currentSlug, currentPath);
228
- const [query, setQuery] = useState("");
229
- const [showingRootMenu, setShowingRootMenu] = useState(false);
230
- const filterRef = useRef<HTMLInputElement>(null);
231
- const [filterPlaceholder, setFilterPlaceholder] = useState("Filter...");
232
-
233
- // Detect OS to show appropriate keyboard shortcut in placeholder
234
- useEffect(() => {
235
- const platform = (navigator as { userAgentData?: { platform: string } }).userAgentData?.platform ?? navigator.platform;
236
- const isMac = /mac/i.test(platform);
237
- setFilterPlaceholder(isMac ? "Filter... (⌘ + /)" : "Filter... (Ctrl + /)");
238
- }, []);
239
-
240
- // Global shortcut: Cmd+/ (Mac) or Ctrl+/ to focus the filter input
241
- useEffect(() => {
242
- function handleKeyDown(e: KeyboardEvent) {
243
- if (e.isComposing) return;
244
- if (e.key === "/" && (e.metaKey || e.ctrlKey)) {
245
- const el = filterRef.current;
246
- if (!el || el.offsetParent === null) return; // skip if hidden
247
- e.preventDefault();
248
- el.focus();
249
- el.select();
250
- }
251
- }
260
+ export function SidebarTree({
261
+ nodes,
262
+ navigation,
263
+ currentSlug,
264
+ currentPath,
265
+ rootMenuItems,
266
+ backToMenuLabel,
267
+ locale: localeProp,
268
+ localeLinks,
269
+ themeDefaultMode,
270
+ themeLabels,
271
+ themeRespectSystem,
272
+ dateFormats,
273
+ }: SidebarTreeProps) {
274
+ const scope = getScope();
275
+ const activeSlug = useActiveSlug(navigation?.roots ?? nodes, currentSlug, currentPath);
276
+ const query = signal("");
277
+ const { selected, selectedNodes, broader, labels, canRestore, changeScope, save, navRef, scopeControls } = useSidebarScope({
278
+ nodes, navigation, activeSlug, query,
279
+ locale: localeProp ?? localeLinks?.find((link) => link.active)?.code ?? "en",
280
+ });
281
+ const showingRootMenu = signal(false);
282
+ const filterRef: Ref<HTMLInputElement> = { current: null };
283
+ const filterPlaceholder = signal("Filter...");
284
+ scope.onActivate(() => {
285
+ ensureSidebarScrollPreserve();
286
+ const platform =
287
+ (navigator as Navigator & { userAgentData?: { platform: string } })
288
+ .userAgentData?.platform ?? navigator.platform;
289
+ filterPlaceholder.value = /mac/i.test(platform)
290
+ ? "Filter... (⌘ + /)"
291
+ : "Filter... (Ctrl + /)";
292
+ const handleKeyDown = (event: Event) => {
293
+ const keyEvent = event as KeyboardEvent;
294
+ if (
295
+ keyEvent.isComposing ||
296
+ keyEvent.key !== "/" ||
297
+ !(keyEvent.metaKey || keyEvent.ctrlKey)
298
+ )
299
+ return;
300
+ const input = filterRef.current;
301
+ if (!input || input.offsetParent === null) return;
302
+ event.preventDefault();
303
+ input.focus();
304
+ input.select();
305
+ };
252
306
  document.addEventListener("keydown", handleKeyDown);
253
307
  return () => document.removeEventListener("keydown", handleKeyDown);
254
- }, []);
255
-
256
- const filteredNodes = useMemo(
257
- () => (query ? filterTree(nodes, query) : nodes),
258
- [nodes, query],
308
+ });
309
+ const filteredNodes = computed(() =>
310
+ query.value ? filterTree(selectedNodes.value, query.value) : selectedNodes.value,
259
311
  );
260
-
261
- const footer = useMemo(
262
- () => (localeLinks || themeDefaultMode) ? <SidebarFooter links={localeLinks} themeDefaultMode={themeDefaultMode} themeLabels={themeLabels} themeRespectSystem={themeRespectSystem} /> : null,
263
- [localeLinks, themeDefaultMode, themeLabels, themeRespectSystem],
312
+ const locale =
313
+ localeProp ?? localeLinks?.find((link) => link.active)?.code ?? "en";
314
+ const footer = (
315
+ <SidebarFooter
316
+ links={localeLinks}
317
+ themeDefaultMode={themeDefaultMode}
318
+ themeLabels={themeLabels}
319
+ themeRespectSystem={themeRespectSystem}
320
+ />
321
+ );
322
+ const menu = (
323
+ <For each={computed(() => rootMenuItems ?? [])} by={(item) => item.href}>
324
+ {(item) => <RootMenuItemEntry item={item} />}
325
+ </For>
264
326
  );
265
-
266
- const noteTrayRoot = nodes.length === 1 && nodes[0]?.shape === "note-tray" ? nodes[0] : undefined;
267
- const filteredNoteTrayRoot = noteTrayRoot
268
- ? filteredNodes.find((node) => node.slug === noteTrayRoot.slug)
269
- : undefined;
270
- const locale = localeProp ?? localeLinks?.find((link) => link.active)?.code ?? "en";
271
-
272
- // Root menu view: show headerNav items as a simple list (Docusaurus-style)
273
- if (showingRootMenu && rootMenuItems) {
274
- return (
275
- <nav>
276
- <button
277
- type="button"
278
- onClick={() => setShowingRootMenu(false)}
279
- className="flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
280
- >
281
- <ChevronRight className="h-icon-sm w-icon-sm shrink-0" />
282
- {backToMenuLabel ?? "Back to main menu"}
283
- </button>
284
- {rootMenuItems.map((item) => (
285
- <RootMenuItemEntry key={item.href} item={item} />
286
- ))}
287
- {footer}
288
- </nav>
289
- );
290
- }
291
-
292
- // Top page: show only header nav links, no doc tree or filter.
293
- if (activeSlug === undefined && rootMenuItems) {
294
- return (
295
- <nav>
296
- {rootMenuItems.map((item) => (
297
- <RootMenuItemEntry key={item.href} item={item} />
298
- ))}
299
- {footer}
300
- </nav>
301
- );
302
- }
303
-
304
327
  return (
305
- <nav>
306
- {rootMenuItems && (
307
- <button
308
- type="button"
309
- onClick={() => setShowingRootMenu(true)}
310
- className="lg:hidden flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
328
+ <Show
329
+ when={computed(() => showingRootMenu.value && !!rootMenuItems)}
330
+ fallback={() => (
331
+ <Show
332
+ when={computed(
333
+ () => activeSlug.value === undefined && !!rootMenuItems,
334
+ )}
335
+ fallback={() => (
336
+ <nav ref={navRef} tabindex={-1}>
337
+ {rootMenuItems ? (
338
+ <button
339
+ type="button"
340
+ on:click={() => {
341
+ showingRootMenu.value = true;
342
+ }}
343
+ class="lg:hidden flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
344
+ >
345
+ <ChevronLeft class="h-icon-sm w-icon-sm shrink-0" />
346
+ {backToMenuLabel ?? "Back to main menu"}
347
+ </button>
348
+ ) : null}
349
+ <div class="px-hsp-sm py-vsp-xs border-b border-muted" data-sidebar-filter-region>
350
+ <div class="flex items-center gap-hsp-xs bg-surface rounded px-hsp-sm py-vsp-2xs">
351
+ <Search class="h-[14px] w-[14px] text-muted shrink-0" />
352
+ <input
353
+ ref={filterRef}
354
+ type="text"
355
+ aria-label="Filter navigation"
356
+ placeholder={filterPlaceholder}
357
+ modelValue={query}
358
+ on:input={(event: Event) => { query.value = (event.target as HTMLInputElement).value; save(); }}
359
+ class="bg-transparent text-small outline-none w-full text-fg placeholder:text-muted focus-visible:outline-2 focus-visible:outline-accent" style="outline-offset:-4px"
360
+ />
361
+ </div>
362
+ </div>
363
+ {navigation ? (
364
+ <Show when={computed(() => broader.value !== null)}>{() => (
365
+ <div class="border-b border-muted px-hsp-md py-vsp-2xs text-small" data-sidebar-scope-toolbar>
366
+ <div class="flex items-center justify-between gap-hsp-xs" data-sidebar-scope-actions>
367
+ <button type="button" data-sidebar-broaden disabled={computed(() => broader.value === null)}
368
+ on:click={() => { if (broader.value !== null) changeScope(broader.value); }}
369
+ style="min-height:40px"
370
+ class="flex items-center gap-hsp-sm px-hsp-xs font-medium rounded text-fg hover:bg-surface focus-visible:bg-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:text-muted disabled:opacity-50">
371
+ <span aria-hidden="true">↑</span>{labels.broaden}
372
+ </button>
373
+ <Show when={canRestore}>{() => (
374
+ <button type="button" data-sidebar-restore on:click={() => changeScope(null)}
375
+ title={labels.restore} aria-label={labels.restore}
376
+ style="width:40px;height:40px"
377
+ class="grid place-items-center shrink-0 rounded text-muted hover:text-fg focus-visible:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">
378
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
379
+ <circle cx="12" cy="12" r="10" />
380
+ <circle cx="12" cy="12" r="6" />
381
+ <circle cx="12" cy="12" r="2" />
382
+ </svg>
383
+ </button>
384
+ )}</Show>
385
+ </div>
386
+ </div>
387
+ )}</Show>
388
+ ) : null}
389
+ <div data-sidebar-tree-root>
390
+ <Show when={computed(() => selectedNodes.value.length === 1 && selectedNodes.value[0]?.shape === "note-tray")}
391
+ fallback={() => <NodeList nodes={filteredNodes} currentSlug={activeSlug} depth={0} forceOpen={computed(() => !!query.value)} scopeControls={scopeControls} />}>
392
+ {() => <Show when={computed(() => !!filteredNodes.value[0])}>{() => (
393
+ <TrayList tray={computed(() => filteredNodes.value[0]!)} itemCount={getNoteTrayItems(selectedNodes.value[0]!).length}
394
+ currentSlug={activeSlug} forceOpen={computed(() => !!query.value)} locale={locale} dateFormats={dateFormats} scopeControls={scopeControls} />
395
+ )}</Show>}
396
+ </Show>
397
+ </div>
398
+ {footer}
399
+ </nav>
400
+ )}
311
401
  >
312
- <ChevronLeft className="h-icon-sm w-icon-sm shrink-0" />
313
- {backToMenuLabel ?? "Back to main menu"}
314
- </button>
402
+ {() => (
403
+ <nav>
404
+ {menu}
405
+ {footer}
406
+ </nav>
407
+ )}
408
+ </Show>
315
409
  )}
316
- <div className="px-hsp-sm py-vsp-xs">
317
- <div className="flex items-center gap-hsp-xs bg-surface rounded px-hsp-sm py-vsp-2xs">
318
- <Search className="h-[14px] w-[14px] text-muted shrink-0" />
319
- <input
320
- ref={filterRef}
321
- type="text"
322
- aria-label="Filter navigation"
323
- placeholder={filterPlaceholder}
324
- value={query}
325
- onInput={(e) => setQuery(e.currentTarget.value)}
326
- className="bg-transparent text-small outline-none w-full text-fg placeholder:text-muted"
327
- />
328
- </div>
329
- </div>
330
- {noteTrayRoot ? (
331
- filteredNoteTrayRoot && (
332
- <TrayList
333
- tray={filteredNoteTrayRoot}
334
- itemCount={getNoteTrayItems(noteTrayRoot).length}
335
- currentSlug={activeSlug}
336
- forceOpen={!!query}
337
- locale={locale}
338
- dateFormats={dateFormats}
339
- />
340
- )
341
- ) : (
342
- <NodeList
343
- nodes={filteredNodes}
344
- currentSlug={activeSlug}
345
- depth={0}
346
- forceOpen={!!query}
347
- />
410
+ >
411
+ {() => (
412
+ <nav>
413
+ <button
414
+ type="button"
415
+ on:click={() => {
416
+ showingRootMenu.value = false;
417
+ }}
418
+ class="flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
419
+ >
420
+ <ChevronRight class="h-icon-sm w-icon-sm shrink-0" />
421
+ {backToMenuLabel ?? "Back to main menu"}
422
+ </button>
423
+ {menu}
424
+ {footer}
425
+ </nav>
348
426
  )}
349
- {footer}
350
- </nav>
427
+ </Show>
351
428
  );
352
429
  }
353
430
  SidebarTree.displayName = "SidebarTree";
354
431
 
432
+ interface ActiveProps {
433
+ scopeControls?: ScopeControls;
434
+ currentSlug: ReadonlySignal<string | undefined>;
435
+ forceOpen: ReadonlySignal<boolean>;
436
+ }
437
+ interface RowProps extends ActiveProps {
438
+ node: ReadonlySignal<SidebarNavNode>;
439
+ depth: number;
440
+ isLast: ReadonlySignal<boolean>;
441
+ }
442
+
355
443
  function TrayList({
356
444
  tray,
357
445
  itemCount,
358
446
  currentSlug,
359
447
  forceOpen,
448
+ scopeControls,
360
449
  locale,
361
450
  dateFormats,
362
451
  }: {
363
- tray: SidebarNavNode;
452
+ tray: ReadonlySignal<SidebarNavNode>;
364
453
  itemCount: number;
365
- currentSlug?: string;
366
- forceOpen: boolean;
367
454
  locale: string;
368
455
  dateFormats?: ResolvedDateFormats;
369
- }) {
370
- const items = getNoteTrayItems(tray);
371
- const sidebarStyle = tray.noteTraySidebar ?? "index";
456
+ } & ActiveProps) {
457
+ const items = computed(() => getNoteTrayItems(tray.value));
458
+ const sidebarStyle = computed(() => tray.value.noteTraySidebar ?? "index");
372
459
  const width = rankWidth(itemCount);
373
-
374
460
  return (
375
461
  <>
376
- <LeafNode node={tray} currentSlug={currentSlug} depth={0} isLast={items.length === 0} />
377
- {sidebarStyle === "index" ? (
378
- items.map((item, index) => (
379
- <TrayItem
380
- key={item.slug}
381
- item={item}
382
- currentSlug={currentSlug}
383
- rankDigits={width}
384
- isLast={index === items.length - 1}
385
- locale={locale}
386
- dateFormats={dateFormats}
387
- />
388
- ))
389
- ) : (
390
- groupItems(items, sidebarStyle, tray.sortOrder ?? "asc").map((group, index, groups) => (
391
- <TrayGroupNode
392
- key={group.key}
393
- traySlug={tray.slug}
394
- group={group}
395
- grouping={sidebarStyle}
396
- locale={locale}
397
- dateFormats={dateFormats}
398
- currentSlug={currentSlug}
399
- forceOpen={forceOpen}
400
- isLast={index === groups.length - 1}
401
- />
402
- ))
403
- )}
462
+ <LeafNode
463
+ node={tray}
464
+ currentSlug={currentSlug}
465
+ depth={0}
466
+ isLast={computed(() => items.value.length === 0)}
467
+ forceOpen={forceOpen}
468
+ />
469
+ <Show
470
+ when={computed(() => sidebarStyle.value === "index")}
471
+ fallback={() => (
472
+ <For
473
+ each={computed(() =>
474
+ groupItems(
475
+ items.value,
476
+ sidebarStyle.value as "year" | "month",
477
+ tray.value.sortOrder ?? "asc",
478
+ ),
479
+ )}
480
+ by={(group) => group.key}
481
+ >
482
+ {(group, index) => (
483
+ <TrayGroupNode
484
+ traySlug={computed(() => tray.value.slug)}
485
+ trayOccurrence={computed(() => tray.value.occurrenceId ?? tray.value.slug)}
486
+ scopeControls={scopeControls}
487
+ group={group}
488
+ grouping={computed(
489
+ () => sidebarStyle.value as "year" | "month",
490
+ )}
491
+ locale={locale}
492
+ dateFormats={dateFormats}
493
+ currentSlug={currentSlug}
494
+ forceOpen={forceOpen}
495
+ isLast={computed(
496
+ () =>
497
+ index.value ===
498
+ groupItems(
499
+ items.value,
500
+ sidebarStyle.value as "year" | "month",
501
+ tray.value.sortOrder ?? "asc",
502
+ ).length -
503
+ 1,
504
+ )}
505
+ />
506
+ )}
507
+ </For>
508
+ )}
509
+ >
510
+ {() => (
511
+ <For each={items} by={(item) => item.slug}>
512
+ {(item, index) => (
513
+ <TrayItem
514
+ item={item}
515
+ currentSlug={currentSlug}
516
+ rankDigits={width}
517
+ isLast={computed(() => index.value === items.value.length - 1)}
518
+ locale={locale}
519
+ dateFormats={dateFormats}
520
+ />
521
+ )}
522
+ </For>
523
+ )}
524
+ </Show>
404
525
  </>
405
526
  );
406
527
  }
@@ -415,53 +536,97 @@ function TrayItem({
415
536
  locale,
416
537
  dateFormats,
417
538
  }: {
418
- item: SidebarNavNode;
419
- currentSlug?: string;
539
+ item: ReadonlySignal<SidebarNavNode>;
540
+ currentSlug: ReadonlySignal<string | undefined>;
420
541
  rankDigits?: number;
421
- isLast: boolean;
542
+ isLast: ReadonlySignal<boolean>;
422
543
  showDate?: boolean;
423
544
  depth?: number;
424
545
  locale?: string;
425
546
  dateFormats?: ResolvedDateFormats;
426
547
  }) {
427
- if (!item.href) return null;
428
- const isActive = item.slug === currentSlug;
429
- const labelHtml = smartBreakToHtml(item.label);
430
- // formatMonthDay is (iso, pattern, locale) — pattern SECOND, unlike every
431
- // other format-date entry point; a locale in slot 2 is read as a pattern.
432
- const shortDate = item.date
433
- ? formatMonthDay(item.date, dateFormats?.numericMonthDay, locale)
434
- : undefined;
548
+ const active = computed(() => item.value.slug === currentSlug.value);
549
+ const shortDate = computed(() =>
550
+ item.value.date
551
+ ? formatMonthDay(item.value.date, dateFormats?.numericMonthDay, locale)
552
+ : undefined,
553
+ );
554
+ return (
555
+ <Show when={computed(() => !!item.value.href)}>
556
+ {() => (
557
+ <div class={computed(() => (isLast.value ? "pb-vsp-md" : ""))}>
558
+ <div class="relative">
559
+ <ConnectorLinesLive
560
+ depth={depth}
561
+ isLast={isLast}
562
+ topPad="var(--spacing-vsp-2xs)"
563
+ />
564
+ <a
565
+ href={computed(() => item.value.href ?? "")}
566
+ aria-current={computed(() => (active.value ? "page" : "false"))}
567
+ data-nav-active={computed(() => (active.value ? "" : null))}
568
+ class={computed(
569
+ () =>
570
+ `flex items-start gap-hsp-xs py-vsp-2xs pr-hsp-xs lg:pr-hsp-sm text-small break-words ${active.value ? "bg-fg font-medium text-bg" : "text-muted hover:text-accent hover:underline focus:underline focus:text-accent"}`,
571
+ )}
572
+ style={{ "padding-left": padLeft(depth, false) }}
573
+ >
574
+ {rankDigits !== undefined ? (
575
+ <span
576
+ class={computed(
577
+ () =>
578
+ `shrink-0 tabular-nums${active.value ? "" : " text-muted"}`,
579
+ )}
580
+ >
581
+ {computed(() =>
582
+ item.value.rank === undefined
583
+ ? ""
584
+ : String(item.value.rank).padStart(rankDigits, "0"),
585
+ )}
586
+ </span>
587
+ ) : null}
588
+ <span
589
+ class="min-w-0 flex-1"
590
+ rawHtml={computed(() => smartBreakToHtml(item.value.label))}
591
+ />
592
+ <Show when={computed(() => showDate && !!shortDate.value)}>
593
+ {() => (
594
+ <span
595
+ class={computed(
596
+ () =>
597
+ `shrink-0 tabular-nums${active.value ? "" : " text-muted"}`,
598
+ )}
599
+ >
600
+ {shortDate}
601
+ </span>
602
+ )}
603
+ </Show>
604
+ </a>
605
+ </div>
606
+ </div>
607
+ )}
608
+ </Show>
609
+ );
610
+ }
435
611
 
612
+ function ConnectorLinesLive({
613
+ depth,
614
+ isLast,
615
+ topPad,
616
+ }: {
617
+ depth: number;
618
+ isLast: ReadonlySignal<boolean>;
619
+ topPad: string;
620
+ }) {
436
621
  return (
437
- <div className={isLast ? "pb-vsp-md" : ""}>
438
- <div className="relative">
439
- <ConnectorLines depth={depth} isLast={isLast} topPad="var(--spacing-vsp-2xs)" />
440
- <a
441
- href={item.href}
442
- aria-current={isActive ? "page" : undefined}
443
- data-nav-active={isActive ? "" : undefined}
444
- className={`flex items-start gap-hsp-xs py-vsp-2xs pr-hsp-xs lg:pr-hsp-sm text-small break-words ${
445
- isActive
446
- ? "bg-fg font-medium text-bg"
447
- : "text-muted hover:text-accent hover:underline focus:underline focus:text-accent"
448
- }`}
449
- style={{ paddingLeft: padLeft(depth, false) }}
450
- >
451
- {rankDigits !== undefined && (
452
- <span className={`shrink-0 tabular-nums${isActive ? "" : " text-muted"}`}>
453
- {item.rank === undefined ? "" : String(item.rank).padStart(rankDigits, "0")}
454
- </span>
455
- )}
456
- <span className="min-w-0 flex-1" dangerouslySetInnerHTML={{ __html: labelHtml }} />
457
- {showDate && shortDate && (
458
- <span className={`shrink-0 tabular-nums${isActive ? "" : " text-muted"}`}>
459
- {shortDate}
460
- </span>
461
- )}
462
- </a>
463
- </div>
464
- </div>
622
+ <Show
623
+ when={isLast}
624
+ fallback={() => (
625
+ <ConnectorLines depth={depth} isLast={false} topPad={topPad} />
626
+ )}
627
+ >
628
+ {() => <ConnectorLines depth={depth} isLast={true} topPad={topPad} />}
629
+ </Show>
465
630
  );
466
631
  }
467
632
 
@@ -471,6 +636,8 @@ function noteTrayGroupStorageKey(traySlug: string, groupKey: string): string {
471
636
 
472
637
  function TrayGroupNode({
473
638
  traySlug,
639
+ trayOccurrence,
640
+ scopeControls,
474
641
  group,
475
642
  grouping,
476
643
  locale,
@@ -479,350 +646,395 @@ function TrayGroupNode({
479
646
  forceOpen,
480
647
  isLast,
481
648
  }: {
482
- traySlug: string;
483
- group: NoteTrayGroup<SidebarNavNode>;
484
- grouping: "year" | "month";
649
+ traySlug: ReadonlySignal<string>;
650
+ trayOccurrence: ReadonlySignal<string>;
651
+ group: ReadonlySignal<NoteTrayGroup<SidebarNavNode>>;
652
+ grouping: ReadonlySignal<"year" | "month">;
485
653
  locale: string;
486
654
  dateFormats?: ResolvedDateFormats;
487
- currentSlug?: string;
488
- forceOpen: boolean;
489
- isLast: boolean;
490
- }) {
491
- const containsCurrent = group.items.some((item) => item.slug === currentSlug);
492
- const [open, setOpen] = useState(containsCurrent);
493
- const storageKey = noteTrayGroupStorageKey(traySlug, group.key);
494
- const label =
495
- grouping === "year"
496
- ? formatYearLabel(group.key, locale, dateFormats?.year)
497
- : formatYearMonthLabel(group.key, locale, dateFormats?.yearMonth);
498
-
499
- useEffect(() => {
500
- const stored = getOpenSet();
501
- if (stored.has(storageKey) && !open) setOpen(true);
502
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
503
-
504
- useEffect(() => {
505
- if (containsCurrent && !open) {
506
- setOpen(true);
507
- const stored = getOpenSet();
508
- stored.add(storageKey);
509
- saveOpenSet(stored);
510
- }
511
- }, [containsCurrent]); // eslint-disable-line react-hooks/exhaustive-deps
512
-
513
- useEffect(() => {
514
- if (open) {
655
+ isLast: ReadonlySignal<boolean>;
656
+ } & ActiveProps) {
657
+ const scope = getScope();
658
+ const containsCurrent = computed(() =>
659
+ group.value.items.some((item) => item.slug === currentSlug.value),
660
+ );
661
+ const occurrenceKey = computed(() => `${trayOccurrence.value}#${group.value.key}`);
662
+ const open = signal(scopeControls?.expansion.value[occurrenceKey.value] ?? (scopeControls?.defaultGroupOpen(group.value.items) ?? containsCurrent.value));
663
+ const storageKey = computed(() =>
664
+ noteTrayGroupStorageKey(traySlug.value, group.value.key),
665
+ );
666
+ const label = computed(() =>
667
+ grouping.value === "year"
668
+ ? formatYearLabel(group.value.key, locale, dateFormats?.year)
669
+ : formatYearMonthLabel(group.value.key, locale, dateFormats?.yearMonth),
670
+ );
671
+ scope.onActivate(() => {
672
+ if (!scopeControls && getOpenSet().has(storageKey.value)) open.value = true;
673
+ });
674
+ scope.effect(() => {
675
+ if (containsCurrent.value) open.value = true;
676
+ });
677
+ scope.effect(() => {
678
+ if (!scopeControls && open.value) {
515
679
  const stored = getOpenSet();
516
- if (!stored.has(storageKey)) {
517
- stored.add(storageKey);
680
+ if (!stored.has(storageKey.value)) {
681
+ stored.add(storageKey.value);
518
682
  saveOpenSet(stored);
519
683
  }
520
684
  }
521
- }, [open, storageKey]);
522
-
523
- const toggle = useCallback(() => {
524
- setOpen((previous) => {
525
- const next = !previous;
526
- const stored = getOpenSet();
527
- if (next) stored.add(storageKey);
528
- else stored.delete(storageKey);
529
- saveOpenSet(stored);
530
- return next;
531
- });
532
- }, [storageKey]);
533
-
534
- const isExpanded = forceOpen || open;
535
-
685
+ });
686
+ const toggle = () => {
687
+ open.value = !open.value;
688
+ if (scopeControls) {
689
+ scopeControls.expansion.value = { ...scopeControls.expansion.value, [occurrenceKey.value]: open.value };
690
+ scopeControls.save();
691
+ return;
692
+ }
693
+ const stored = getOpenSet();
694
+ if (open.value) stored.add(storageKey.value);
695
+ else stored.delete(storageKey.value);
696
+ saveOpenSet(stored);
697
+ };
698
+ scope.effect(() => {
699
+ const stored = scopeControls?.expansion.value[occurrenceKey.value];
700
+ if (scopeControls) open.value = stored ?? scopeControls.defaultGroupOpen(group.value.items);
701
+ });
702
+ const expanded = computed(() => forceOpen.value || open.value);
703
+ const items = computed(() => group.value.items);
536
704
  return (
537
- <div className={!isLast && isExpanded ? "relative" : ""}>
538
- {!isLast && isExpanded && (
539
- <div
540
- className="absolute border-l border-solid border-muted z-local-1"
541
- style={{ left: connectorLeft(1), top: 0, bottom: 0 }}
542
- />
705
+ <div
706
+ class={computed(() =>
707
+ !isLast.value && expanded.value ? "relative" : "",
543
708
  )}
544
- <div className="relative">
545
- <ConnectorLines depth={1} isLast={isLast} topPad="var(--spacing-vsp-xs)" />
709
+ >
710
+ <Show when={computed(() => !isLast.value && expanded.value)}>
711
+ {() => (
712
+ <div
713
+ class="absolute border-l border-solid border-muted z-local-1"
714
+ style={{ left: connectorLeft(1), top: "0px", bottom: "0px" }}
715
+ />
716
+ )}
717
+ </Show>
718
+ <div class="relative">
719
+ <ConnectorLinesLive
720
+ depth={1}
721
+ isLast={isLast}
722
+ topPad="var(--spacing-vsp-xs)"
723
+ />
546
724
  <button
547
725
  type="button"
548
- onClick={toggle}
549
- className="flex w-full items-center gap-hsp-md py-vsp-xs text-left text-small font-semibold text-fg hover:text-accent hover:underline focus:underline focus:text-accent break-words"
550
- style={{ paddingLeft: padLeft(1, true) }}
551
- aria-expanded={isExpanded}
552
- aria-label={isExpanded ? `Collapse ${label}` : `Expand ${label}`}
726
+ on:click={toggle}
727
+ class="flex w-full items-center gap-hsp-md py-vsp-xs text-left text-small font-semibold text-fg hover:text-accent hover:underline focus:underline focus:text-accent break-words"
728
+ style={{ "padding-left": padLeft(1, true) }}
729
+ aria-expanded={computed(() => (expanded.value ? "true" : "false"))}
730
+ aria-label={computed(
731
+ () => `${expanded.value ? "Collapse" : "Expand"} ${label.value}`,
732
+ )}
553
733
  data-zd-sidebar-open-key={storageKey}
554
734
  >
555
- <span className="aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted">
556
- <ToggleChevron isExpanded={isExpanded} className="text-muted" />
735
+ <span class="aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted">
736
+ <ToggleChevron isExpanded={expanded} className="text-muted" />
557
737
  </span>
558
738
  <span>{label}</span>
559
739
  </button>
560
740
  </div>
561
- {isExpanded && (
562
- <div>
563
- {group.items.map((item, index) => (
564
- <TrayItem
565
- key={item.slug}
566
- item={item}
567
- currentSlug={currentSlug}
568
- isLast={index === group.items.length - 1}
569
- showDate
570
- locale={locale}
571
- dateFormats={dateFormats}
572
- // Leave one visual indentation step between the group branch
573
- // and its dated child branch so the hierarchy reads clearly.
574
- depth={GROUPED_TRAY_ITEM_DEPTH}
575
- />
576
- ))}
577
- </div>
578
- )}
741
+ <Show when={expanded}>
742
+ {() => (
743
+ <div>
744
+ <For each={items} by={(item) => item.slug}>
745
+ {(item, index) => (
746
+ <TrayItem
747
+ item={item}
748
+ currentSlug={currentSlug}
749
+ isLast={computed(
750
+ () => index.value === items.value.length - 1,
751
+ )}
752
+ showDate
753
+ locale={locale}
754
+ dateFormats={dateFormats}
755
+ depth={GROUPED_TRAY_ITEM_DEPTH}
756
+ />
757
+ )}
758
+ </For>
759
+ </div>
760
+ )}
761
+ </Show>
579
762
  </div>
580
763
  );
581
764
  }
582
765
 
583
- // NodeList is memo-wrapped so that when only the filter query changes but
584
- // a subtree's nodes/currentSlug/depth/forceOpen are unchanged, Preact can
585
- // skip re-rendering the whole subtree.
586
- const NodeList = memo(function NodeList({
766
+ function NodeList({
587
767
  nodes,
588
768
  currentSlug,
589
769
  depth,
590
770
  forceOpen,
591
- }: {
592
- nodes: SidebarNavNode[];
593
- currentSlug?: string;
594
- depth: number;
595
- forceOpen: boolean;
596
- }) {
771
+ scopeControls,
772
+ }: { nodes: ReadonlySignal<SidebarNavNode[]>; depth: number } & ActiveProps) {
597
773
  return (
598
- <>
599
- {nodes.map((node, index) => {
600
- const isLast = index === nodes.length - 1;
601
- return node.children.length > 0 ? (
602
- <CategoryNode
603
- key={node.slug}
604
- node={node}
605
- currentSlug={currentSlug}
606
- depth={depth}
607
- isLast={isLast}
608
- forceOpen={forceOpen}
609
- />
610
- ) : (
611
- <LeafNode
612
- key={node.slug}
613
- node={node}
614
- currentSlug={currentSlug}
615
- depth={depth}
616
- isLast={isLast}
617
- />
774
+ <For each={nodes} by={(node) => node.occurrenceId ?? node.slug}>
775
+ {(node, index) => {
776
+ const isLast = computed(() => index.value === nodes.value.length - 1);
777
+ return (
778
+ <Show
779
+ when={computed(() => node.value.children.length > 0)}
780
+ fallback={() => (
781
+ <LeafNode
782
+ node={node}
783
+ currentSlug={currentSlug}
784
+ depth={depth}
785
+ isLast={isLast}
786
+ forceOpen={forceOpen}
787
+ scopeControls={scopeControls}
788
+ />
789
+ )}
790
+ >
791
+ {() => (
792
+ <CategoryNode
793
+ node={node}
794
+ currentSlug={currentSlug}
795
+ depth={depth}
796
+ isLast={isLast}
797
+ forceOpen={forceOpen}
798
+ scopeControls={scopeControls}
799
+ />
800
+ )}
801
+ </Show>
618
802
  );
619
- })}
620
- </>
803
+ }}
804
+ </For>
621
805
  );
622
- });
806
+ }
623
807
 
624
- /** Check if currentSlug is anywhere in this node's subtree */
625
808
  function subtreeContainsSlug(node: SidebarNavNode, slug?: string): boolean {
626
- if (!slug) return false;
627
- if (node.slug === slug) return true;
628
- return node.children.some((child) => subtreeContainsSlug(child, slug));
809
+ return (
810
+ slug !== undefined &&
811
+ (node.slug === slug ||
812
+ node.children.some((child) => subtreeContainsSlug(child, slug)))
813
+ );
629
814
  }
630
815
 
631
- // CategoryNode is memo-wrapped so unchanged category nodes are skipped during
632
- // filter-query re-renders.
633
- const CategoryNode = memo(function CategoryNode({
816
+ function CategoryNode({
634
817
  node,
635
818
  currentSlug,
636
819
  depth,
637
820
  isLast,
638
821
  forceOpen,
639
- }: {
640
- node: SidebarNavNode;
641
- currentSlug?: string;
642
- depth: number;
643
- isLast: boolean;
644
- forceOpen: boolean;
645
- }) {
646
- const containsCurrent = useMemo(
647
- () => subtreeContainsSlug(node, currentSlug),
648
- [node, currentSlug],
822
+ scopeControls,
823
+ }: RowProps) {
824
+ const scope = getScope();
825
+ const containsCurrent = computed(() =>
826
+ subtreeContainsSlug(node.value, currentSlug.value),
649
827
  );
650
- const isActive = node.slug === currentSlug;
651
- const labelHtml = useMemo(() => smartBreakToHtml(node.label), [node.label]);
652
-
653
- const [open, setOpen] = useState(containsCurrent ? true : !node.collapsed);
654
-
655
- useEffect(() => {
656
- const stored = getOpenSet();
657
- if (stored.has(node.slug) && !open) {
658
- setOpen(true);
659
- }
660
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
661
-
662
- useEffect(() => {
663
- if (subtreeContainsSlug(node, currentSlug) && !open) {
664
- setOpen(true);
828
+ const active = computed(() => node.value.slug === currentSlug.value);
829
+ const occurrenceKey = () => node.value.occurrenceId ?? node.value.slug;
830
+ const open = signal(scopeControls?.expansion.value[occurrenceKey()] ?? (scopeControls?.defaultOpen(node.value) ?? (containsCurrent.value || !node.value.collapsed)));
831
+ scope.onActivate(() => {
832
+ if (!scopeControls && getOpenSet().has(node.value.slug)) open.value = true;
833
+ });
834
+ scope.effect(() => {
835
+ const slug = currentSlug.value;
836
+ const revision = scopeControls?.reveal.value;
837
+ if (slug !== undefined && subtreeContainsSlug(node.value, slug)) open.value = true;
838
+ void revision;
839
+ });
840
+ scope.effect(() => {
841
+ if (!scopeControls && open.value) {
665
842
  const stored = getOpenSet();
666
- stored.add(node.slug);
667
- saveOpenSet(stored);
668
- }
669
- }, [currentSlug]); // eslint-disable-line react-hooks/exhaustive-deps
670
-
671
- useEffect(() => {
672
- if (open) {
673
- const stored = getOpenSet();
674
- if (!stored.has(node.slug)) {
675
- stored.add(node.slug);
843
+ if (!stored.has(node.value.slug)) {
844
+ stored.add(node.value.slug);
676
845
  saveOpenSet(stored);
677
846
  }
678
847
  }
679
- }, [open, node.slug]);
680
-
681
- const toggle = useCallback(() => {
682
- setOpen((prev) => {
683
- const next = !prev;
684
- const stored = getOpenSet();
685
- if (next) {
686
- stored.add(node.slug);
687
- } else {
688
- stored.delete(node.slug);
689
- }
690
- saveOpenSet(stored);
691
- return next;
692
- });
693
- }, [node.slug]);
694
-
695
- const isExpanded = forceOpen || open;
848
+ });
849
+ const toggle = () => {
850
+ open.value = !open.value;
851
+ if (scopeControls) {
852
+ scopeControls.expansion.value = { ...scopeControls.expansion.value, [occurrenceKey()]: open.value };
853
+ scopeControls.save();
854
+ return;
855
+ }
856
+ const stored = getOpenSet();
857
+ if (open.value) stored.add(node.value.slug);
858
+ else stored.delete(node.value.slug);
859
+ saveOpenSet(stored);
860
+ };
861
+ scope.effect(() => {
862
+ const stored = scopeControls?.expansion.value[occurrenceKey()];
863
+ if (scopeControls) open.value = stored ?? scopeControls.defaultOpen(node.value);
864
+ });
865
+ const expanded = computed(() => forceOpen.value || open.value);
866
+ const focusButton = scopeControls ? (
867
+ <button type="button" data-sidebar-focus data-sidebar-focus-scope={computed(() => node.value.occurrenceId ?? "")}
868
+ on:click={() => scopeControls.focus(node.value)}
869
+ aria-label={computed(() => `${scopeControls.focusLabel}: ${node.value.label}`)}
870
+ title={computed(() => `${scopeControls.focusLabel}: ${node.value.label}`)}
871
+ class={computed(() => `ml-auto shrink-0 rounded px-hsp-xs py-vsp-2xs ${active.value ? "text-bg hover:bg-bg/10 focus-visible:bg-bg/10" : "text-muted hover:bg-surface hover:text-fg focus-visible:bg-surface focus-visible:text-fg"}`)}>
872
+ <span aria-hidden="true">◎</span>
873
+ </button>
874
+ ) : null;
696
875
  const paddingLeft = padLeft(depth, true);
697
-
698
876
  return (
699
- <div className={`${depth === 0 ? "border-t border-muted" : ""} ${depth >= 1 && !isLast ? "relative" : ""}`}>
700
- {depth >= 1 && !isLast && isExpanded && (
701
- <div
702
- className="absolute border-l border-solid border-muted z-local-1"
703
- style={{
704
- left: connectorLeft(depth),
705
- top: 0,
706
- bottom: 0,
707
- }}
708
- />
877
+ <div
878
+ class={computed(
879
+ () =>
880
+ `${depth === 0 ? "border-t border-muted first:border-t-0" : ""} ${depth >= 1 && !isLast.value ? "relative" : ""}`,
709
881
  )}
710
- <div className="relative">
711
- <ConnectorLines depth={depth} isLast={isLast} topPad="calc(0.15rem + var(--spacing-vsp-xs))" />
712
- {node.href ? (
882
+ >
883
+ <Show
884
+ when={computed(() => depth >= 1 && !isLast.value && expanded.value)}
885
+ >
886
+ {() => (
713
887
  <div
714
- className={`flex w-full items-center text-small font-semibold pt-[0.15rem] ${isActive ? "bg-fg text-bg" : "text-fg"}`}
715
- >
716
- <a
717
- href={node.href}
718
- aria-current={isActive ? "page" : undefined}
719
- className={`flex-1 flex items-start gap-hsp-xs py-vsp-xs hover:underline focus:underline break-words ${isActive ? "text-bg" : "text-fg hover:text-accent focus:text-accent"}`}
720
- style={{ paddingLeft }}
721
- >
722
- {depth === 0 && (
723
- <span className="flex h-[1lh] items-center">
724
- <CategoryLinkIcon className={`w-[14px] ${isActive ? "text-bg" : ""}`} />
725
- </span>
726
- )}
727
- <span dangerouslySetInnerHTML={{ __html: labelHtml }} />
728
- </a>
888
+ class="absolute border-l border-solid border-muted z-local-1"
889
+ style={{ left: connectorLeft(depth), top: "0px", bottom: "0px" }}
890
+ />
891
+ )}
892
+ </Show>
893
+ <div class="relative">
894
+ <ConnectorLinesLive
895
+ depth={depth}
896
+ isLast={isLast}
897
+ topPad="calc(0.15rem + var(--spacing-vsp-xs))"
898
+ />
899
+ <Show
900
+ when={computed(() => !!node.value.href)}
901
+ fallback={() => (
902
+ <div class="flex flex-wrap items-center">
729
903
  <button
730
904
  type="button"
731
- onClick={toggle}
732
- className={`aspect-square flex items-center justify-center w-[1.5rem] border-y border-l hover:underline focus:underline ${isActive ? "border-bg/30" : "border-muted"}`}
733
- aria-expanded={isExpanded}
734
- aria-label={isExpanded ? `Collapse ${node.label}` : `Expand ${node.label}`}
905
+ on:click={toggle}
906
+ class="flex flex-1 max-w-full items-center gap-hsp-md text-left text-small font-semibold py-vsp-xs text-fg hover:text-accent hover:underline focus:underline focus:text-accent break-words"
907
+ style={{ "padding-left": paddingLeft }}
908
+ aria-expanded={computed(() =>
909
+ expanded.value ? "true" : "false",
910
+ )}
911
+ aria-label={computed(
912
+ () =>
913
+ `${expanded.value ? "Collapse" : "Expand"} ${node.value.label}`,
914
+ )}
735
915
  >
736
- <ToggleChevron isExpanded={isExpanded} className={isActive ? "text-bg" : "text-muted"} />
916
+ <span class="aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted">
917
+ <ToggleChevron isExpanded={expanded} className="text-muted" />
918
+ </span>
919
+ <span
920
+ rawHtml={computed(() => smartBreakToHtml(node.value.label))}
921
+ />
737
922
  </button>
923
+ {focusButton}
924
+ </div>
925
+ )}
926
+ >
927
+ {() => (
928
+ <div
929
+ class={computed(
930
+ () =>
931
+ `flex w-full items-center text-small font-semibold pt-[0.15rem] ${active.value ? "bg-fg text-bg" : "text-fg"}`,
932
+ )}
933
+ >
934
+ <a
935
+ href={computed(() => node.value.href ?? "")}
936
+ aria-current={computed(() => (active.value ? "page" : "false"))}
937
+ class={computed(
938
+ () =>
939
+ `flex-1 flex items-start gap-hsp-xs py-vsp-xs hover:underline focus:underline break-words ${active.value ? "text-bg" : "text-fg hover:text-accent focus:text-accent"}`,
940
+ )}
941
+ style={{ "padding-left": paddingLeft }}
942
+ >
943
+ {depth === 0 ? (
944
+ <span class="flex h-[1lh] items-center">
945
+ <CategoryLinkIcon class="w-[14px]" />
946
+ </span>
947
+ ) : null}
948
+ <span
949
+ rawHtml={computed(() => smartBreakToHtml(node.value.label))}
950
+ />
951
+ </a>
952
+ {focusButton}
953
+ <button
954
+ type="button"
955
+ on:click={toggle}
956
+ class={computed(
957
+ () =>
958
+ `aspect-square flex items-center justify-center w-[1.5rem] border-y border-l hover:underline focus:underline ${active.value ? "border-bg/30" : "border-muted"}`,
959
+ )}
960
+ aria-expanded={computed(() =>
961
+ expanded.value ? "true" : "false",
962
+ )}
963
+ aria-label={computed(
964
+ () =>
965
+ `${expanded.value ? "Collapse" : "Expand"} ${node.value.label}`,
966
+ )}
967
+ >
968
+ <ToggleChevron
969
+ isExpanded={expanded}
970
+ className={computed(() =>
971
+ active.value ? "text-bg" : "text-muted",
972
+ )}
973
+ />
974
+ </button>
975
+ </div>
976
+ )}
977
+ </Show>
978
+ </div>
979
+ <Show when={expanded}>
980
+ {() => (
981
+ <div>
982
+ <NodeList
983
+ nodes={computed(() => node.value.children)}
984
+ currentSlug={currentSlug}
985
+ depth={depth + 1}
986
+ forceOpen={forceOpen}
987
+ scopeControls={scopeControls}
988
+ />
738
989
  </div>
739
- ) : (
740
- <button
741
- type="button"
742
- onClick={toggle}
743
- className={`flex w-full items-center gap-hsp-md text-left text-small font-semibold py-vsp-xs text-fg hover:text-accent hover:underline focus:underline focus:text-accent break-words`}
744
- style={{ paddingLeft }}
745
- aria-expanded={isExpanded}
746
- aria-label={isExpanded ? `Collapse ${node.label}` : `Expand ${node.label}`}
747
- >
748
- <span className="aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted">
749
- <ToggleChevron isExpanded={isExpanded} className="text-muted" />
750
- </span>
751
- <span dangerouslySetInnerHTML={{ __html: labelHtml }} />
752
- </button>
753
990
  )}
754
- </div>
755
- {isExpanded && (
756
- <div>
757
- <NodeList
758
- nodes={node.children}
759
- currentSlug={currentSlug}
760
- depth={depth + 1}
761
- forceOpen={forceOpen}
762
- />
763
- </div>
764
- )}
991
+ </Show>
765
992
  </div>
766
993
  );
767
- });
994
+ }
768
995
 
769
- // LeafNode is memo-wrapped and labelHtml is memoised so pure leaf rows are
770
- // skipped entirely during filter-query re-renders when their props are stable.
771
- const LeafNode = memo(function LeafNode({
772
- node,
773
- currentSlug,
774
- depth,
775
- isLast,
776
- }: {
777
- node: SidebarNavNode;
778
- currentSlug?: string;
779
- depth: number;
780
- isLast: boolean;
781
- }) {
782
- const labelHtml = useMemo(() => smartBreakToHtml(node.label), [node.label]);
783
- if (!node.href) return null;
784
- const isActive = node.slug === currentSlug;
996
+ function LeafNode({ node, currentSlug, depth, isLast }: RowProps) {
997
+ const active = computed(() => node.value.slug === currentSlug.value);
785
998
  const isRoot = depth === 0;
786
999
  const paddingLeft = padLeft(depth, isRoot);
787
-
788
- const outerClass = isRoot
789
- ? "border-t border-muted"
790
- : !isRoot && isLast
791
- ? "pb-vsp-md"
792
- : "";
793
-
794
1000
  const topPad = isRoot
795
1001
  ? "calc(var(--spacing-vsp-xs) + 0.15rem)"
796
1002
  : "var(--spacing-vsp-2xs)";
797
-
798
1003
  return (
799
- <div className={outerClass}>
800
- <div className="relative">
801
- <ConnectorLines depth={depth} isLast={isLast} topPad={topPad} />
802
- <a
803
- href={node.href}
804
- aria-current={isActive ? "page" : undefined}
805
- data-nav-active={!isRoot && isActive ? "" : undefined}
806
- className={isRoot
807
- ? `flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)+0.15rem)] pr-hsp-xs lg:pr-hsp-sm text-small font-semibold break-words ${
808
- isActive ? "bg-fg text-bg" : "text-fg hover:text-accent hover:underline focus:underline focus:text-accent"
809
- }`
810
- : `block py-vsp-2xs pr-hsp-xs lg:pr-hsp-sm text-small break-words ${
811
- isActive
812
- ? "bg-fg font-medium text-bg"
813
- : "text-muted hover:text-accent hover:underline focus:underline focus:text-accent"
814
- }`
815
- }
816
- style={{ paddingLeft }}
817
- >
818
- {isRoot && (
819
- <span className="flex h-[1lh] items-center">
820
- <CategoryLinkIcon className={`w-[14px] ${isActive ? "text-bg" : ""}`} />
821
- </span>
1004
+ <Show when={computed(() => !!node.value.href)}>
1005
+ {() => (
1006
+ <div
1007
+ class={computed(() =>
1008
+ isRoot ? "border-t border-muted first:border-t-0" : isLast.value ? "pb-vsp-md" : "",
822
1009
  )}
823
- <span dangerouslySetInnerHTML={{ __html: labelHtml }} />
824
- </a>
825
- </div>
826
- </div>
1010
+ >
1011
+ <div class="relative">
1012
+ <ConnectorLinesLive depth={depth} isLast={isLast} topPad={topPad} />
1013
+ <a
1014
+ href={computed(() => node.value.href ?? "")}
1015
+ aria-current={computed(() => (active.value ? "page" : "false"))}
1016
+ data-nav-active={computed(() =>
1017
+ !isRoot && active.value ? "" : null,
1018
+ )}
1019
+ class={computed(() =>
1020
+ isRoot
1021
+ ? `flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)_+_0.15rem)] pr-hsp-xs lg:pr-hsp-sm text-small font-semibold break-words ${active.value ? "bg-fg text-bg" : "text-fg hover:text-accent hover:underline focus:underline focus:text-accent"}`
1022
+ : `block py-vsp-2xs pr-hsp-xs lg:pr-hsp-sm text-small break-words ${active.value ? "bg-fg font-medium text-bg" : "text-muted hover:text-accent hover:underline focus:underline focus:text-accent"}`,
1023
+ )}
1024
+ style={{ "padding-left": paddingLeft }}
1025
+ >
1026
+ {isRoot ? (
1027
+ <span class="flex h-[1lh] items-center">
1028
+ <CategoryLinkIcon class="w-[14px]" />
1029
+ </span>
1030
+ ) : null}
1031
+ <span
1032
+ rawHtml={computed(() => smartBreakToHtml(node.value.label))}
1033
+ />
1034
+ </a>
1035
+ </div>
1036
+ </div>
1037
+ )}
1038
+ </Show>
827
1039
  );
828
- });
1040
+ }