create-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 (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/create-zudo-doc.js +2 -0
  4. package/dist/api.d.ts +20 -0
  5. package/dist/api.js +13 -0
  6. package/dist/claude-md-gen.d.ts +2 -0
  7. package/dist/claude-md-gen.js +113 -0
  8. package/dist/cli.d.ts +39 -0
  9. package/dist/cli.js +157 -0
  10. package/dist/compose.d.ts +95 -0
  11. package/dist/compose.js +206 -0
  12. package/dist/constants.d.ts +20 -0
  13. package/dist/constants.js +224 -0
  14. package/dist/features/body-foot-util.d.ts +10 -0
  15. package/dist/features/body-foot-util.js +12 -0
  16. package/dist/features/claude-resources.d.ts +2 -0
  17. package/dist/features/claude-resources.js +6 -0
  18. package/dist/features/design-token-panel.d.ts +14 -0
  19. package/dist/features/design-token-panel.js +27 -0
  20. package/dist/features/doc-history.d.ts +9 -0
  21. package/dist/features/doc-history.js +11 -0
  22. package/dist/features/doc-tags.d.ts +19 -0
  23. package/dist/features/doc-tags.js +33 -0
  24. package/dist/features/footer-taglist.d.ts +14 -0
  25. package/dist/features/footer-taglist.js +17 -0
  26. package/dist/features/footer.d.ts +8 -0
  27. package/dist/features/footer.js +10 -0
  28. package/dist/features/i18n.d.ts +22 -0
  29. package/dist/features/i18n.js +41 -0
  30. package/dist/features/image-enlarge.d.ts +11 -0
  31. package/dist/features/image-enlarge.js +13 -0
  32. package/dist/features/index.d.ts +15 -0
  33. package/dist/features/index.js +53 -0
  34. package/dist/features/llms-txt.d.ts +11 -0
  35. package/dist/features/llms-txt.js +13 -0
  36. package/dist/features/search.d.ts +9 -0
  37. package/dist/features/search.js +11 -0
  38. package/dist/features/sidebar-resizer.d.ts +14 -0
  39. package/dist/features/sidebar-resizer.js +16 -0
  40. package/dist/features/sidebar-toggle.d.ts +13 -0
  41. package/dist/features/sidebar-toggle.js +15 -0
  42. package/dist/features/tag-governance.d.ts +14 -0
  43. package/dist/features/tag-governance.js +16 -0
  44. package/dist/features/tauri-dev.d.ts +2 -0
  45. package/dist/features/tauri-dev.js +25 -0
  46. package/dist/features/tauri.d.ts +11 -0
  47. package/dist/features/tauri.js +52 -0
  48. package/dist/features/versioning.d.ts +27 -0
  49. package/dist/features/versioning.js +43 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +150 -0
  52. package/dist/preset.d.ts +37 -0
  53. package/dist/preset.js +156 -0
  54. package/dist/prompts.d.ts +32 -0
  55. package/dist/prompts.js +248 -0
  56. package/dist/scaffold.d.ts +4 -0
  57. package/dist/scaffold.js +344 -0
  58. package/dist/settings-gen.d.ts +2 -0
  59. package/dist/settings-gen.js +237 -0
  60. package/dist/utils.d.ts +8 -0
  61. package/dist/utils.js +34 -0
  62. package/dist/zfb-config-gen.d.ts +19 -0
  63. package/dist/zfb-config-gen.js +222 -0
  64. package/package.json +65 -0
  65. package/templates/base/.htmlvalidate.json +5 -0
  66. package/templates/base/.zfb/doc-history-meta.json +1 -0
  67. package/templates/base/pages/404.tsx +55 -0
  68. package/templates/base/pages/_data.ts +179 -0
  69. package/templates/base/pages/_mdx-components.ts +249 -0
  70. package/templates/base/pages/docs/[...slug].tsx +448 -0
  71. package/templates/base/pages/index.tsx +158 -0
  72. package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
  73. package/templates/base/pages/lib/_category-nav.tsx +148 -0
  74. package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
  75. package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
  76. package/templates/base/pages/lib/_details.tsx +30 -0
  77. package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
  78. package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
  79. package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
  80. package/templates/base/pages/lib/_extract-headings.ts +81 -0
  81. package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
  82. package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
  83. package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
  84. package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
  85. package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
  86. package/templates/base/pages/lib/_math-block.tsx +63 -0
  87. package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
  88. package/templates/base/pages/lib/_preset-generator.tsx +81 -0
  89. package/templates/base/pages/lib/_search-widget-script.ts +388 -0
  90. package/templates/base/pages/lib/_search-widget.tsx +196 -0
  91. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
  92. package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
  93. package/templates/base/pages/lib/locale-merge.ts +58 -0
  94. package/templates/base/pages/lib/route-enumerators.ts +302 -0
  95. package/templates/base/pages/sitemap.xml.tsx +51 -0
  96. package/templates/base/plugins/connect-adapter.mjs +144 -0
  97. package/templates/base/plugins/copy-public-plugin.mjs +50 -0
  98. package/templates/base/plugins/search-index-plugin.mjs +54 -0
  99. package/templates/base/scripts/run-b4push.sh +102 -0
  100. package/templates/base/src/components/ai-chat-modal.tsx +15 -0
  101. package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
  102. package/templates/base/src/components/content/component-map.ts +25 -0
  103. package/templates/base/src/components/content/content-blockquote.tsx +16 -0
  104. package/templates/base/src/components/content/content-code.tsx +117 -0
  105. package/templates/base/src/components/content/content-link.tsx +83 -0
  106. package/templates/base/src/components/content/content-ol.tsx +19 -0
  107. package/templates/base/src/components/content/content-paragraph.tsx +10 -0
  108. package/templates/base/src/components/content/content-strong.tsx +16 -0
  109. package/templates/base/src/components/content/content-table.tsx +18 -0
  110. package/templates/base/src/components/content/content-ul.tsx +18 -0
  111. package/templates/base/src/components/content/heading-h2.tsx +26 -0
  112. package/templates/base/src/components/content/heading-h3.tsx +26 -0
  113. package/templates/base/src/components/content/heading-h4.tsx +26 -0
  114. package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
  115. package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
  116. package/templates/base/src/components/doc-history.tsx +18 -0
  117. package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
  118. package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
  119. package/templates/base/src/components/html-preview/preflight.ts +112 -0
  120. package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
  121. package/templates/base/src/components/image-enlarge.tsx +19 -0
  122. package/templates/base/src/components/mobile-toc.tsx +94 -0
  123. package/templates/base/src/components/preset-generator.tsx +14 -0
  124. package/templates/base/src/components/sidebar-toggle.tsx +98 -0
  125. package/templates/base/src/components/sidebar-tree.tsx +543 -0
  126. package/templates/base/src/components/site-tree-nav.tsx +233 -0
  127. package/templates/base/src/components/theme-toggle.tsx +93 -0
  128. package/templates/base/src/components/toc.tsx +63 -0
  129. package/templates/base/src/components/tree-nav-shared.tsx +71 -0
  130. package/templates/base/src/config/color-scheme-utils.ts +182 -0
  131. package/templates/base/src/config/color-schemes.ts +128 -0
  132. package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
  133. package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
  134. package/templates/base/src/config/i18n.ts +225 -0
  135. package/templates/base/src/config/settings-types.ts +162 -0
  136. package/templates/base/src/config/sidebars.ts +66 -0
  137. package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
  138. package/templates/base/src/config/tag-vocabulary.ts +20 -0
  139. package/templates/base/src/hooks/use-active-heading.ts +133 -0
  140. package/templates/base/src/plugins/docs-source-map.ts +103 -0
  141. package/templates/base/src/plugins/hast-utils.ts +10 -0
  142. package/templates/base/src/plugins/rehype-code-title.ts +50 -0
  143. package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
  144. package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
  145. package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
  146. package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
  147. package/templates/base/src/plugins/remark-admonitions.ts +99 -0
  148. package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
  149. package/templates/base/src/plugins/url-utils.ts +4 -0
  150. package/templates/base/src/styles/global.css +1066 -0
  151. package/templates/base/src/types/docs-entry.ts +39 -0
  152. package/templates/base/src/types/heading.ts +5 -0
  153. package/templates/base/src/types/locale.ts +10 -0
  154. package/templates/base/src/utils/base.ts +139 -0
  155. package/templates/base/src/utils/content-files.ts +106 -0
  156. package/templates/base/src/utils/dedent.ts +24 -0
  157. package/templates/base/src/utils/docs.ts +335 -0
  158. package/templates/base/src/utils/git-info.ts +70 -0
  159. package/templates/base/src/utils/github.ts +19 -0
  160. package/templates/base/src/utils/header-right-items.ts +38 -0
  161. package/templates/base/src/utils/nav-scope.ts +63 -0
  162. package/templates/base/src/utils/sidebar.ts +104 -0
  163. package/templates/base/src/utils/slug.ts +10 -0
  164. package/templates/base/src/utils/smart-break.tsx +126 -0
  165. package/templates/base/src/utils/tags.ts +126 -0
  166. package/templates/base/tsconfig.json +36 -0
  167. package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
  168. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
  169. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
  170. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
  171. package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
  172. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
  173. package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
  174. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
  175. package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
  176. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
  177. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
  178. package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
  179. package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
  180. package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
  181. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
  182. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
  183. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
  184. package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
  185. package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
  186. package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
  187. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
  188. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
  189. package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
  190. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
  191. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
  192. package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
  193. package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
  194. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
  195. package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
  196. package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
  197. package/templates/features/tauri/files/src-tauri/build.rs +3 -0
  198. package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
  199. package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
  200. package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
  201. package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
  202. package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
  203. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
  204. package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
  205. package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
  206. package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
  207. package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
  208. package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
  209. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
  210. package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
  211. package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
  212. package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
@@ -0,0 +1,201 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Host-side body-end islands helper.
4
+ //
5
+ // Wave 8 (Path A — super-epic #1333 / child epic #1355) drops the local
6
+ // SSR-skip wrapper functions in `@takazudo/zudo-doc/ssr-skip` and uses
7
+ // zfb's native `<Island ssrFallback={...}>` API directly with the real
8
+ // component constructors imported by the host.
9
+ //
10
+ // The previous indirection (page → wrapper → placeholder div) created an
11
+ // orphan-component bug: the real components were `"use client"` modules
12
+ // that no page module ever imported transitively, so zfb's island scanner
13
+ // never walked page → real-component, the manifest never bound the marker
14
+ // to the real constructor, and the bundle never contained the real
15
+ // component body. PR #150 to zfb fixed the marker-name alignment but the
16
+ // orphan problem stayed — see issue zudolab/zudo-doc#1355 Wave 7 report.
17
+ //
18
+ // This helper is the page → real-component import chain the scanner
19
+ // needs. Each island below is composed with zfb's `<Island>` wrapper,
20
+ // which emits `<div data-zfb-island-skip-ssr="<ComponentName>">…</div>`
21
+ // at SSR (zfb's `captureComponentName` derives the marker from
22
+ // `child.displayName ?? child.name`). Because the page imports this
23
+ // file, and this file imports the real components, the scanner walks
24
+ // page → helper → real component and registers the constructor under
25
+ // the SSR marker name.
26
+ //
27
+ // Pattern mirrors `_header-with-defaults.tsx`: the JSX-shim widens
28
+ // `Island`'s return type to `unknown`, so call-sites cast through
29
+ // `as unknown as VNode` at the boundary.
30
+
31
+ import type { VNode, JSX } from "preact";
32
+ import { Island } from "@takazudo/zfb";
33
+ import { settings } from "@/config/settings";
34
+
35
+ import AiChatModal from "@/components/ai-chat-modal";
36
+ import ClientRouterBootstrap from "@/components/client-router-bootstrap";
37
+ import DesignTokenPanelBootstrap from "@/components/design-token-panel-bootstrap";
38
+ import ImageEnlarge, { ImageEnlargeSsrFallback } from "@/components/image-enlarge";
39
+ import { PageLoadingOverlay } from "@takazudo/zudo-doc/page-loading";
40
+
41
+ // Set explicit `displayName` on each default-exported island so zfb's
42
+ // `captureComponentName` produces a stable marker even after the SSR
43
+ // pipeline runs the components through a function-name-rewriting layer.
44
+ // The marker must match the third-arg literal that zfb's scanner records
45
+ // for the same source-level identifier (zfb PR #150). esbuild preserves
46
+ // function names by default, but the explicit assignment is a
47
+ // belt-and-braces guard for production minification regressions.
48
+ (AiChatModal as { displayName?: string }).displayName = "AiChatModal";
49
+ (ClientRouterBootstrap as { displayName?: string }).displayName =
50
+ "ClientRouterBootstrap";
51
+ (DesignTokenPanelBootstrap as { displayName?: string }).displayName =
52
+ "DesignTokenPanelBootstrap";
53
+ (ImageEnlarge as { displayName?: string }).displayName = "ImageEnlarge";
54
+
55
+ /**
56
+ * Default sr-only label rendered as the AiChatModal SSR fallback. This
57
+ * mirrors the body-label string the deleted `AiChatModalIsland` wrapper
58
+ * produced verbatim so assistive tech can discover the chat entrypoint
59
+ * in the static HTML before JS hydration. English-only for now — the
60
+ * previous default was also English-only; pass `aiChatBodyLabel` to
61
+ * localise.
62
+ */
63
+ const DEFAULT_AI_CHAT_BODY_LABEL = "Ask a question about the documentation.";
64
+
65
+ /**
66
+ * SSR-emitted inline script that acts as a pre-hydration shim for the
67
+ * `toggle-design-token-panel` window event. Because the
68
+ * DesignTokenPanelBootstrap Island is deferred, zdtp's real
69
+ * `toggle-design-token-panel` listener (registered in index.tsx at
70
+ * module init) is not yet installed when the user clicks the header
71
+ * palette button. This shim:
72
+ *
73
+ * 1. Records the first (and only meaningful) click as a boolean flag.
74
+ * 2. Exposes `window.__zdtpReadyClicks` so the bootstrap Island can
75
+ * drain the queue and re-dispatch a single event once the real
76
+ * listener is live.
77
+ * 3. Guards against double-installation across any re-evaluation path
78
+ * (SPA body swap, HMR, etc.) via `__zdtpToggleShimInstalled`.
79
+ *
80
+ * A single boolean (not an array) is used because the panel is a toggle —
81
+ * any number of pre-hydration clicks should result in at most one open.
82
+ */
83
+ const ZDTP_TOGGLE_SHIM_SRC = `(function(){
84
+ if(window.__zdtpToggleShimInstalled)return;
85
+ window.__zdtpToggleShimInstalled=true;
86
+ var pending=false;
87
+ function shim(){pending=true;}
88
+ window.addEventListener('toggle-design-token-panel',shim);
89
+ window.__zdtpReadyClicks=function(){
90
+ window.removeEventListener('toggle-design-token-panel',shim);
91
+ delete window.__zdtpReadyClicks;
92
+ if(pending){pending=false;window.dispatchEvent(new CustomEvent('toggle-design-token-panel'));}
93
+ };
94
+ })();`;
95
+
96
+ /** Props for {@link BodyEndIslands}. */
97
+ export interface BodyEndIslandsProps {
98
+ /** Base path the AI chat modal uses to construct API URLs. */
99
+ basePath: string;
100
+ /**
101
+ * Sr-only label rendered as the AiChatModal SSR fallback. Defaults to
102
+ * the English string. Pass a locale-translated string for non-default
103
+ * locales so screen readers announce the chat entrypoint correctly
104
+ * before hydration.
105
+ */
106
+ aiChatBodyLabel?: string;
107
+ }
108
+
109
+ /**
110
+ * The three default body-end islands every doc page mounts: the
111
+ * design-token tweak panel (overlay, fixed-position), the AI chat
112
+ * modal (`<dialog>` overlay), and the image-enlarge dialog (mounted
113
+ * lazily based on viewport scan).
114
+ *
115
+ * Each island is wrapped in `<Island ssrFallback>` so the heavy
116
+ * component is NOT evaluated server-side — they depend on
117
+ * `dialog.showModal()`, `localStorage`, `ResizeObserver`, runtime
118
+ * fetch, etc. The hydration runtime swaps each placeholder on the
119
+ * client.
120
+ *
121
+ * The `<h2 class="sr-only">AI Assistant</h2>` heading is emitted in
122
+ * the SSG output so screen readers and crawlers can discover the chat
123
+ * section landmark before JS hydration.
124
+ */
125
+ export function BodyEndIslands({
126
+ basePath,
127
+ aiChatBodyLabel = DEFAULT_AI_CHAT_BODY_LABEL,
128
+ }: BodyEndIslandsProps): JSX.Element {
129
+ // Hydrates first (when="load") so the SPA-router click intercept is
130
+ // registered as soon as the islands runtime mounts the marker. The
131
+ // component renders nothing visually — the island bundle's top-level
132
+ // `import "@takazudo/zfb-runtime/client-router"` is what actually
133
+ // wires up the router (zudolab/zudo-doc#1524 W7A fix).
134
+ const clientRouterBootstrap = Island({
135
+ when: "load",
136
+ children: <ClientRouterBootstrap />,
137
+ }) as unknown as VNode;
138
+
139
+ // Hydrates on load so configurePanel() runs as early as possible and
140
+ // the `toggle-design-token-panel` window listener is registered before
141
+ // the user can click the header trigger. Renders nothing visually —
142
+ // the zdtp panel self-mounts as a side-effect (zudolab/zudo-doc#1623).
143
+ //
144
+ // The inline <script> emitted alongside the Island is the pre-hydration
145
+ // toggle shim (zudolab/zudo-doc#1627 Part B). It captures the first
146
+ // click as a boolean flag and exposes window.__zdtpReadyClicks so the
147
+ // bootstrap module can drain and re-dispatch once the real zdtp listener
148
+ // is registered. Mirrors the PageLoadingOverlay SSR-script pattern.
149
+ const designTokenPanelBootstrap =
150
+ settings.designTokenPanel || settings.colorTweakPanel
151
+ ? (
152
+ <>
153
+ <script
154
+ dangerouslySetInnerHTML={{ __html: ZDTP_TOGGLE_SHIM_SRC }}
155
+ />
156
+ {Island({
157
+ when: "load",
158
+ children: <DesignTokenPanelBootstrap />,
159
+ }) as unknown as VNode}
160
+ </>
161
+ )
162
+ : null;
163
+
164
+ // Use a visually-hidden paragraph as the AiChatModal SSR fallback so
165
+ // the body label is present in static HTML for screen readers before
166
+ // JS hydration. sr-only keeps it invisible to sighted users.
167
+ const aiChat = Island({
168
+ ssrFallback: <p class="sr-only">{aiChatBodyLabel}</p>,
169
+ children: <AiChatModal basePath={basePath} />,
170
+ }) as unknown as VNode;
171
+
172
+ // Wave 11 (zudolab/zudo-doc#1355): the SSR fallback is the empty,
173
+ // closed `<dialog class="zd-enlarge-dialog ...">` shell so the dist
174
+ // HTML carries one dialog from the start. Without this the smoke
175
+ // "exactly one zd-enlarge-dialog element" assertion sees zero
176
+ // (skip-ssr placeholders are empty divs) and the no-JS path has no
177
+ // dialog at all. Hydration replaces this shell with the real
178
+ // ImageEnlarge component when the page goes idle.
179
+ const imageEnlarge = Island({
180
+ when: "idle",
181
+ ssrFallback: <ImageEnlargeSsrFallback />,
182
+ children: <ImageEnlarge />,
183
+ }) as unknown as VNode;
184
+
185
+ return (
186
+ <>
187
+ {/* Pure SSR — no Island wrap. The component emits its overlay div,
188
+ inline styles, and a small inline script that self-wires
189
+ zfb:before-preparation / zfb:after-swap listeners at runtime. */}
190
+ <PageLoadingOverlay />
191
+ {clientRouterBootstrap}
192
+ {designTokenPanelBootstrap}
193
+ {/* Emits the "AI Assistant" heading in the SSG output so screen
194
+ readers can discover the chat section landmark before JS
195
+ hydration. */}
196
+ <h2 class="sr-only">AI Assistant</h2>
197
+ {aiChat}
198
+ {imageEnlarge}
199
+ </>
200
+ );
201
+ }
@@ -0,0 +1,148 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Host-side MDX wrapper for <CategoryNav category="..." />.
4
+ //
5
+ // Mirrors the data-resolution shape of src/components/category-nav.astro:
6
+ // 1. Load docs for the active locale (or defaultLocale when not passed).
7
+ // 2. Build the nav tree with buildNavTree().
8
+ // 3. Find the target category node via findNode().
9
+ // 4. Filter to children with hasPage === true.
10
+ // 5. Forward the resolved children to the v2 CategoryNav component.
11
+ //
12
+ // All data access is synchronous (ADR-004 zfb content snapshot contract)
13
+ // via loadDocs() from pages/_data.ts.
14
+ //
15
+ // The `lang` prop is injected by createMdxComponents() in
16
+ // pages/_mdx-components.ts so locale routes get locale-aware nav data.
17
+
18
+ import type { JSX } from "preact";
19
+ import { CategoryNav as CategoryNavV2 } from "@takazudo/zudo-doc/nav-indexing";
20
+ import type { NavNode as V2NavNode } from "@takazudo/zudo-doc/nav-indexing/types";
21
+ import {
22
+ buildNavTree,
23
+ findNode,
24
+ loadCategoryMeta,
25
+ isNavVisible,
26
+ } from "@/utils/docs";
27
+ import { settings } from "@/config/settings";
28
+ import { defaultLocale, type Locale } from "@/config/i18n";
29
+ import { docsUrl } from "@/utils/base";
30
+ import { loadDocs } from "../_data";
31
+
32
+ export interface CategoryNavWrapperProps {
33
+ /**
34
+ * Slug of the category whose immediate children should be listed, e.g.
35
+ * "getting-started" or "guides/layout-demos".
36
+ */
37
+ category?: string;
38
+ /**
39
+ * Explicit list of top-level category slugs to render as cards. Use this
40
+ * when the target categories are not children of a single parent node in
41
+ * the nav tree (e.g. "claude-md", "claude-skills" are top-level siblings
42
+ * of "claude", not children). Each slug is resolved to its nav node; nodes
43
+ * not found in the tree are silently skipped.
44
+ *
45
+ * For nodes with noPage=true (no index.mdx), the href falls back to the
46
+ * auto-generated category index URL via docsUrl(slug, lang).
47
+ */
48
+ categories?: string[];
49
+ /**
50
+ * Active locale. Injected via createMdxComponents() closure.
51
+ * Defaults to defaultLocale when not provided.
52
+ */
53
+ lang?: Locale | string;
54
+ /** Optional extra CSS classes forwarded to the <nav> element. */
55
+ class?: string;
56
+ }
57
+
58
+ /**
59
+ * Load merged docs + categoryMeta for the given locale.
60
+ * Mirrors the locale-merge strategy from _header-with-defaults.tsx:
61
+ * default locale → "docs"; non-default → locale-first + EN fallback.
62
+ */
63
+ function loadNavSource(
64
+ locale: string,
65
+ ): { docs: ReturnType<typeof loadDocs>; categoryMeta: Map<string, import("@/utils/docs").CategoryMeta> } {
66
+ if (locale === defaultLocale) {
67
+ return {
68
+ docs: loadDocs("docs").filter((d) => !d.data.draft),
69
+ categoryMeta: loadCategoryMeta(settings.docsDir),
70
+ };
71
+ }
72
+
73
+ // Non-default locale: locale-first merge with EN fallback.
74
+ const localeDocs = loadDocs(`docs-${locale}`).filter((d) => !d.data.draft);
75
+ const baseDocs = loadDocs("docs").filter((d) => !d.data.draft);
76
+ const localeSlugSet = new Set(localeDocs.map((d) => d.data.slug ?? d.id));
77
+ const fallbackDocs = baseDocs.filter(
78
+ (d) => !localeSlugSet.has(d.data.slug ?? d.id),
79
+ );
80
+
81
+ const localeDir =
82
+ (settings.locales as Record<string, { dir?: string }>)[locale]?.dir ??
83
+ settings.docsDir;
84
+ const categoryMeta = new Map([
85
+ ...loadCategoryMeta(settings.docsDir),
86
+ ...loadCategoryMeta(localeDir),
87
+ ]);
88
+
89
+ return { docs: [...localeDocs, ...fallbackDocs], categoryMeta };
90
+ }
91
+
92
+ /**
93
+ * MDX wrapper for CategoryNav. Resolves nav tree data host-side and forwards
94
+ * the resolved category children into the v2 CategoryNav component.
95
+ *
96
+ * Supports two modes:
97
+ * - `category`: resolves immediate children of a single category node.
98
+ * - `categories`: resolves an explicit list of top-level slugs as cards.
99
+ * Use this when the target categories are siblings in the nav tree rather
100
+ * than children of a common parent (e.g. claude-md / claude-skills are
101
+ * top-level peers of claude, not children of it). Nodes with noPage=true
102
+ * get their href computed via docsUrl() since auto-index pages exist.
103
+ *
104
+ * Returns null when no visible children are resolved.
105
+ */
106
+ export function CategoryNavWrapper({
107
+ category,
108
+ categories,
109
+ lang = defaultLocale,
110
+ class: className,
111
+ }: CategoryNavWrapperProps): JSX.Element | null {
112
+ const locale = lang as Locale;
113
+
114
+ const { docs, categoryMeta } = loadNavSource(locale);
115
+ const navDocs = docs.filter(isNavVisible);
116
+ const tree = buildNavTree(navDocs, locale, categoryMeta);
117
+
118
+ let children: V2NavNode[];
119
+
120
+ if (categories !== undefined) {
121
+ // Explicit slug list mode: resolve each slug to its nav node and build
122
+ // a card for it. noPage nodes have no href in the tree but their
123
+ // auto-generated category index page is reachable via docsUrl().
124
+ children = categories
125
+ .map((slug): V2NavNode | null => {
126
+ const node = findNode(tree, slug);
127
+ if (!node) return null;
128
+ const href = node.href ?? docsUrl(slug, locale);
129
+ return {
130
+ label: node.label,
131
+ description: node.description,
132
+ href,
133
+ hasPage: true,
134
+ children: [],
135
+ };
136
+ })
137
+ .filter((n): n is V2NavNode => n !== null);
138
+ } else if (category !== undefined) {
139
+ const categoryNode = findNode(tree, category);
140
+ children = (categoryNode?.children.filter((c) => c.hasPage) ?? []) as V2NavNode[];
141
+ } else {
142
+ return null;
143
+ }
144
+
145
+ if (children.length === 0) return null;
146
+
147
+ return <CategoryNavV2 children={children} class={className} />;
148
+ }
@@ -0,0 +1,104 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Host-side MDX wrapper for <CategoryTreeNav category="..." />.
4
+ //
5
+ // Mirrors the data-resolution shape of src/components/category-tree-nav.astro:
6
+ // 1. Load docs for the active locale (defaultLocale when not passed).
7
+ // 2. Build the full nav tree with buildNavTree() + groupSatelliteNodes()
8
+ // (category slug is passed as the grouping prefix list).
9
+ // 3. Find the target category node via findNode().
10
+ // 4. Filter to children with hasPage === true or children.length > 0.
11
+ // 5. Forward the resolved children to the v2 CategoryTreeNav component.
12
+ //
13
+ // All data access is synchronous (ADR-004 zfb content snapshot contract).
14
+ // The `lang` prop is injected by createMdxComponents() in
15
+ // pages/_mdx-components.ts so locale routes get locale-aware nav data.
16
+
17
+ import type { JSX } from "preact";
18
+ import { CategoryTreeNav as CategoryTreeNavV2 } from "@takazudo/zudo-doc/nav-indexing";
19
+ import {
20
+ buildNavTree,
21
+ groupSatelliteNodes,
22
+ findNode,
23
+ loadCategoryMeta,
24
+ isNavVisible,
25
+ } from "@/utils/docs";
26
+ import { settings } from "@/config/settings";
27
+ import { defaultLocale, type Locale } from "@/config/i18n";
28
+ import { loadDocs } from "../_data";
29
+
30
+ export interface CategoryTreeNavWrapperProps {
31
+ /**
32
+ * Slug of the category whose children should be rendered as a tree,
33
+ * e.g. "guides" or "getting-started".
34
+ */
35
+ category: string;
36
+ /**
37
+ * Active locale. Injected via createMdxComponents() closure.
38
+ * Defaults to defaultLocale when not provided.
39
+ */
40
+ lang?: Locale | string;
41
+ }
42
+
43
+ /**
44
+ * Load merged docs + categoryMeta for the given locale.
45
+ * Matches the locale-merge strategy used by _category-nav.tsx.
46
+ */
47
+ function loadNavSource(
48
+ locale: string,
49
+ ): { docs: ReturnType<typeof loadDocs>; categoryMeta: Map<string, import("@/utils/docs").CategoryMeta> } {
50
+ if (locale === defaultLocale) {
51
+ return {
52
+ docs: loadDocs("docs").filter((d) => !d.data.draft),
53
+ categoryMeta: loadCategoryMeta(settings.docsDir),
54
+ };
55
+ }
56
+
57
+ const localeDocs = loadDocs(`docs-${locale}`).filter((d) => !d.data.draft);
58
+ const baseDocs = loadDocs("docs").filter((d) => !d.data.draft);
59
+ const localeSlugSet = new Set(localeDocs.map((d) => d.data.slug ?? d.id));
60
+ const fallbackDocs = baseDocs.filter(
61
+ (d) => !localeSlugSet.has(d.data.slug ?? d.id),
62
+ );
63
+
64
+ const localeDir =
65
+ (settings.locales as Record<string, { dir?: string }>)[locale]?.dir ??
66
+ settings.docsDir;
67
+ const categoryMeta = new Map([
68
+ ...loadCategoryMeta(settings.docsDir),
69
+ ...loadCategoryMeta(localeDir),
70
+ ]);
71
+
72
+ return { docs: [...localeDocs, ...fallbackDocs], categoryMeta };
73
+ }
74
+
75
+ /**
76
+ * MDX wrapper for CategoryTreeNav. Resolves nav tree data host-side and
77
+ * forwards the resolved category children into the v2 CategoryTreeNav
78
+ * component.
79
+ *
80
+ * Returns null when the category is not found or has no renderable children —
81
+ * matching the original Astro component's guard.
82
+ */
83
+ export function CategoryTreeNavWrapper({
84
+ category,
85
+ lang = defaultLocale,
86
+ }: CategoryTreeNavWrapperProps): JSX.Element | null {
87
+ const locale = lang as Locale;
88
+
89
+ const { docs, categoryMeta } = loadNavSource(locale);
90
+ const navDocs = docs.filter(isNavVisible);
91
+ const rawTree = buildNavTree(navDocs, locale, categoryMeta);
92
+ // groupSatelliteNodes with [category] groups satellite nodes under the
93
+ // target category — matching the original Astro component.
94
+ const tree = groupSatelliteNodes(rawTree, [category]);
95
+
96
+ const categoryNode = findNode(tree, category);
97
+ const children =
98
+ categoryNode?.children.filter((c) => c.hasPage || c.children.length > 0) ??
99
+ [];
100
+
101
+ if (children.length === 0) return null;
102
+
103
+ return <CategoryTreeNavV2 children={children} />;
104
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Compose the canonical "<title> | <siteName>" page-title shape used by
3
+ * both <title> (emitted by DocLayout) and og:title (emitted by
4
+ * HeadWithDefaults).
5
+ *
6
+ * Why this exists: the legacy Astro layout synthesised this suffix
7
+ * inline. The zfb DocLayout shell intentionally renders only what the
8
+ * host passes as `title`, so the host has to compose the suffix itself.
9
+ * Centralising the composition in one helper keeps every host call
10
+ * site in sync and matches the SEO/UX-recognised shape the original
11
+ * site shipped (also asserted by smoke-seo.spec.ts).
12
+ *
13
+ * Edge cases:
14
+ * - When `title` is identical to `settings.siteName` (e.g. the home
15
+ * page already passes `settings.siteName` as the title), do NOT
16
+ * duplicate — return just the bare site name. Mirrors the legacy
17
+ * Astro behaviour.
18
+ * - When `siteName` is missing/empty (defensive — settings.ts always
19
+ * has it in practice), fall back to the bare title.
20
+ *
21
+ */
22
+ import { settings } from "@/config/settings";
23
+
24
+ export function composeMetaTitle(title: string): string {
25
+ const siteName = settings.siteName;
26
+ if (!siteName) return title;
27
+ if (title === siteName) return siteName;
28
+ return `${title} | ${siteName}`;
29
+ }
@@ -0,0 +1,30 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // Host-side MDX wrapper for <Details> — trivial passthrough to the v2
4
+ // Details component.
5
+ //
6
+ // The legacy src/components/details.astro used Astro's <slot /> for children;
7
+ // v2's Details accepts standard Preact children. MDX passes slot content as
8
+ // `children`, so the mapping is direct. The title prop (default "Details")
9
+ // is forwarded unchanged.
10
+
11
+ import type { ComponentChildren, VNode } from "preact";
12
+ import { Details as DetailsV2 } from "@takazudo/zudo-doc/details";
13
+
14
+ export interface DetailsWrapperProps {
15
+ /** Summary label shown in the <summary> element. Defaults to "Details". */
16
+ title?: string;
17
+ /** MDX slot content rendered inside the collapsed body. */
18
+ children?: ComponentChildren;
19
+ }
20
+
21
+ /**
22
+ * Passthrough wrapper for the v2 Details component.
23
+ *
24
+ * Used in pages/_mdx-components.ts as the Details binding so that MDX
25
+ * content using `<Details title="...">...</Details>` renders correctly
26
+ * on zfb routes.
27
+ */
28
+ export function DetailsWrapper({ title, children }: DetailsWrapperProps): VNode {
29
+ return <DetailsV2 title={title}>{children}</DetailsV2>;
30
+ }