@takazudo/zudo-doc 0.1.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 (255) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/body-foot-util/body-foot-util-area.d.ts +55 -0
  4. package/dist/body-foot-util/body-foot-util-area.js +71 -0
  5. package/dist/body-foot-util/edit-link.d.ts +45 -0
  6. package/dist/body-foot-util/edit-link.js +42 -0
  7. package/dist/body-foot-util/index.d.ts +3 -0
  8. package/dist/body-foot-util/index.js +11 -0
  9. package/dist/breadcrumb/breadcrumb.d.ts +57 -0
  10. package/dist/breadcrumb/breadcrumb.js +113 -0
  11. package/dist/breadcrumb/find-path.d.ts +16 -0
  12. package/dist/breadcrumb/find-path.js +13 -0
  13. package/dist/breadcrumb/index.d.ts +4 -0
  14. package/dist/breadcrumb/index.js +7 -0
  15. package/dist/breadcrumb/types.d.ts +32 -0
  16. package/dist/breadcrumb/types.js +0 -0
  17. package/dist/code-syntax/code-block-enhancer-script.d.ts +3 -0
  18. package/dist/code-syntax/code-block-enhancer-script.js +165 -0
  19. package/dist/code-syntax/code-block-enhancer.d.ts +22 -0
  20. package/dist/code-syntax/code-block-enhancer.js +13 -0
  21. package/dist/code-syntax/index.d.ts +8 -0
  22. package/dist/code-syntax/index.js +29 -0
  23. package/dist/code-syntax/mermaid-init-script.d.ts +51 -0
  24. package/dist/code-syntax/mermaid-init-script.js +225 -0
  25. package/dist/code-syntax/mermaid-init.d.ts +48 -0
  26. package/dist/code-syntax/mermaid-init.js +14 -0
  27. package/dist/code-syntax/tabs-init-script.d.ts +3 -0
  28. package/dist/code-syntax/tabs-init-script.js +106 -0
  29. package/dist/code-syntax/tabs-init.d.ts +24 -0
  30. package/dist/code-syntax/tabs-init.js +10 -0
  31. package/dist/code-syntax/tabs.d.ts +36 -0
  32. package/dist/code-syntax/tabs.js +62 -0
  33. package/dist/content/component-map.d.ts +51 -0
  34. package/dist/content/component-map.js +29 -0
  35. package/dist/content/content-blockquote.d.ts +11 -0
  36. package/dist/content/content-blockquote.js +14 -0
  37. package/dist/content/content-code.d.ts +26 -0
  38. package/dist/content/content-code.js +39 -0
  39. package/dist/content/content-link.d.ts +12 -0
  40. package/dist/content/content-link.js +43 -0
  41. package/dist/content/content-ol.d.ts +11 -0
  42. package/dist/content/content-ol.js +15 -0
  43. package/dist/content/content-paragraph.d.ts +10 -0
  44. package/dist/content/content-paragraph.js +7 -0
  45. package/dist/content/content-strong.d.ts +11 -0
  46. package/dist/content/content-strong.js +14 -0
  47. package/dist/content/content-table.d.ts +11 -0
  48. package/dist/content/content-table.js +14 -0
  49. package/dist/content/content-ul.d.ts +11 -0
  50. package/dist/content/content-ul.js +15 -0
  51. package/dist/content/heading-h2.d.ts +14 -0
  52. package/dist/content/heading-h2.js +18 -0
  53. package/dist/content/heading-h3.d.ts +14 -0
  54. package/dist/content/heading-h3.js +18 -0
  55. package/dist/content/heading-h4.d.ts +14 -0
  56. package/dist/content/heading-h4.js +18 -0
  57. package/dist/content/index.d.ts +13 -0
  58. package/dist/content/index.js +27 -0
  59. package/dist/details/details.d.ts +26 -0
  60. package/dist/details/details.js +10 -0
  61. package/dist/details/index.d.ts +2 -0
  62. package/dist/details/index.js +4 -0
  63. package/dist/doclayout/anchors.d.ts +55 -0
  64. package/dist/doclayout/anchors.js +42 -0
  65. package/dist/doclayout/doc-layout-with-defaults.d.ts +83 -0
  66. package/dist/doclayout/doc-layout-with-defaults.js +159 -0
  67. package/dist/doclayout/doc-layout.d.ts +121 -0
  68. package/dist/doclayout/doc-layout.js +89 -0
  69. package/dist/doclayout/index.d.ts +6 -0
  70. package/dist/doclayout/index.js +17 -0
  71. package/dist/footer/footer.d.ts +42 -0
  72. package/dist/footer/footer.js +79 -0
  73. package/dist/footer/index.d.ts +3 -0
  74. package/dist/footer/index.js +4 -0
  75. package/dist/footer/types.d.ts +58 -0
  76. package/dist/footer/types.js +0 -0
  77. package/dist/head/doc-head.d.ts +33 -0
  78. package/dist/head/doc-head.js +90 -0
  79. package/dist/head/index.d.ts +5 -0
  80. package/dist/head/index.js +8 -0
  81. package/dist/head/og-tags.d.ts +25 -0
  82. package/dist/head/og-tags.js +16 -0
  83. package/dist/head/twitter-card.d.ts +21 -0
  84. package/dist/head/twitter-card.js +14 -0
  85. package/dist/head/types.d.ts +83 -0
  86. package/dist/head/types.js +0 -0
  87. package/dist/header/header.d.ts +158 -0
  88. package/dist/header/header.js +367 -0
  89. package/dist/header/index.d.ts +6 -0
  90. package/dist/header/index.js +18 -0
  91. package/dist/header/nav-active.d.ts +39 -0
  92. package/dist/header/nav-active.js +24 -0
  93. package/dist/header/nav-overflow-script.d.ts +3 -0
  94. package/dist/header/nav-overflow-script.js +169 -0
  95. package/dist/header/right-items.d.ts +38 -0
  96. package/dist/header/right-items.js +30 -0
  97. package/dist/header/types.d.ts +40 -0
  98. package/dist/header/types.js +0 -0
  99. package/dist/html-preview-wrapper/dedent.d.ts +10 -0
  100. package/dist/html-preview-wrapper/dedent.js +16 -0
  101. package/dist/html-preview-wrapper/highlighted-code.d.ts +18 -0
  102. package/dist/html-preview-wrapper/highlighted-code.js +53 -0
  103. package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +60 -0
  104. package/dist/html-preview-wrapper/html-preview-wrapper.js +36 -0
  105. package/dist/html-preview-wrapper/html-preview.d.ts +35 -0
  106. package/dist/html-preview-wrapper/html-preview.js +75 -0
  107. package/dist/html-preview-wrapper/index.d.ts +5 -0
  108. package/dist/html-preview-wrapper/index.js +12 -0
  109. package/dist/html-preview-wrapper/preflight.d.ts +13 -0
  110. package/dist/html-preview-wrapper/preflight.js +108 -0
  111. package/dist/html-preview-wrapper/preview-base.d.ts +29 -0
  112. package/dist/html-preview-wrapper/preview-base.js +127 -0
  113. package/dist/i18n-version/index.d.ts +5 -0
  114. package/dist/i18n-version/index.js +14 -0
  115. package/dist/i18n-version/language-switcher.d.ts +24 -0
  116. package/dist/i18n-version/language-switcher.js +25 -0
  117. package/dist/i18n-version/types.d.ts +43 -0
  118. package/dist/i18n-version/types.js +0 -0
  119. package/dist/i18n-version/version-banner.d.ts +30 -0
  120. package/dist/i18n-version/version-banner.js +21 -0
  121. package/dist/i18n-version/version-switcher.d.ts +112 -0
  122. package/dist/i18n-version/version-switcher.js +157 -0
  123. package/dist/index.d.ts +2 -0
  124. package/dist/index.js +0 -0
  125. package/dist/integrations/claude-resources/escape-for-mdx.d.ts +8 -0
  126. package/dist/integrations/claude-resources/escape-for-mdx.js +131 -0
  127. package/dist/integrations/claude-resources/generate.d.ts +13 -0
  128. package/dist/integrations/claude-resources/generate.js +404 -0
  129. package/dist/integrations/claude-resources/index.d.ts +46 -0
  130. package/dist/integrations/claude-resources/index.js +24 -0
  131. package/dist/integrations/doc-history/index.d.ts +137 -0
  132. package/dist/integrations/doc-history/index.js +138 -0
  133. package/dist/integrations/doc-history/pre-build.d.ts +58 -0
  134. package/dist/integrations/doc-history/pre-build.js +67 -0
  135. package/dist/integrations/llms-txt/dev-middleware.d.ts +40 -0
  136. package/dist/integrations/llms-txt/dev-middleware.js +76 -0
  137. package/dist/integrations/llms-txt/emit.d.ts +22 -0
  138. package/dist/integrations/llms-txt/emit.js +55 -0
  139. package/dist/integrations/llms-txt/generate.d.ts +26 -0
  140. package/dist/integrations/llms-txt/generate.js +36 -0
  141. package/dist/integrations/llms-txt/index.d.ts +7 -0
  142. package/dist/integrations/llms-txt/index.js +27 -0
  143. package/dist/integrations/llms-txt/load.d.ts +51 -0
  144. package/dist/integrations/llms-txt/load.js +83 -0
  145. package/dist/integrations/llms-txt/strip.d.ts +22 -0
  146. package/dist/integrations/llms-txt/strip.js +10 -0
  147. package/dist/integrations/llms-txt/types.d.ts +145 -0
  148. package/dist/integrations/llms-txt/types.js +0 -0
  149. package/dist/integrations/search-index/build-emitter.d.ts +30 -0
  150. package/dist/integrations/search-index/build-emitter.js +15 -0
  151. package/dist/integrations/search-index/collect.d.ts +12 -0
  152. package/dist/integrations/search-index/collect.js +48 -0
  153. package/dist/integrations/search-index/content-files.d.ts +32 -0
  154. package/dist/integrations/search-index/content-files.js +50 -0
  155. package/dist/integrations/search-index/dev-middleware.d.ts +16 -0
  156. package/dist/integrations/search-index/dev-middleware.js +30 -0
  157. package/dist/integrations/search-index/index.d.ts +5 -0
  158. package/dist/integrations/search-index/index.js +14 -0
  159. package/dist/integrations/search-index/types.d.ts +38 -0
  160. package/dist/integrations/search-index/types.js +6 -0
  161. package/dist/metainfo/doc-metainfo.d.ts +50 -0
  162. package/dist/metainfo/doc-metainfo.js +107 -0
  163. package/dist/metainfo/doc-tags.d.ts +66 -0
  164. package/dist/metainfo/doc-tags.js +53 -0
  165. package/dist/metainfo/frontmatter-preview.d.ts +95 -0
  166. package/dist/metainfo/frontmatter-preview.js +55 -0
  167. package/dist/metainfo/index.d.ts +4 -0
  168. package/dist/metainfo/index.js +28 -0
  169. package/dist/nav-indexing/category-nav.d.ts +28 -0
  170. package/dist/nav-indexing/category-nav.js +41 -0
  171. package/dist/nav-indexing/category-tree-nav.d.ts +27 -0
  172. package/dist/nav-indexing/category-tree-nav.js +49 -0
  173. package/dist/nav-indexing/doc-card-grid.d.ts +38 -0
  174. package/dist/nav-indexing/doc-card-grid.js +40 -0
  175. package/dist/nav-indexing/docs-sitemap.d.ts +25 -0
  176. package/dist/nav-indexing/docs-sitemap.js +40 -0
  177. package/dist/nav-indexing/index.d.ts +10 -0
  178. package/dist/nav-indexing/index.js +18 -0
  179. package/dist/nav-indexing/nav-card-grid.d.ts +31 -0
  180. package/dist/nav-indexing/nav-card-grid.js +43 -0
  181. package/dist/nav-indexing/site-tree-nav-demo.d.ts +39 -0
  182. package/dist/nav-indexing/site-tree-nav-demo.js +65 -0
  183. package/dist/nav-indexing/tag-nav.d.ts +38 -0
  184. package/dist/nav-indexing/tag-nav.js +96 -0
  185. package/dist/nav-indexing/types.d.ts +95 -0
  186. package/dist/nav-indexing/types.js +0 -0
  187. package/dist/nav-indexing/versions-page-content.d.ts +29 -0
  188. package/dist/nav-indexing/versions-page-content.js +76 -0
  189. package/dist/page-loading/index.d.ts +2 -0
  190. package/dist/page-loading/index.js +10 -0
  191. package/dist/page-loading/page-loading-overlay.d.ts +35 -0
  192. package/dist/page-loading/page-loading-overlay.js +50 -0
  193. package/dist/sidebar/index.d.ts +3 -0
  194. package/dist/sidebar/index.js +4 -0
  195. package/dist/sidebar/sidebar.d.ts +54 -0
  196. package/dist/sidebar/sidebar.js +35 -0
  197. package/dist/sidebar/types.d.ts +71 -0
  198. package/dist/sidebar/types.js +0 -0
  199. package/dist/sidebar-resizer/index.d.ts +14 -0
  200. package/dist/sidebar-resizer/index.js +165 -0
  201. package/dist/sidebar-resizer/sidebar-resizer-init.d.ts +20 -0
  202. package/dist/sidebar-resizer/sidebar-resizer-init.js +119 -0
  203. package/dist/sidebar-tree/build-tree.d.ts +43 -0
  204. package/dist/sidebar-tree/build-tree.js +127 -0
  205. package/dist/sidebar-tree/category-meta.d.ts +19 -0
  206. package/dist/sidebar-tree/category-meta.js +63 -0
  207. package/dist/sidebar-tree/index.d.ts +3 -0
  208. package/dist/sidebar-tree/index.js +13 -0
  209. package/dist/sidebar-tree/types.d.ts +120 -0
  210. package/dist/sidebar-tree/types.js +0 -0
  211. package/dist/tab-item/index.d.ts +2 -0
  212. package/dist/tab-item/index.js +5 -0
  213. package/dist/tab-item/tab-item.d.ts +38 -0
  214. package/dist/tab-item/tab-item.js +25 -0
  215. package/dist/theme/color-scheme-provider.d.ts +32 -0
  216. package/dist/theme/color-scheme-provider.js +43 -0
  217. package/dist/theme/color-tweak-export-modal.d.ts +20 -0
  218. package/dist/theme/color-tweak-export-modal.js +138 -0
  219. package/dist/theme/design-token-serde.d.ts +136 -0
  220. package/dist/theme/design-token-serde.js +254 -0
  221. package/dist/theme/design-token-tweak-panel.d.ts +11 -0
  222. package/dist/theme/design-token-tweak-panel.js +10 -0
  223. package/dist/theme/design-token-types.d.ts +19 -0
  224. package/dist/theme/design-token-types.js +6 -0
  225. package/dist/theme/iframe-bridge.d.ts +44 -0
  226. package/dist/theme/iframe-bridge.js +70 -0
  227. package/dist/theme/index.d.ts +9 -0
  228. package/dist/theme/index.js +42 -0
  229. package/dist/theme/theme-toggle.d.ts +17 -0
  230. package/dist/theme/theme-toggle.js +91 -0
  231. package/dist/toc/cx.d.ts +16 -0
  232. package/dist/toc/cx.js +22 -0
  233. package/dist/toc/index.d.ts +5 -0
  234. package/dist/toc/index.js +9 -0
  235. package/dist/toc/mobile-toc.d.ts +46 -0
  236. package/dist/toc/mobile-toc.js +88 -0
  237. package/dist/toc/smart-break.d.ts +27 -0
  238. package/dist/toc/smart-break.js +36 -0
  239. package/dist/toc/toc-title.d.ts +10 -0
  240. package/dist/toc/toc-title.js +14 -0
  241. package/dist/toc/toc.d.ts +60 -0
  242. package/dist/toc/toc.js +56 -0
  243. package/dist/toc/types.d.ts +23 -0
  244. package/dist/toc/types.js +0 -0
  245. package/dist/toc/use-active-heading.d.ts +30 -0
  246. package/dist/toc/use-active-heading.js +106 -0
  247. package/dist/transitions/index.d.ts +1 -0
  248. package/dist/transitions/index.js +12 -0
  249. package/dist/transitions/page-events.d.ts +41 -0
  250. package/dist/transitions/page-events.js +20 -0
  251. package/dist/url-normalizer/index.d.ts +1 -0
  252. package/dist/url-normalizer/index.js +6 -0
  253. package/dist/url-normalizer/normalize.d.ts +104 -0
  254. package/dist/url-normalizer/normalize.js +22 -0
  255. package/package.json +186 -0
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "preact/jsx-runtime";
3
+ import { useState, useEffect } from "preact/hooks";
4
+ import { Island } from "@takazudo/zfb";
5
+ const STORAGE_KEY = "zudo-doc-theme";
6
+ function SunIcon() {
7
+ return /* @__PURE__ */ jsxs(
8
+ "svg",
9
+ {
10
+ "aria-hidden": "true",
11
+ xmlns: "http://www.w3.org/2000/svg",
12
+ width: "20",
13
+ height: "20",
14
+ viewBox: "0 0 24 24",
15
+ fill: "none",
16
+ stroke: "currentColor",
17
+ strokeWidth: "2",
18
+ strokeLinecap: "round",
19
+ strokeLinejoin: "round",
20
+ children: [
21
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "5" }),
22
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
23
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
24
+ /* @__PURE__ */ jsx("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
25
+ /* @__PURE__ */ jsx("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
26
+ /* @__PURE__ */ jsx("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
27
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
28
+ /* @__PURE__ */ jsx("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
29
+ /* @__PURE__ */ jsx("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
30
+ ]
31
+ }
32
+ );
33
+ }
34
+ function MoonIcon() {
35
+ return /* @__PURE__ */ jsx(
36
+ "svg",
37
+ {
38
+ "aria-hidden": "true",
39
+ xmlns: "http://www.w3.org/2000/svg",
40
+ width: "20",
41
+ height: "20",
42
+ viewBox: "0 0 24 24",
43
+ fill: "none",
44
+ stroke: "currentColor",
45
+ strokeWidth: "2",
46
+ strokeLinecap: "round",
47
+ strokeLinejoin: "round",
48
+ children: /* @__PURE__ */ jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
49
+ }
50
+ );
51
+ }
52
+ function ThemeToggleInner({ defaultMode = "dark" }) {
53
+ const [mode, setMode] = useState(defaultMode);
54
+ useEffect(() => {
55
+ const actual = document.documentElement.getAttribute("data-theme") || defaultMode;
56
+ if (actual !== mode) {
57
+ setMode(actual);
58
+ }
59
+ }, []);
60
+ function toggle() {
61
+ const next = mode === "dark" ? "light" : "dark";
62
+ setMode(next);
63
+ document.documentElement.setAttribute("data-theme", next);
64
+ document.documentElement.style.colorScheme = next;
65
+ localStorage.setItem(STORAGE_KEY, next);
66
+ localStorage.removeItem("zudo-doc-tweak-state");
67
+ localStorage.removeItem("zudo-doc-tweak-state-v2");
68
+ window.dispatchEvent(new CustomEvent("color-scheme-changed"));
69
+ }
70
+ const nextMode = mode === "dark" ? "light" : "dark";
71
+ return /* @__PURE__ */ jsx(
72
+ "button",
73
+ {
74
+ onClick: toggle,
75
+ "aria-label": `Switch to ${nextMode} mode`,
76
+ className: "text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
77
+ children: mode === "dark" ? /* @__PURE__ */ jsx(SunIcon, {}) : /* @__PURE__ */ jsx(MoonIcon, {})
78
+ }
79
+ );
80
+ }
81
+ ThemeToggleInner.displayName = "ThemeToggle";
82
+ function ThemeToggle(props = {}) {
83
+ const rendered = Island({
84
+ when: "load",
85
+ children: /* @__PURE__ */ jsx(ThemeToggleInner, { ...props })
86
+ });
87
+ return rendered;
88
+ }
89
+ export {
90
+ ThemeToggle as default
91
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Tiny `clsx`-style class name joiner. Accepts strings, falsy values,
3
+ * and arrays/objects of the same — only truthy strings survive.
4
+ *
5
+ * The host project depends on `clsx`, but framework primitives keep
6
+ * their dependency surface minimal so that a downstream consumer of
7
+ * `@takazudo/zudo-doc/toc` does not need an extra runtime dep just
8
+ * for class name composition. The behavior here is the subset the TOC
9
+ * primitives use; see clsx for the full feature set.
10
+ */
11
+ type ClassValue = string | number | bigint | boolean | null | undefined | ClassValue[] | {
12
+ [key: string]: unknown;
13
+ };
14
+ declare function cx(...args: ClassValue[]): string;
15
+
16
+ export { cx };
package/dist/toc/cx.js ADDED
@@ -0,0 +1,22 @@
1
+ function cx(...args) {
2
+ const out = [];
3
+ for (const arg of args) {
4
+ if (!arg) continue;
5
+ if (typeof arg === "string") {
6
+ out.push(arg);
7
+ } else if (typeof arg === "number" || typeof arg === "bigint") {
8
+ out.push(String(arg));
9
+ } else if (Array.isArray(arg)) {
10
+ const inner = cx(...arg);
11
+ if (inner) out.push(inner);
12
+ } else if (typeof arg === "object") {
13
+ for (const key of Object.keys(arg)) {
14
+ if (arg[key]) out.push(key);
15
+ }
16
+ }
17
+ }
18
+ return out.join(" ");
19
+ }
20
+ export {
21
+ cx
22
+ };
@@ -0,0 +1,5 @@
1
+ export { Toc, TocProps } from './toc.js';
2
+ export { MobileToc, MobileTocProps } from './mobile-toc.js';
3
+ export { UseActiveHeadingResult, getActiveHeadingId, useActiveHeading } from './use-active-heading.js';
4
+ export { HeadingItem, TocItem } from './types.js';
5
+ import 'preact';
@@ -0,0 +1,9 @@
1
+ import { Toc } from "./toc.js";
2
+ import { MobileToc } from "./mobile-toc.js";
3
+ import { useActiveHeading, getActiveHeadingId } from "./use-active-heading.js";
4
+ export {
5
+ MobileToc,
6
+ Toc,
7
+ getActiveHeadingId,
8
+ useActiveHeading
9
+ };
@@ -0,0 +1,46 @@
1
+ import { VNode } from 'preact';
2
+ import { HeadingItem } from './types.js';
3
+
4
+ /** @jsxRuntime automatic */
5
+ /** @jsxImportSource preact */
6
+
7
+ interface MobileTocProps {
8
+ headings: readonly HeadingItem[];
9
+ title?: string;
10
+ }
11
+ /**
12
+ * Collapsible TOC for narrow viewports (`xl:hidden`) — a Preact island
13
+ * component. Closed by default; tapping the header toggles, and tapping
14
+ * any entry closes the panel after navigation.
15
+ *
16
+ * Renders the `<div class="xl:hidden …">` panel directly. **The caller
17
+ * is responsible for wrapping this in `<Island when="load">`** so the
18
+ * SSG output emits the `data-zfb-island="MobileToc"` hydration marker
19
+ * around the panel. `<DocLayoutWithDefaults>` does this for you;
20
+ * consumers who render `<MobileToc>` outside the default layout (e.g.
21
+ * via the `mobileTocOverride` prop or in a custom layout) must apply
22
+ * the wrapper themselves — otherwise the open/close toggle never
23
+ * hydrates on the client and the panel stays in its initial closed
24
+ * state.
25
+ *
26
+ * Wave 13 (zudolab/zudo-doc#1355): previously this module exported a
27
+ * `MobileToc` wrapper that called `Island(...)` itself; on hydration
28
+ * the runtime ran `hydrate(<MobileToc/>, dataIslandDiv)` and the
29
+ * wrapper re-emitted *another* `<div data-zfb-island="MobileToc">…</div>`
30
+ * inside the existing one, leaving two `xl:hidden …` panels in the
31
+ * post-hydration DOM. See `./toc.tsx` for the full diagnosis. Moving
32
+ * the `<Island>` wrapper to the call site lets the bundle hydrate the
33
+ * bare panel against the existing DOM in-place.
34
+ *
35
+ * Like `Toc`, this always includes the `title` text in the SSG HTML
36
+ * even when no headings qualify. When no headings are present, a
37
+ * CSS-hidden container carries the title string so no-JS users and
38
+ * crawlers can still find the locale label in the static markup. When
39
+ * headings are present the full interactive toggle UI is rendered.
40
+ */
41
+ declare function MobileToc({ headings, title, }: MobileTocProps): VNode;
42
+ declare namespace MobileToc {
43
+ var displayName: string;
44
+ }
45
+
46
+ export { MobileToc, type MobileTocProps };
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "preact/jsx-runtime";
3
+ import { useMemo, useState } from "preact/hooks";
4
+ import { SmartBreak } from "./smart-break.js";
5
+ import { cx } from "./cx.js";
6
+ function MobileToc({
7
+ headings,
8
+ title = "On this page"
9
+ }) {
10
+ const filtered = useMemo(
11
+ () => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
12
+ [headings]
13
+ );
14
+ const [open, setOpen] = useState(false);
15
+ if (filtered.length === 0) {
16
+ return /* @__PURE__ */ jsx("div", { className: "hidden", "aria-hidden": "true", children: title });
17
+ }
18
+ return /* @__PURE__ */ jsxs("div", { className: "xl:hidden border border-muted mb-vsp-lg", children: [
19
+ /* @__PURE__ */ jsxs(
20
+ "button",
21
+ {
22
+ type: "button",
23
+ onClick: () => setOpen((prev) => !prev),
24
+ "aria-expanded": open,
25
+ className: "flex w-full items-center justify-between px-hsp-lg py-vsp-xs text-small font-medium text-fg",
26
+ children: [
27
+ /* @__PURE__ */ jsx("span", { children: title }),
28
+ /* @__PURE__ */ jsx(
29
+ "svg",
30
+ {
31
+ xmlns: "http://www.w3.org/2000/svg",
32
+ "aria-hidden": "true",
33
+ className: cx(
34
+ "h-icon-sm w-icon-sm text-muted transition-transform duration-150",
35
+ open && "rotate-180"
36
+ ),
37
+ fill: "none",
38
+ viewBox: "0 0 24 24",
39
+ stroke: "currentColor",
40
+ strokeWidth: 2,
41
+ children: /* @__PURE__ */ jsx(
42
+ "path",
43
+ {
44
+ strokeLinecap: "round",
45
+ strokeLinejoin: "round",
46
+ d: "M19 9l-7 7-7-7"
47
+ }
48
+ )
49
+ }
50
+ )
51
+ ]
52
+ }
53
+ ),
54
+ /* @__PURE__ */ jsx(
55
+ "ul",
56
+ {
57
+ className: cx(
58
+ "border-t border-muted px-hsp-lg py-vsp-xs space-y-vsp-2xs",
59
+ !open && "hidden"
60
+ ),
61
+ "aria-hidden": !open,
62
+ children: filtered.map((heading, index) => /* @__PURE__ */ jsx(
63
+ "li",
64
+ {
65
+ className: cx(
66
+ heading.depth === 3 && "ml-hsp-lg",
67
+ heading.depth === 4 && "ml-hsp-2xl"
68
+ ),
69
+ children: /* @__PURE__ */ jsx(
70
+ "a",
71
+ {
72
+ href: `#${heading.slug}`,
73
+ onClick: () => setOpen(false),
74
+ className: "block py-vsp-2xs text-small text-muted hover:text-fg hover:underline focus-visible:underline",
75
+ children: /* @__PURE__ */ jsx(SmartBreak, { children: heading.text })
76
+ }
77
+ )
78
+ },
79
+ `${heading.slug}-${index}`
80
+ ))
81
+ }
82
+ )
83
+ ] });
84
+ }
85
+ MobileToc.displayName = "MobileToc";
86
+ export {
87
+ MobileToc
88
+ };
@@ -0,0 +1,27 @@
1
+ import { VNode } from 'preact';
2
+
3
+ /** @jsxRuntime automatic */
4
+ /** @jsxImportSource preact */
5
+
6
+ /**
7
+ * Local copy of the SmartBreak helper used by TOC entries. Kept inside
8
+ * the toc primitive folder so the package is self-contained and does
9
+ * not reach back into the host application's `src/utils/smart-break`.
10
+ *
11
+ * The host's full smart-break util exposes additional helpers
12
+ * (`smartBreakToHtml`, `escapeAndInjectWbr`) that the TOC does not
13
+ * need; this file ports only `isPathLike`, `smartBreak`, and the
14
+ * `SmartBreak` Preact wrapper. Keep the heuristic byte-identical to
15
+ * the host implementation so wbr placement matches everywhere.
16
+ */
17
+ declare function isPathLike(text: string): boolean;
18
+ declare function smartBreak(text: string): VNode | string;
19
+ /**
20
+ * Preact function component wrapper — pure, server-renderable.
21
+ * Stringifies children and defers to smartBreak.
22
+ */
23
+ declare function SmartBreak({ children }: {
24
+ children?: unknown;
25
+ }): any;
26
+
27
+ export { SmartBreak, isPathLike, smartBreak };
@@ -0,0 +1,36 @@
1
+ import { Fragment, jsx } from "preact/jsx-runtime";
2
+ function isPathLike(text) {
3
+ if (!text) return false;
4
+ if (text.includes("://")) return true;
5
+ if (/^\.{0,2}\//.test(text)) return true;
6
+ if (/^[A-Za-z]:[\\/]/.test(text)) return true;
7
+ const forwardMatches = text.match(/[A-Za-z0-9]\/[A-Za-z0-9]/g);
8
+ if (forwardMatches && forwardMatches.length >= 2) return true;
9
+ const backMatches = text.match(/[A-Za-z0-9]\\[A-Za-z0-9]/g);
10
+ if (backMatches && backMatches.length >= 2) return true;
11
+ const hasDomainDot = /[A-Za-z0-9]\.[A-Za-z0-9]/.test(text);
12
+ const hasSlash = /[\\/]/.test(text);
13
+ if (hasDomainDot && hasSlash) return true;
14
+ return false;
15
+ }
16
+ const DELIM_SPLIT = /([/\\\-_.:?#&=])/;
17
+ function smartBreak(text) {
18
+ if (!isPathLike(text)) return text;
19
+ const parts = text.split(DELIM_SPLIT);
20
+ const nodes = [];
21
+ for (let i = 0; i < parts.length; i++) {
22
+ const part = parts[i];
23
+ if (part === "") continue;
24
+ nodes.push(part);
25
+ if (i % 2 === 1) nodes.push(/* @__PURE__ */ jsx("wbr", {}, `wbr-${i}`));
26
+ }
27
+ return /* @__PURE__ */ jsx(Fragment, { children: nodes });
28
+ }
29
+ function SmartBreak({ children }) {
30
+ return /* @__PURE__ */ jsx(Fragment, { children: smartBreak(String(children ?? "")) });
31
+ }
32
+ export {
33
+ SmartBreak,
34
+ isPathLike,
35
+ smartBreak
36
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Return the TOC section label for the given BCP-47 language tag.
3
+ *
4
+ * Accepts full tags ("en-US", "ja-JP") by checking the primary subtag
5
+ * first, then falls back to the full tag, then falls back to the English
6
+ * default so callers always receive a non-empty string.
7
+ */
8
+ declare function getTocTitle(lang?: string): string;
9
+
10
+ export { getTocTitle };
@@ -0,0 +1,14 @@
1
+ const TOC_TITLES = {
2
+ en: "On this page",
3
+ ja: "\u76EE\u6B21",
4
+ de: "Auf dieser Seite"
5
+ };
6
+ const DEFAULT_TOC_TITLE = "On this page";
7
+ function getTocTitle(lang) {
8
+ if (!lang) return DEFAULT_TOC_TITLE;
9
+ const primary = lang.split("-")[0];
10
+ return TOC_TITLES[primary] ?? TOC_TITLES[lang] ?? DEFAULT_TOC_TITLE;
11
+ }
12
+ export {
13
+ getTocTitle
14
+ };
@@ -0,0 +1,60 @@
1
+ import { VNode } from 'preact';
2
+ import { HeadingItem } from './types.js';
3
+
4
+ /** @jsxRuntime automatic */
5
+ /** @jsxImportSource preact */
6
+
7
+ interface TocProps {
8
+ headings: readonly HeadingItem[];
9
+ /**
10
+ * Section label used by MobileToc. The desktop Toc no longer renders
11
+ * a visible heading (issue #1655 / T1) but the prop is kept so callers
12
+ * can share a single title value for both desktop and mobile surfaces.
13
+ * Defaults to "On this page" (English).
14
+ */
15
+ title?: string;
16
+ }
17
+ /**
18
+ * Desktop right-rail Table of Contents — a Preact island component.
19
+ *
20
+ * Renders the `<nav aria-label="Table of contents">` directly. **The
21
+ * caller is responsible for wrapping this in `<Island when="load">`**
22
+ * so the SSG output emits the `data-zfb-island="Toc"` hydration marker
23
+ * around the nav. `<DocLayoutWithDefaults>` does this for you; consumers
24
+ * who render `<Toc>` outside the default layout (e.g. via the
25
+ * `tocOverride` prop or in a custom layout) must apply the wrapper
26
+ * themselves — otherwise the active-section scroll-spy never hydrates
27
+ * on the client and the nav stays static.
28
+ *
29
+ * Wave 13 ("smoke-toc duplicate-nav regression", zudolab/zudo-doc#1355):
30
+ * before this refactor, this module exported a `Toc` wrapper that itself
31
+ * called `Island(...)` internally and an inner `TocInner` component that
32
+ * carried `displayName = "Toc"`. The zfb island scanner picked the
33
+ * exported `Toc` (the wrapper) as the hydration target, so on the
34
+ * client `mountIslands` ran `hydrate(<Toc/>, dataIslandDiv)` where the
35
+ * vnode itself rendered to *another* `<div data-zfb-island="Toc">…<nav>…</nav></div>`
36
+ * — Preact appended the new wrapper-div as a child of the existing
37
+ * data-zfb-island element instead of in-place hydrating the inner nav,
38
+ * leaving two `<nav aria-label="Table of contents">` elements in the
39
+ * post-hydration DOM. The host's Wave 12 prop-serialisation pin
40
+ * (`data-props`) made hydration succeed where it previously crashed
41
+ * silently on the missing `headings` prop, which is why the duplicate
42
+ * only became visible in CI smoke runs after Wave 12. Moving the
43
+ * `<Island>` wrapper to the call site lets the bundle hydrate the bare
44
+ * `<nav>` against the existing DOM in-place, eliminating the duplicate.
45
+ *
46
+ * Renders only depth 2–4 headings (h2/h3/h4), matching zudo-doc's
47
+ * historical behavior — the page title (h1) is rendered separately by
48
+ * the layout and h5+ are deemed too granular for the TOC.
49
+ *
50
+ * The `title` prop is kept on the signature (MobileToc and callers still
51
+ * use it) but the desktop Toc no longer renders a visible heading — the
52
+ * `<nav aria-label="Table of contents">` landmark provides the section
53
+ * semantics for screen readers. Issue #1655 / T1.
54
+ */
55
+ declare function Toc({ headings, title }: TocProps): VNode;
56
+ declare namespace Toc {
57
+ var displayName: string;
58
+ }
59
+
60
+ export { Toc, type TocProps };
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx } from "preact/jsx-runtime";
3
+ import { useMemo } from "preact/hooks";
4
+ import { useActiveHeading } from "./use-active-heading.js";
5
+ import { SmartBreak } from "./smart-break.js";
6
+ import { cx } from "./cx.js";
7
+ function Toc({ headings, title = "On this page" }) {
8
+ const filtered = useMemo(
9
+ () => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
10
+ [headings]
11
+ );
12
+ const { activeId, activate } = useActiveHeading(filtered);
13
+ return /* @__PURE__ */ jsx(
14
+ "nav",
15
+ {
16
+ "aria-label": "Table of contents",
17
+ className: cx(
18
+ "hidden xl:flex flex-col",
19
+ "w-[280px] shrink-0",
20
+ "sticky top-[3.5rem] self-start z-10",
21
+ "pt-vsp-xl lg:pt-vsp-2xl",
22
+ "h-[calc(100vh-3.5rem)]"
23
+ ),
24
+ children: filtered.length > 0 && /* @__PURE__ */ jsx("ul", { className: "border-l border-muted pl-hsp-lg overflow-y-auto", children: filtered.map((heading, index) => {
25
+ const isActive = heading.slug === activeId;
26
+ return /* @__PURE__ */ jsx(
27
+ "li",
28
+ {
29
+ className: cx(
30
+ heading.depth === 3 && "ml-hsp-lg",
31
+ heading.depth === 4 && "ml-hsp-2xl"
32
+ ),
33
+ children: /* @__PURE__ */ jsx(
34
+ "a",
35
+ {
36
+ href: `#${heading.slug}`,
37
+ onClick: () => activate(heading.slug),
38
+ "aria-current": isActive ? "true" : void 0,
39
+ className: cx(
40
+ "block py-vsp-2xs text-small leading-snug transition-colors",
41
+ isActive ? "bg-fg text-bg font-medium" : "text-muted hover:underline focus:underline"
42
+ ),
43
+ children: /* @__PURE__ */ jsx(SmartBreak, { children: heading.text })
44
+ }
45
+ )
46
+ },
47
+ `${heading.slug}-${index}`
48
+ );
49
+ }) })
50
+ }
51
+ );
52
+ }
53
+ Toc.displayName = "Toc";
54
+ export {
55
+ Toc
56
+ };
@@ -0,0 +1,23 @@
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
+ interface HeadingItem {
11
+ readonly depth: number;
12
+ readonly slug: string;
13
+ readonly text: string;
14
+ }
15
+ /**
16
+ * Alias kept for ergonomic imports — `TocItem` is the same shape the
17
+ * TOC components consume after filtering by depth. Filtering does not
18
+ * change the per-item shape, so the alias documents intent without
19
+ * introducing a structural difference.
20
+ */
21
+ type TocItem = HeadingItem;
22
+
23
+ export type { HeadingItem, TocItem };
File without changes
@@ -0,0 +1,30 @@
1
+ import { HeadingItem } from './types.js';
2
+
3
+ /**
4
+ * Pure helper that picks the active heading id given the heading slug
5
+ * order and a slug→element map. Extracted from the hook so it is unit-
6
+ * testable without DOM event plumbing.
7
+ *
8
+ * Algorithm: find the first heading whose top is at or below the
9
+ * SCROLL_MARGIN_TOP threshold ("first visible"). If none, the user has
10
+ * scrolled past every heading — return the last one. If the very first
11
+ * heading is the first visible AND its top is above the viewport
12
+ * midline, treat it as already active. Otherwise activate the
13
+ * predecessor of the first visible heading.
14
+ */
15
+ declare function getActiveHeadingId(headingIds: readonly string[], elementMap: ReadonlyMap<string, HTMLElement>, viewportHeight: number): string | null;
16
+ interface UseActiveHeadingResult {
17
+ activeId: string | null;
18
+ activate: (id: string) => void;
19
+ }
20
+ /**
21
+ * Scroll-spy hook. Tracks which heading slug should be highlighted
22
+ * based on the current scroll position. Provides an `activate(id)`
23
+ * imperatively-callable helper used by click handlers — sets the
24
+ * active id immediately and suppresses the scroll-driven update until
25
+ * the smooth-scroll completes (`scrollend`, with a timeout fallback
26
+ * for older Safari).
27
+ */
28
+ declare function useActiveHeading(headings: readonly HeadingItem[]): UseActiveHeadingResult;
29
+
30
+ export { type UseActiveHeadingResult, getActiveHeadingId, useActiveHeading };
@@ -0,0 +1,106 @@
1
+ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
2
+ const SCROLL_MARGIN_TOP = 80;
3
+ const DEBOUNCE_MS = 200;
4
+ function getActiveHeadingId(headingIds, elementMap, viewportHeight) {
5
+ if (headingIds.length === 0) return null;
6
+ let firstVisibleIndex = -1;
7
+ for (let i = 0; i < headingIds.length; i++) {
8
+ const el2 = elementMap.get(headingIds[i]);
9
+ if (!el2) continue;
10
+ const { top } = el2.getBoundingClientRect();
11
+ if (top >= SCROLL_MARGIN_TOP) {
12
+ firstVisibleIndex = i;
13
+ break;
14
+ }
15
+ }
16
+ if (firstVisibleIndex === -1) {
17
+ return headingIds[headingIds.length - 1] ?? null;
18
+ }
19
+ if (firstVisibleIndex === 0) {
20
+ const el2 = elementMap.get(headingIds[0]);
21
+ if (el2) {
22
+ const { top } = el2.getBoundingClientRect();
23
+ if (top < viewportHeight / 2) {
24
+ return headingIds[0] ?? null;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ const el = elementMap.get(headingIds[firstVisibleIndex]);
30
+ if (el) {
31
+ const { top } = el.getBoundingClientRect();
32
+ if (top < viewportHeight / 2) {
33
+ return headingIds[firstVisibleIndex] ?? null;
34
+ }
35
+ }
36
+ return headingIds[firstVisibleIndex - 1] ?? null;
37
+ }
38
+ function useActiveHeading(headings) {
39
+ const [activeId, setActiveId] = useState(null);
40
+ const headingIdsRef = useRef([]);
41
+ const elementMapRef = useRef(/* @__PURE__ */ new Map());
42
+ const suppressedRef = useRef(false);
43
+ const activate = useCallback((id) => {
44
+ setActiveId(id);
45
+ suppressedRef.current = true;
46
+ setTimeout(() => {
47
+ suppressedRef.current = false;
48
+ }, 2e3);
49
+ }, []);
50
+ useEffect(() => {
51
+ const ids = headings.map((h) => h.slug);
52
+ headingIdsRef.current = ids;
53
+ const map = /* @__PURE__ */ new Map();
54
+ for (const id of ids) {
55
+ const el = document.getElementById(id);
56
+ if (el) map.set(id, el);
57
+ }
58
+ elementMapRef.current = map;
59
+ let timerId = null;
60
+ function update() {
61
+ timerId = null;
62
+ setActiveId(
63
+ getActiveHeadingId(
64
+ headingIdsRef.current,
65
+ elementMapRef.current,
66
+ window.innerHeight
67
+ )
68
+ );
69
+ }
70
+ let fallbackTimerId = null;
71
+ function onScroll() {
72
+ if (suppressedRef.current) {
73
+ if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
74
+ fallbackTimerId = setTimeout(onScrollEnd, 1500);
75
+ return;
76
+ }
77
+ if (timerId !== null) clearTimeout(timerId);
78
+ timerId = setTimeout(update, DEBOUNCE_MS);
79
+ }
80
+ function onScrollEnd() {
81
+ suppressedRef.current = false;
82
+ if (fallbackTimerId !== null) {
83
+ clearTimeout(fallbackTimerId);
84
+ fallbackTimerId = null;
85
+ }
86
+ if (timerId !== null) clearTimeout(timerId);
87
+ update();
88
+ }
89
+ update();
90
+ window.addEventListener("scroll", onScroll, { passive: true });
91
+ window.addEventListener("resize", onScroll, { passive: true });
92
+ window.addEventListener("scrollend", onScrollEnd, { passive: true });
93
+ return () => {
94
+ window.removeEventListener("scroll", onScroll);
95
+ window.removeEventListener("resize", onScroll);
96
+ window.removeEventListener("scrollend", onScrollEnd);
97
+ if (timerId !== null) clearTimeout(timerId);
98
+ if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
99
+ };
100
+ }, [headings]);
101
+ return { activeId, activate };
102
+ }
103
+ export {
104
+ getActiveHeadingId,
105
+ useActiveHeading
106
+ };
@@ -0,0 +1 @@
1
+ export { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT, onAfterNavigate, onBeforeNavigate } from './page-events.js';
@@ -0,0 +1,12 @@
1
+ import {
2
+ BEFORE_NAVIGATE_EVENT,
3
+ AFTER_NAVIGATE_EVENT,
4
+ onBeforeNavigate,
5
+ onAfterNavigate
6
+ } from "./page-events.js";
7
+ export {
8
+ AFTER_NAVIGATE_EVENT,
9
+ BEFORE_NAVIGATE_EVENT,
10
+ onAfterNavigate,
11
+ onBeforeNavigate
12
+ };