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,108 @@
1
+ import { type VNode, useMemo } from "preact/compat";
2
+ import PreviewBase from "./preview-base";
3
+ import { dedent } from "@/utils/dedent";
4
+ import { preflightCss } from "./preflight";
5
+
6
+ interface HtmlPreviewProps {
7
+ html: string;
8
+ css?: string;
9
+ head?: string;
10
+ js?: string;
11
+ title?: string;
12
+ height?: number;
13
+ defaultOpen?: boolean;
14
+ /** Per-component css for code block display (before global merge) */
15
+ componentCss?: string;
16
+ /** Per-component head for code block display (before global merge) */
17
+ componentHead?: string;
18
+ /** Per-component js for code block display (before global merge) */
19
+ componentJs?: string;
20
+ }
21
+
22
+ function containsScript(head?: string, js?: string): boolean {
23
+ if (js) return true;
24
+ if (head && /<script/i.test(head)) return true;
25
+ return false;
26
+ }
27
+
28
+ function buildSrcdoc(
29
+ html: string,
30
+ css?: string,
31
+ head?: string,
32
+ js?: string,
33
+ ): string {
34
+ return `<!doctype html>
35
+ <html>
36
+ <head>
37
+ <meta charset="utf-8">
38
+ <meta name="viewport" content="width=device-width,initial-scale=1">
39
+ <style>${preflightCss}</style>
40
+ ${head ?? ""}
41
+ ${css ? `<style>${css}</style>` : ""}
42
+ </head>
43
+ <body>${html}
44
+ ${js ? `<script>${js}</script>` : ""}
45
+ </body>
46
+ </html>`;
47
+ }
48
+
49
+ export default function HtmlPreview({
50
+ html,
51
+ css,
52
+ head,
53
+ js,
54
+ title,
55
+ height,
56
+ defaultOpen,
57
+ componentCss,
58
+ componentHead,
59
+ componentJs,
60
+ }: HtmlPreviewProps): VNode {
61
+ const srcdoc = useMemo(
62
+ () => buildSrcdoc(html, css, head, js),
63
+ [html, css, head, js],
64
+ );
65
+ const hasScripts = containsScript(head, js);
66
+ const syncDelay = hasScripts ? 300 : 0;
67
+ // allow-same-origin is always required so that syncHeight can access
68
+ // iframe.contentDocument for auto-height measurement; sandbox="" without
69
+ // allow-same-origin gives the srcdoc iframe an opaque origin and blocks
70
+ // contentDocument reads even from the parent page.
71
+ const sandboxValue = hasScripts
72
+ ? "allow-scripts allow-same-origin"
73
+ : "allow-same-origin";
74
+
75
+ const codeBlocks = useMemo(
76
+ () => [
77
+ { language: "html", title: "HTML", code: dedent(html) },
78
+ ...(componentCss
79
+ ? [{ language: "css", title: "CSS", code: dedent(componentCss) }]
80
+ : []),
81
+ ...(componentHead
82
+ ? [{ language: "html", title: "Head", code: dedent(componentHead) }]
83
+ : []),
84
+ ...(componentJs
85
+ ? [
86
+ {
87
+ language: "javascript",
88
+ title: "JS",
89
+ code: dedent(componentJs),
90
+ },
91
+ ]
92
+ : []),
93
+ ],
94
+ [html, componentCss, componentHead, componentJs],
95
+ );
96
+
97
+ return (
98
+ <PreviewBase
99
+ title={title}
100
+ height={height}
101
+ srcdoc={srcdoc}
102
+ defaultOpen={defaultOpen}
103
+ sandbox={sandboxValue}
104
+ syncDelay={syncDelay}
105
+ codeBlocks={codeBlocks}
106
+ />
107
+ );
108
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tailwind CSS v4 preflight (modern-normalize/reset).
3
+ * Source: https://unpkg.com/tailwindcss@4/preflight.css
4
+ *
5
+ * The --theme() references are replaced with standard fallback values
6
+ * so the reset works standalone inside an iframe.
7
+ */
8
+ export const preflightCss = `
9
+ *,
10
+ ::after,
11
+ ::before,
12
+ ::backdrop,
13
+ ::file-selector-button {
14
+ box-sizing: border-box;
15
+ margin: 0;
16
+ padding: 0;
17
+ border: 0 solid;
18
+ }
19
+
20
+ html,
21
+ :host {
22
+ line-height: 1.5;
23
+ -webkit-text-size-adjust: 100%;
24
+ tab-size: 4;
25
+ font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
26
+ -webkit-tap-highlight-color: transparent;
27
+ }
28
+
29
+ hr {
30
+ height: 0;
31
+ color: inherit;
32
+ border-top-width: 1px;
33
+ }
34
+
35
+ abbr:where([title]) {
36
+ -webkit-text-decoration: underline dotted;
37
+ text-decoration: underline dotted;
38
+ }
39
+
40
+ h1, h2, h3, h4, h5, h6 {
41
+ font-size: inherit;
42
+ font-weight: inherit;
43
+ }
44
+
45
+ a {
46
+ color: inherit;
47
+ -webkit-text-decoration: inherit;
48
+ text-decoration: inherit;
49
+ }
50
+
51
+ b, strong {
52
+ font-weight: bolder;
53
+ }
54
+
55
+ code, kbd, samp, pre {
56
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
57
+ font-size: 1em;
58
+ }
59
+
60
+ small {
61
+ font-size: 80%;
62
+ }
63
+
64
+ sub, sup {
65
+ font-size: 75%;
66
+ line-height: 0;
67
+ position: relative;
68
+ vertical-align: baseline;
69
+ }
70
+
71
+ sub { bottom: -0.25em; }
72
+ sup { top: -0.5em; }
73
+
74
+ table {
75
+ text-indent: 0;
76
+ border-color: inherit;
77
+ border-collapse: collapse;
78
+ }
79
+
80
+ ol, ul, menu {
81
+ list-style: none;
82
+ }
83
+
84
+ img, svg, video, canvas, audio, iframe, embed, object {
85
+ display: block;
86
+ vertical-align: middle;
87
+ }
88
+
89
+ img, video {
90
+ max-width: 100%;
91
+ height: auto;
92
+ }
93
+
94
+ button, input, select, optgroup, textarea, ::file-selector-button {
95
+ font: inherit;
96
+ font-feature-settings: inherit;
97
+ font-variation-settings: inherit;
98
+ letter-spacing: inherit;
99
+ color: inherit;
100
+ border-radius: 0;
101
+ background-color: transparent;
102
+ opacity: 1;
103
+ }
104
+
105
+ textarea {
106
+ resize: vertical;
107
+ }
108
+
109
+ [hidden]:where(:not([hidden='until-found'])) {
110
+ display: none !important;
111
+ }
112
+ `;
@@ -0,0 +1,159 @@
1
+ import { type VNode, useCallback, useEffect, useRef, useState } from "preact/compat";
2
+ import HighlightedCode from "./highlighted-code";
3
+
4
+ export interface CodeBlockData {
5
+ language: string;
6
+ title: string;
7
+ code: string;
8
+ }
9
+
10
+ export interface PreviewBaseProps {
11
+ title?: string;
12
+ height?: number;
13
+ srcdoc: string;
14
+ sandbox?: string;
15
+ syncDelay: number;
16
+ codeBlocks: CodeBlockData[];
17
+ defaultOpen?: boolean;
18
+ }
19
+
20
+ type Viewport = { label: string; width: string };
21
+
22
+ const VIEWPORTS: Viewport[] = [
23
+ { label: "Mobile", width: "320px" },
24
+ { label: "Tablet", width: "768px" },
25
+ { label: "Full", width: "100%" },
26
+ ];
27
+
28
+ export default function PreviewBase({
29
+ title,
30
+ height,
31
+ srcdoc,
32
+ sandbox,
33
+ syncDelay,
34
+ codeBlocks,
35
+ defaultOpen,
36
+ }: PreviewBaseProps): VNode {
37
+ const [activeViewport, setActiveViewport] = useState(2); // default: Full
38
+ const [codeOpen, setCodeOpen] = useState(defaultOpen ?? false);
39
+ const [iframeHeight, setIframeHeight] = useState(height ?? 200);
40
+ const iframeRef = useRef<HTMLIFrameElement>(null);
41
+
42
+ const syncHeight = useCallback(() => {
43
+ const iframe = iframeRef.current;
44
+ if (!iframe || height != null) return;
45
+ try {
46
+ const doc = iframe.contentDocument;
47
+ if (doc?.body) {
48
+ const h = doc.body.scrollHeight;
49
+ if (h > 0) setIframeHeight(Math.max(h + 16, 200));
50
+ }
51
+ } catch {
52
+ // cross-origin or not yet loaded — ignore
53
+ }
54
+ }, [height]);
55
+
56
+ useEffect(() => {
57
+ const iframe = iframeRef.current;
58
+ if (!iframe) return;
59
+ let timeoutId: ReturnType<typeof setTimeout>;
60
+ const onLoad = () => {
61
+ if (syncDelay > 0) {
62
+ timeoutId = setTimeout(syncHeight, syncDelay);
63
+ } else {
64
+ syncHeight();
65
+ }
66
+ };
67
+ iframe.addEventListener("load", onLoad);
68
+ return () => {
69
+ iframe.removeEventListener("load", onLoad);
70
+ clearTimeout(timeoutId);
71
+ };
72
+ }, [syncHeight, srcdoc, syncDelay]);
73
+
74
+ // Re-measure height when viewport changes (content reflows)
75
+ useEffect(() => {
76
+ if (height != null) return;
77
+ const id = setTimeout(syncHeight, 150);
78
+ return () => clearTimeout(id);
79
+ }, [activeViewport, syncHeight, height]);
80
+
81
+ const containerWidth = VIEWPORTS[activeViewport].width;
82
+
83
+ return (
84
+ <div className="border border-muted rounded-lg overflow-hidden my-vsp-md">
85
+ {/* Title bar with viewport buttons */}
86
+ <div className="flex items-center justify-between px-hsp-md py-hsp-sm bg-surface border-b border-muted gap-hsp-sm flex-wrap">
87
+ {title && <span className="text-caption font-semibold text-fg">{title}</span>}
88
+ <div className="flex gap-hsp-2xs" role="group" aria-label="Viewport size">
89
+ {VIEWPORTS.map((vp, i) => (
90
+ <button
91
+ key={vp.label}
92
+ type="button"
93
+ className={`px-hsp-sm py-hsp-2xs text-caption border rounded-full cursor-pointer transition-[background,color,border-color] duration-150 leading-snug ${
94
+ i === activeViewport
95
+ ? "bg-accent text-bg border-accent hover:bg-accent-hover hover:border-accent-hover"
96
+ : "bg-transparent text-muted border-muted hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)]"
97
+ }`}
98
+ aria-pressed={i === activeViewport}
99
+ onClick={() => setActiveViewport(i)}
100
+ >
101
+ {vp.label}
102
+ </button>
103
+ ))}
104
+ </div>
105
+ </div>
106
+
107
+ {/* Preview area */}
108
+ <div className="bg-surface p-hsp-lg">
109
+ <div
110
+ className="resize-x overflow-auto max-w-full mx-auto"
111
+ style={{ width: containerWidth }}
112
+ >
113
+ {/* Intentional: white canvas regardless of site theme — matches standard browser context */}
114
+ <iframe
115
+ ref={iframeRef}
116
+ className="block w-full border-none bg-[#fff] rounded shadow-[0_1px_3px_color-mix(in_srgb,var(--color-fg)_8%,transparent)]"
117
+ srcDoc={srcdoc}
118
+ sandbox={sandbox}
119
+ style={{ height: iframeHeight }}
120
+ title={title ?? "Preview"}
121
+ />
122
+ </div>
123
+ </div>
124
+
125
+ {/* Code section */}
126
+ <div className="border-t border-muted">
127
+ <button
128
+ type="button"
129
+ className="flex items-center w-full px-hsp-md py-hsp-sm text-caption font-medium text-muted bg-surface border-none cursor-pointer gap-hsp-xs hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)]"
130
+ onClick={() => setCodeOpen((v) => !v)}
131
+ aria-expanded={codeOpen}
132
+ >
133
+ <span
134
+ className={`text-caption transition-transform duration-200 ${codeOpen ? "rotate-90" : ""}`}
135
+ aria-hidden="true"
136
+ >
137
+ &#9654;
138
+ </span>
139
+ {codeOpen ? "Hide code" : "Show code"}
140
+ </button>
141
+ {codeOpen && (
142
+ <div>
143
+ {codeBlocks.map((block, idx) => (
144
+ <div key={block.title} className={`overflow-x-auto ${idx > 0 ? "border-t border-muted" : ""}`}>
145
+ <span className="block px-hsp-md py-hsp-xs text-caption font-semibold text-muted bg-surface border-b border-muted uppercase tracking-wider">
146
+ {block.title}
147
+ </span>
148
+ <HighlightedCode
149
+ code={block.code}
150
+ language={block.language}
151
+ />
152
+ </div>
153
+ ))}
154
+ </div>
155
+ )}
156
+ </div>
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,19 @@
1
+ // W6A stub — no-op default + ImageEnlargeSsrFallback named exports.
2
+ //
3
+ // When the imageEnlarge feature is enabled, the feature template
4
+ // overwrites this file with the real island and SSR fallback. Generated
5
+ // projects without the feature ship the no-op so the unconditional
6
+ // `pages/lib/_body-end-islands` import resolves (the body-end islands
7
+ // renderer references both the default and the SSR fallback).
8
+ import type { JSX } from "preact";
9
+
10
+ function ImageEnlarge(): JSX.Element | null {
11
+ return null;
12
+ }
13
+ ImageEnlarge.displayName = "ImageEnlarge";
14
+
15
+ export default ImageEnlarge;
16
+
17
+ export function ImageEnlargeSsrFallback(): JSX.Element | null {
18
+ return null;
19
+ }
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "preact/hooks";
4
+ import type { Heading } from "@/types/heading";
5
+ import { SmartBreak } from "@/utils/smart-break";
6
+ import clsx from "clsx";
7
+
8
+ interface MobileTocProps {
9
+ headings: Heading[];
10
+ title?: string;
11
+ }
12
+
13
+ export function MobileToc({ headings, title = "On this page" }: MobileTocProps) {
14
+ const filtered = useMemo(
15
+ () => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
16
+ [headings],
17
+ );
18
+ const [open, setOpen] = useState(false);
19
+
20
+ // No qualifying headings: emit a CSS-hidden container that still
21
+ // carries the locale title text in the static markup so no-JS users
22
+ // and crawlers still see it ("On this page" / "目次"). aria-hidden
23
+ // prevents screen readers from announcing the invisible label.
24
+ if (filtered.length === 0) {
25
+ return (
26
+ <div className="hidden" aria-hidden="true">
27
+ {title}
28
+ </div>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <div className="xl:hidden border border-muted mb-vsp-lg">
34
+ <button
35
+ type="button"
36
+ onClick={() => setOpen((prev) => !prev)}
37
+ aria-expanded={open}
38
+ className="flex w-full items-center justify-between px-hsp-lg py-vsp-xs text-small font-medium text-fg"
39
+ >
40
+ <span>{title}</span>
41
+ <svg
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ aria-hidden="true"
44
+ className={clsx(
45
+ "h-icon-sm w-icon-sm text-muted transition-transform duration-150",
46
+ open && "rotate-180",
47
+ )}
48
+ fill="none"
49
+ viewBox="0 0 24 24"
50
+ stroke="currentColor"
51
+ strokeWidth={2}
52
+ >
53
+ <path
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ d="M19 9l-7 7-7-7"
57
+ />
58
+ </svg>
59
+ </button>
60
+ {/* Items list is always rendered so the SSG HTML carries every
61
+ anchor link for crawlers / JS-off readers AND so the hydration
62
+ DOM tree matches the SSR DOM byte-for-byte (no conditional
63
+ subtree-mount on hydration that could otherwise drop the
64
+ rendered markup or detach already-attached event handlers).
65
+ Visibility is toggled via the `hidden` utility — display:none
66
+ when closed, intrinsic display when open. */}
67
+ <ul
68
+ className={clsx(
69
+ "border-t border-muted px-hsp-lg py-vsp-xs space-y-vsp-2xs",
70
+ !open && "hidden",
71
+ )}
72
+ aria-hidden={!open}
73
+ >
74
+ {filtered.map((heading, index) => (
75
+ <li
76
+ key={`${heading.slug}-${index}`}
77
+ className={clsx(
78
+ heading.depth === 3 && "ml-hsp-lg",
79
+ heading.depth === 4 && "ml-hsp-2xl",
80
+ )}
81
+ >
82
+ <a
83
+ href={`#${heading.slug}`}
84
+ onClick={() => setOpen(false)}
85
+ className="block py-vsp-2xs text-small text-muted hover:text-fg hover:underline focus-visible:underline"
86
+ >
87
+ <SmartBreak>{heading.text}</SmartBreak>
88
+ </a>
89
+ </li>
90
+ ))}
91
+ </ul>
92
+ </div>
93
+ );
94
+ }
@@ -0,0 +1,14 @@
1
+ // W6A stub — no-op default export.
2
+ //
3
+ // The host (zudo-doc showcase) ships an interactive preset-generator
4
+ // island used by its onboarding pages. Generated downstream projects
5
+ // ship the no-op so unconditional page imports (`pages/lib/_preset-generator`)
6
+ // resolve. Wire a real implementation by replacing this file.
7
+ import type { JSX } from "preact";
8
+
9
+ function PresetGenerator(): JSX.Element | null {
10
+ return null;
11
+ }
12
+ PresetGenerator.displayName = "PresetGenerator";
13
+
14
+ export default PresetGenerator;
@@ -0,0 +1,98 @@
1
+ import { useState, useEffect } from "preact/compat";
2
+ import type { ComponentChildren } from "preact";
3
+
4
+ interface SidebarToggleProps {
5
+ children: ComponentChildren;
6
+ }
7
+
8
+ export default function SidebarToggle({ children }: SidebarToggleProps) {
9
+ const [open, setOpen] = useState(false);
10
+
11
+ useEffect(() => {
12
+ if (open) {
13
+ document.body.style.overflow = "hidden";
14
+ } else {
15
+ document.body.style.overflow = "";
16
+ }
17
+ return () => {
18
+ document.body.style.overflow = "";
19
+ };
20
+ }, [open]);
21
+
22
+ // Close mobile sidebar on View Transition navigation
23
+ useEffect(() => {
24
+ function handleSwap() {
25
+ setOpen(false);
26
+ }
27
+ // zfb's `<ViewTransitions />` does a real page load on every
28
+ // navigation, so `DOMContentLoaded` is the post-navigate signal.
29
+ document.addEventListener("DOMContentLoaded", handleSwap);
30
+ return () => document.removeEventListener("DOMContentLoaded", handleSwap);
31
+ }, []);
32
+
33
+ return (
34
+ <>
35
+ {/* Hamburger button - visible only on mobile */}
36
+ <button
37
+ type="button"
38
+ onClick={() => setOpen(!open)}
39
+ className="lg:hidden px-hsp-sm py-vsp-xs -ml-hsp-sm mr-hsp-sm text-muted hover:text-fg"
40
+ aria-label={open ? "Close sidebar" : "Open sidebar"}
41
+ >
42
+ {open ? (
43
+ <svg
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ className="h-icon-lg w-icon-lg"
46
+ fill="none"
47
+ viewBox="0 0 24 24"
48
+ stroke="currentColor"
49
+ strokeWidth={2}
50
+ >
51
+ <path
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ d="M6 18L18 6M6 6l12 12"
55
+ />
56
+ </svg>
57
+ ) : (
58
+ <svg
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ className="h-icon-lg w-icon-lg"
61
+ fill="none"
62
+ viewBox="0 0 24 24"
63
+ stroke="currentColor"
64
+ strokeWidth={2}
65
+ >
66
+ <path
67
+ strokeLinecap="round"
68
+ strokeLinejoin="round"
69
+ d="M4 6h16M4 12h16M4 18h16"
70
+ />
71
+ </svg>
72
+ )}
73
+ </button>
74
+
75
+ {/* Backdrop overlay - mobile only */}
76
+ {open && (
77
+ <div
78
+ className="fixed inset-0 z-30 bg-overlay/30 lg:hidden"
79
+ onClick={() => setOpen(false)}
80
+ />
81
+ )}
82
+
83
+ {/* Sidebar panel - mobile only (desktop sidebar is in doc-layout) */}
84
+ <aside
85
+ className={`
86
+ fixed top-[3.5rem] left-0 z-40 h-[calc(100vh-3.5rem)] w-[16rem] flex flex-col
87
+ border-r border-muted bg-bg transition-transform duration-200
88
+ lg:hidden
89
+ ${open ? "translate-x-0" : "-translate-x-full"}
90
+ `}
91
+ >
92
+ <div className="flex-1 overflow-y-auto">
93
+ {children}
94
+ </div>
95
+ </aside>
96
+ </>
97
+ );
98
+ }