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,248 @@
1
+ import { useState, useEffect, useRef } from "preact/compat";
2
+ import type { JSX } from "preact";
3
+
4
+ interface ImageData {
5
+ src: string;
6
+ currentSrc: string;
7
+ srcset?: string;
8
+ sizes?: string;
9
+ alt: string;
10
+ naturalWidth: number;
11
+ naturalHeight: number;
12
+ }
13
+
14
+ // Shared shell for the enlarge `<dialog>`. The hydrated component and the
15
+ // SSR fallback (below) render into the same Island container, so they MUST
16
+ // agree on class string and inline style — otherwise the dist HTML and the
17
+ // post-hydration DOM disagree on size / position and the first interaction
18
+ // flashes. Sourcing both from the same constants closes the drift gap.
19
+ const DIALOG_CLASS =
20
+ "zd-enlarge-dialog mx-auto max-h-[90vh] max-w-[90vw] overflow-hidden border border-muted bg-surface p-0";
21
+ const DIALOG_STYLE = {
22
+ position: "fixed",
23
+ top: "50%",
24
+ left: "50%",
25
+ transform: "translate(-50%, -50%)",
26
+ } as const;
27
+
28
+ export default function ImageEnlarge() {
29
+ const [imgData, setImgData] = useState<ImageData | null>(null);
30
+ const dialogRef = useRef<HTMLDialogElement>(null);
31
+
32
+ // Eligibility detection: toggle .zd-enlarge-btn[hidden] per image
33
+ useEffect(() => {
34
+ const resizeObservers = new Map<HTMLImageElement, ResizeObserver>();
35
+ let mutationObserver: MutationObserver | null = null;
36
+ let resizeTimer = 0;
37
+
38
+ function evaluateEligibility(img: HTMLImageElement) {
39
+ const container = img.closest(".zd-enlargeable");
40
+ if (!container) return;
41
+ const btn = container.querySelector(".zd-enlarge-btn") as HTMLElement | null;
42
+ if (!btn) return;
43
+ const eligible = img.naturalWidth > img.clientWidth * window.devicePixelRatio;
44
+ if (eligible) {
45
+ btn.removeAttribute("hidden");
46
+ } else {
47
+ btn.setAttribute("hidden", "");
48
+ }
49
+ }
50
+
51
+ function observeImage(img: HTMLImageElement) {
52
+ if (resizeObservers.has(img)) return;
53
+ const ro = new ResizeObserver(() => evaluateEligibility(img));
54
+ ro.observe(img);
55
+ resizeObservers.set(img, ro);
56
+ if (img.complete) {
57
+ evaluateEligibility(img);
58
+ } else {
59
+ img.addEventListener("load", () => evaluateEligibility(img), { once: true });
60
+ }
61
+ }
62
+
63
+ function scanContent() {
64
+ const scope = document.querySelector("main .zd-content");
65
+ if (!scope) return;
66
+ scope.querySelectorAll<HTMLImageElement>(".zd-enlargeable img").forEach(observeImage);
67
+ }
68
+
69
+ function startObserving() {
70
+ const scope = document.querySelector("main .zd-content");
71
+ if (scope) {
72
+ mutationObserver = new MutationObserver((mutations) => {
73
+ for (const mutation of mutations) {
74
+ for (const node of mutation.addedNodes) {
75
+ if (!(node instanceof Element)) continue;
76
+ if (node.matches(".zd-enlargeable")) {
77
+ node.querySelectorAll<HTMLImageElement>("img").forEach(observeImage);
78
+ }
79
+ node.querySelectorAll<HTMLImageElement>(".zd-enlargeable img").forEach(observeImage);
80
+ }
81
+ }
82
+ });
83
+ mutationObserver.observe(scope, { childList: true, subtree: true });
84
+ }
85
+ scanContent();
86
+ }
87
+
88
+ function handleWindowResize() {
89
+ clearTimeout(resizeTimer);
90
+ resizeTimer = window.setTimeout(() => {
91
+ resizeObservers.forEach((_, img) => evaluateEligibility(img));
92
+ }, 150);
93
+ }
94
+
95
+ function handleAfterSwap() {
96
+ resizeObservers.forEach((ro) => ro.disconnect());
97
+ resizeObservers.clear();
98
+ mutationObserver?.disconnect();
99
+ mutationObserver = null;
100
+ startObserving();
101
+ }
102
+
103
+ startObserving();
104
+ window.addEventListener("resize", handleWindowResize);
105
+ document.addEventListener("DOMContentLoaded", handleAfterSwap);
106
+
107
+ return () => {
108
+ resizeObservers.forEach((ro) => ro.disconnect());
109
+ resizeObservers.clear();
110
+ mutationObserver?.disconnect();
111
+ window.removeEventListener("resize", handleWindowResize);
112
+ document.removeEventListener("DOMContentLoaded", handleAfterSwap);
113
+ clearTimeout(resizeTimer);
114
+ };
115
+ }, []);
116
+
117
+ useEffect(() => {
118
+ function handleDocumentClick(e: MouseEvent) {
119
+ const target = e.target as Element;
120
+ const container = target.closest(".zd-enlargeable");
121
+ if (!container) return;
122
+ const btn = container.querySelector(".zd-enlarge-btn") as HTMLElement | null;
123
+ // Eligibility gate: only open when the expand button is visible (image is large enough).
124
+ if (!btn || btn.hasAttribute("hidden")) return;
125
+ const img = container.querySelector("img") as HTMLImageElement | null;
126
+ if (!img) return;
127
+ setImgData({
128
+ src: img.src,
129
+ currentSrc: img.currentSrc,
130
+ srcset: img.srcset || undefined,
131
+ sizes: img.sizes || undefined,
132
+ alt: img.alt,
133
+ naturalWidth: img.naturalWidth,
134
+ naturalHeight: img.naturalHeight,
135
+ });
136
+ }
137
+ document.addEventListener("click", handleDocumentClick);
138
+ return () => document.removeEventListener("click", handleDocumentClick);
139
+ }, []);
140
+
141
+ // Open dialog when imgData is set
142
+ useEffect(() => {
143
+ if (!imgData) return;
144
+ const dialog = dialogRef.current;
145
+ if (!dialog) return;
146
+ dialog.showModal();
147
+ }, [imgData]);
148
+
149
+ // Handle cancel event (ESC key)
150
+ useEffect(() => {
151
+ const dialog = dialogRef.current;
152
+ if (!dialog) return;
153
+ function handleCancel() {
154
+ setImgData(null);
155
+ }
156
+ dialog.addEventListener("cancel", handleCancel);
157
+ return () => dialog.removeEventListener("cancel", handleCancel);
158
+ }, []);
159
+
160
+ // Reset state when dialog closes
161
+ useEffect(() => {
162
+ const dialog = dialogRef.current;
163
+ if (!dialog) return;
164
+ function handleClose() {
165
+ setImgData(null);
166
+ }
167
+ dialog.addEventListener("close", handleClose);
168
+ return () => dialog.removeEventListener("close", handleClose);
169
+ }, []);
170
+
171
+ // Close and reset on ClientRouter navigation
172
+ useEffect(() => {
173
+ function handleAfterSwap() {
174
+ const dialog = dialogRef.current;
175
+ if (dialog?.open) dialog.close();
176
+ setImgData(null);
177
+ }
178
+ document.addEventListener("DOMContentLoaded", handleAfterSwap);
179
+ return () => document.removeEventListener("DOMContentLoaded", handleAfterSwap);
180
+ }, []);
181
+
182
+ function handleBackdropClick(e: JSX.TargetedMouseEvent<HTMLDialogElement>) {
183
+ const dialog = dialogRef.current;
184
+ if (!dialog) return;
185
+ const rect = dialog.getBoundingClientRect();
186
+ if (
187
+ e.clientX < rect.left ||
188
+ e.clientX > rect.right ||
189
+ e.clientY < rect.top ||
190
+ e.clientY > rect.bottom
191
+ ) {
192
+ dialog.close();
193
+ }
194
+ }
195
+
196
+ return (
197
+ <dialog
198
+ ref={dialogRef}
199
+ onClick={handleBackdropClick}
200
+ className={DIALOG_CLASS}
201
+ style={DIALOG_STYLE}
202
+ >
203
+ {imgData && (
204
+ <>
205
+ <div className="relative">
206
+ <img
207
+ src={imgData.currentSrc || imgData.src}
208
+ srcSet={imgData.srcset}
209
+ sizes={imgData.srcset ? "100vw" : undefined}
210
+ alt={imgData.alt}
211
+ className="block max-h-[85vh] max-w-[85vw] object-contain"
212
+ />
213
+ </div>
214
+ <button
215
+ type="button"
216
+ onClick={() => dialogRef.current?.close()}
217
+ className="zd-enlarge-dialog-close"
218
+ aria-label="Close enlarged image"
219
+ >
220
+ <svg viewBox="0 0 161.03 161.03" fill="currentColor" aria-hidden="true" focusable="false">
221
+ <polygon points="161.03 10.27 150.76 0 80.51 70.24 10.27 0 0 10.27 70.24 80.51 0 150.76 10.27 161.03 80.51 90.78 150.76 161.03 161.03 150.76 90.78 80.51 161.03 10.27" />
222
+ </svg>
223
+ </button>
224
+ </>
225
+ )}
226
+ </dialog>
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Static SSR fallback for the {@link ImageEnlarge} island.
232
+ *
233
+ * Renders an empty, closed `<dialog class="zd-enlarge-dialog ...">` so the
234
+ * dist HTML carries the dialog shell even before hydration. A `<dialog>`
235
+ * without `open` is `display:none` per UA stylesheet, so screen readers
236
+ * and crawlers see the same shape they would post-hydration. Sources its
237
+ * class and inline style from the shared `DIALOG_CLASS` / `DIALOG_STYLE`
238
+ * constants above so the SSR shell cannot drift from the hydrated
239
+ * dialog (a drift would surface as a cosmetic flash on first interaction).
240
+ */
241
+ export function ImageEnlargeSsrFallback() {
242
+ return (
243
+ <dialog
244
+ className={DIALOG_CLASS}
245
+ style={DIALOG_STYLE}
246
+ />
247
+ );
248
+ }
@@ -0,0 +1,74 @@
1
+ // zfb plugin module: llms-txt.
2
+ //
3
+ // Wires two lifecycle hooks for the llms-txt integration:
4
+ //
5
+ // postBuild — invokes `emitLlmsTxt` to write `dist/llms.txt`,
6
+ // `dist/llms-full.txt`, and the per-locale variants.
7
+ // `siteUrl` is normalised to `undefined` when falsy because
8
+ // the runner switches between absolute and root-relative URLs
9
+ // based on its presence (matches legacy Astro behaviour).
10
+ //
11
+ // devMiddleware — serves `/llms.txt`, `/llms-full.txt`, and the per-locale
12
+ // `/<code>/llms.txt` / `/<code>/llms-full.txt` variants from
13
+ // the on-the-fly `generateLlmsTxt` generator so dev output
14
+ // stays in lockstep with the production `emitLlmsTxt`
15
+ // byte-for-byte.
16
+ //
17
+ // `options` carries `{ siteName, siteDescription, base, siteUrl,
18
+ // defaultLocaleDir, locales }` from the matching entry in `zfb.config.ts`.
19
+ //
20
+ // Inline functions are not supported by zfb's plugin runtime; see the
21
+ // sibling `doc-history-plugin.mjs` for the rationale.
22
+
23
+ import { emitLlmsTxt, createLlmsTxtDevMiddleware } from "@takazudo/zudo-doc/integrations/llms-txt";
24
+ import { connectToZfbHandler } from "./connect-adapter.mjs";
25
+
26
+ export default {
27
+ name: "llms-txt",
28
+
29
+ postBuild(ctx) {
30
+ const {
31
+ siteName,
32
+ siteDescription,
33
+ base,
34
+ siteUrl,
35
+ defaultLocaleDir,
36
+ locales,
37
+ } = ctx.options;
38
+ emitLlmsTxt({
39
+ outDir: ctx.outDir,
40
+ siteName,
41
+ siteDescription,
42
+ base,
43
+ siteUrl: siteUrl || undefined,
44
+ defaultLocaleDir,
45
+ locales,
46
+ logger: ctx.logger,
47
+ });
48
+ },
49
+
50
+ devMiddleware(ctx) {
51
+ const middleware = createLlmsTxtDevMiddleware(ctx.options, ctx.logger);
52
+ const handler = connectToZfbHandler(middleware);
53
+
54
+ // zfb's `register(path, handler)` matches against the FULL request
55
+ // URL (no base-stripping). For a non-root base (e.g. "/my-docs/"),
56
+ // requests arrive as `/my-docs/llms.txt` (etc.), so we register
57
+ // every route with the base prefix. For base="/", the prefix is
58
+ // empty and routes are `/llms.txt` etc. as expected. The middleware
59
+ // accepts base-prefixed URLs via the matcher (see `matchLlmsRoute`
60
+ // in `dev-middleware.ts`).
61
+ const basePrefix = stripTrailingSlash(ctx.options.base ?? "");
62
+ ctx.register(`${basePrefix}/llms.txt`, handler);
63
+ ctx.register(`${basePrefix}/llms-full.txt`, handler);
64
+ for (const locale of ctx.options.locales ?? []) {
65
+ ctx.register(`${basePrefix}/${locale.code}/llms.txt`, handler);
66
+ ctx.register(`${basePrefix}/${locale.code}/llms-full.txt`, handler);
67
+ }
68
+ },
69
+ };
70
+
71
+ function stripTrailingSlash(s) {
72
+ if (typeof s !== "string" || s.length === 0) return "";
73
+ return s.endsWith("/") ? s.slice(0, -1) : s;
74
+ }
@@ -0,0 +1,185 @@
1
+ export function initSidebarResizer() {
2
+ const sidebar = document.getElementById("desktop-sidebar");
3
+ if (!sidebar || sidebar.querySelector("[data-sidebar-resizer]")) return;
4
+
5
+ // Resizer allows a wider range (192–448px) than the CSS default
6
+ // (clamp(14rem, 20vw, 22rem) = 224–352px at 16px base).
7
+ // CSS provides the responsive initial width; the resizer lets users
8
+ // go beyond that range when explicitly dragging or using keyboard arrows.
9
+ const MIN_W = 192;
10
+ const MAX_W = 448;
11
+ const STEP = 10;
12
+ const LS_KEY = "zudo-doc-sidebar-width";
13
+ const CSS_PROP = "--zd-sidebar-w";
14
+ const ACCENT_BG = "var(--zd-accent, rgba(128,128,128,0.3))";
15
+ const ACCENT_OUTLINE = "2px solid var(--zd-accent, rgba(128,128,128,0.5))";
16
+ const ACCENT_GHOST = "var(--zd-accent, rgba(128,128,128,0.5))";
17
+
18
+ function readCurrentWidth(): number {
19
+ const raw = getComputedStyle(document.documentElement).getPropertyValue(CSS_PROP);
20
+ return raw ? parseFloat(raw) || MIN_W : MIN_W;
21
+ }
22
+
23
+ let cachedWidth = readCurrentWidth();
24
+
25
+ const handle = document.createElement("div");
26
+ handle.setAttribute("data-sidebar-resizer", "");
27
+ handle.setAttribute("tabindex", "0");
28
+ handle.setAttribute("role", "separator");
29
+ handle.setAttribute("aria-orientation", "vertical");
30
+ handle.setAttribute("aria-label", "Resize sidebar");
31
+ handle.setAttribute("aria-valuemin", String(MIN_W));
32
+ handle.setAttribute("aria-valuemax", String(MAX_W));
33
+ handle.setAttribute("aria-valuenow", String(Math.round(cachedWidth)));
34
+ // 20px is wider than every common native y-scrollbar (~12-17px on
35
+ // Win/Linux classic; 0 on macOS overlay) so a draggable strip always remains
36
+ // visible to the LEFT of the scrollbar when sidebar content overflows.
37
+ // zudolab/zudo-doc#1660
38
+ Object.assign(handle.style, {
39
+ position: "absolute",
40
+ top: "0",
41
+ right: "0",
42
+ width: "20px",
43
+ height: "100%",
44
+ cursor: "col-resize",
45
+ zIndex: "10",
46
+ transition: "background 0.15s",
47
+ });
48
+
49
+ let dragging = false;
50
+
51
+ function applyWidth(w: number) {
52
+ cachedWidth = Math.max(MIN_W, Math.min(MAX_W, w));
53
+ document.documentElement.style.setProperty(CSS_PROP, cachedWidth + "px");
54
+ try { localStorage.setItem(LS_KEY, String(Math.round(cachedWidth))); } catch {}
55
+ handle.setAttribute("aria-valuenow", String(Math.round(cachedWidth)));
56
+ }
57
+
58
+ let focused = false;
59
+
60
+ function updateHandleVisual() {
61
+ if (dragging || focused) {
62
+ handle.style.background = ACCENT_BG;
63
+ } else {
64
+ handle.style.background = "";
65
+ }
66
+ handle.style.outline = focused && !dragging ? ACCENT_OUTLINE : "";
67
+ handle.style.outlineOffset = focused && !dragging ? "1px" : "";
68
+ }
69
+
70
+ handle.addEventListener("focus", () => {
71
+ focused = true;
72
+ updateHandleVisual();
73
+ });
74
+ handle.addEventListener("blur", () => {
75
+ focused = false;
76
+ updateHandleVisual();
77
+ });
78
+
79
+ handle.addEventListener("keydown", (e: KeyboardEvent) => {
80
+ let w = cachedWidth;
81
+ switch (e.key) {
82
+ case "ArrowLeft":
83
+ w = Math.max(MIN_W, w - STEP);
84
+ break;
85
+ case "ArrowRight":
86
+ w = Math.min(MAX_W, w + STEP);
87
+ break;
88
+ case "Home":
89
+ w = MIN_W;
90
+ break;
91
+ case "End":
92
+ w = MAX_W;
93
+ break;
94
+ default:
95
+ return;
96
+ }
97
+ e.preventDefault();
98
+ applyWidth(w);
99
+ });
100
+
101
+ handle.addEventListener("mouseenter", () => {
102
+ if (!dragging && !focused) handle.style.background = ACCENT_BG;
103
+ });
104
+ handle.addEventListener("mouseleave", () => {
105
+ if (!dragging && !focused) handle.style.background = "";
106
+ });
107
+
108
+ handle.addEventListener("pointerdown", (e: PointerEvent) => {
109
+ e.preventDefault();
110
+ handle.setPointerCapture(e.pointerId);
111
+ dragging = true;
112
+ updateHandleVisual();
113
+ document.documentElement.style.cursor = "col-resize";
114
+ document.documentElement.style.userSelect = "none";
115
+
116
+ // Ghost line — cheap to move (no reflow), shows target position
117
+ const ghost = document.createElement("div");
118
+ Object.assign(ghost.style, {
119
+ position: "fixed",
120
+ top: "0",
121
+ width: "2px",
122
+ height: "100vh",
123
+ background: ACCENT_GHOST,
124
+ pointerEvents: "none",
125
+ zIndex: "9999",
126
+ });
127
+ const sidebarRect = sidebar.getBoundingClientRect();
128
+ const sidebarLeft = sidebarRect.left;
129
+ ghost.style.left = (sidebarLeft + sidebarRect.width) + "px";
130
+ document.body.appendChild(ghost);
131
+ let targetWidth = 0;
132
+ let cleaned = false;
133
+
134
+ const onMove = (ev: PointerEvent) => {
135
+ targetWidth = Math.max(MIN_W, Math.min(MAX_W, ev.clientX - sidebarLeft));
136
+ ghost.style.left = (sidebarLeft + targetWidth) + "px";
137
+ };
138
+
139
+ const cleanup = () => {
140
+ if (cleaned) return;
141
+ cleaned = true;
142
+ dragging = false;
143
+ updateHandleVisual();
144
+ document.documentElement.style.cursor = "";
145
+ document.documentElement.style.userSelect = "";
146
+ ghost.remove();
147
+ handle.removeEventListener("pointermove", onMove);
148
+ handle.removeEventListener("pointerup", onUp);
149
+ handle.removeEventListener("pointercancel", onCancel);
150
+ handle.removeEventListener("lostpointercapture", onLost);
151
+ };
152
+
153
+ const commit = () => {
154
+ if (targetWidth > 0) applyWidth(targetWidth);
155
+ };
156
+
157
+ // pointerup: normal end-of-drag. Commit, then teardown.
158
+ const onUp = () => {
159
+ commit();
160
+ cleanup();
161
+ };
162
+
163
+ // lostpointercapture: per spec fires AFTER pointerup, but browsers reorder
164
+ // these in edge cases (cursor near y-scrollbar, fast drags, OS handoff).
165
+ // Commit here too so a real drag still applies if pointerup is dropped.
166
+ // Idempotent with onUp via the `cleaned` guard.
167
+ const onLost = () => {
168
+ commit();
169
+ cleanup();
170
+ };
171
+
172
+ // pointercancel: actual user/OS cancellation (touch interrupted, etc.).
173
+ // Do NOT commit — caller intent was to abort.
174
+ const onCancel = () => {
175
+ cleanup();
176
+ };
177
+
178
+ handle.addEventListener("pointermove", onMove);
179
+ handle.addEventListener("pointerup", onUp);
180
+ handle.addEventListener("pointercancel", onCancel);
181
+ handle.addEventListener("lostpointercapture", onLost);
182
+ });
183
+
184
+ sidebar.appendChild(handle);
185
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from 'preact/hooks';
4
+ import { BEFORE_NAVIGATE_EVENT, AFTER_NAVIGATE_EVENT } from '@takazudo/zudo-doc/transitions';
5
+
6
+ export const SIDEBAR_STORAGE_KEY = 'zudo-doc-sidebar-visible';
7
+
8
+ function readState(): boolean {
9
+ if (typeof window === 'undefined') return true;
10
+ try {
11
+ return localStorage.getItem(SIDEBAR_STORAGE_KEY) !== 'false';
12
+ } catch {
13
+ return true;
14
+ }
15
+ }
16
+
17
+ function setDataAttribute(isVisible: boolean) {
18
+ if (isVisible) {
19
+ document.documentElement.removeAttribute('data-sidebar-hidden');
20
+ } else {
21
+ document.documentElement.setAttribute('data-sidebar-hidden', '');
22
+ }
23
+ }
24
+
25
+ export default function DesktopSidebarToggle() {
26
+ // Initial state must match server render (always `true`) to avoid a
27
+ // hydration mismatch when the persisted preference is "hidden". The
28
+ // doc-layout's pre-paint inline script applies `data-sidebar-hidden`
29
+ // to <html> from localStorage *before* this island mounts, so the
30
+ // visual state stays correct; we only need to sync this island's
31
+ // React state to the persisted preference after hydration. Same
32
+ // pattern as src/components/theme-toggle.tsx (commit 9aebd8e).
33
+ const [visible, setVisible] = useState<boolean>(true);
34
+ // Tracks whether the hydration sync (below) has run. The persistence
35
+ // effect below skips the very first mount so we don't overwrite the
36
+ // user's persisted "hidden" preference with the SSR-safe default
37
+ // `true` before the hydration sync gets a chance to fire.
38
+ const hydrated = useRef(false);
39
+
40
+ // Persist state changes to localStorage and the <html> data-attribute.
41
+ // The `hydrated.current` guard is the real protection: it is still
42
+ // `false` on the very first effect run (the hydration-sync effect
43
+ // below sets it to `true` only after this one fires, since effects
44
+ // run in declaration order on mount), so the first run bails out
45
+ // and we don't clobber the user's persisted "hidden" preference
46
+ // with the SSR-safe default `true`.
47
+ useEffect(() => {
48
+ if (!hydrated.current) return;
49
+ setDataAttribute(visible);
50
+ try {
51
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, String(visible));
52
+ } catch {
53
+ // ignore storage errors
54
+ }
55
+ }, [visible]);
56
+
57
+ // After mount, read the persisted preference and reconcile state
58
+ // with the SSR default. Sets the ref so subsequent runs of the
59
+ // persistence effect above start syncing normally.
60
+ useEffect(() => {
61
+ hydrated.current = true;
62
+ const actual = readState();
63
+ if (actual !== visible) {
64
+ setVisible(actual);
65
+ }
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
67
+ }, []);
68
+
69
+ // Re-apply data-sidebar-hidden to <html> after every SPA nav.
70
+ // zfb's swapRootAttributes wipes all non-preserved <html> attributes on
71
+ // each navigation (data-sidebar-hidden is not in NON_OVERRIDABLE_ZFB_ATTRS),
72
+ // and the pre-paint inline script does not re-run on SPA nav. Since this
73
+ // island is persisted (data-zfb-transition-persist), this listener stays
74
+ // registered across SPA swaps.
75
+ //
76
+ // Strategy: capture the attribute presence just before the swap
77
+ // (BEFORE_NAVIGATE_EVENT fires before swapRootAttributes runs), then
78
+ // restore it once the swap completes (AFTER_NAVIGATE_EVENT). This is
79
+ // authoritative regardless of how the attribute was set (toggle click,
80
+ // localStorage, or external mutation). (#1551, #1552 B10)
81
+ useEffect(() => {
82
+ let wasHidden = false;
83
+ const capture = () => {
84
+ wasHidden = document.documentElement.hasAttribute('data-sidebar-hidden');
85
+ };
86
+ const restore = () => {
87
+ if (wasHidden) {
88
+ document.documentElement.setAttribute('data-sidebar-hidden', '');
89
+ }
90
+ // If not hidden, swapRootAttributes already cleared it — nothing to do.
91
+ };
92
+ document.addEventListener(BEFORE_NAVIGATE_EVENT, capture);
93
+ document.addEventListener(AFTER_NAVIGATE_EVENT, restore);
94
+ return () => {
95
+ document.removeEventListener(BEFORE_NAVIGATE_EVENT, capture);
96
+ document.removeEventListener(AFTER_NAVIGATE_EVENT, restore);
97
+ };
98
+ }, []);
99
+
100
+ return (
101
+ <button
102
+ type="button"
103
+ onClick={() => setVisible((v) => !v)}
104
+ className="zd-desktop-sidebar-toggle hidden lg:flex fixed bottom-vsp-xl z-40 items-center justify-center w-[1.5rem] h-[3rem] bg-surface border border-muted border-l-0 rounded-r-DEFAULT text-muted cursor-pointer transition-[left,color] duration-200 ease-in-out hover:text-fg"
105
+ aria-label={visible ? 'Hide sidebar' : 'Show sidebar'}
106
+ aria-pressed={visible}
107
+ data-zfb-transition-persist="desktop-sidebar-toggle"
108
+ >
109
+ <svg
110
+ xmlns="http://www.w3.org/2000/svg"
111
+ className="h-icon-sm w-icon-sm"
112
+ aria-hidden="true"
113
+ fill="none"
114
+ viewBox="0 0 24 24"
115
+ stroke="currentColor"
116
+ strokeWidth={2}
117
+ >
118
+ <path
119
+ strokeLinecap="round"
120
+ strokeLinejoin="round"
121
+ d={visible ? 'M15 19l-7-7 7-7' : 'M9 5l7 7-7 7'}
122
+ />
123
+ </svg>
124
+ </button>
125
+ );
126
+ }