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,63 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // MathBlock — server-rendered KaTeX component for MDX math expressions.
4
+ //
5
+ // Registered in pages/_mdx-components.ts as `MathBlock` so the MDX corpus
6
+ // can reference it as <MathBlock latex="…" block />.
7
+ //
8
+ // Used by the math-equations.mdx content files (both EN and JA) which write
9
+ // `<MathBlock>` JSX directly instead of `$$…$$` fences. The explicit JSX
10
+ // form is required because the zfb Rust MDX→JSX emitter does not understand
11
+ // remark-math `$$…$$` syntax — LaTeX identifiers like `\infty` become invalid
12
+ // JSX expressions `{\infty}` that esbuild rejects (zudo-front-builder #93).
13
+ // Using `<MathBlock>` directly keeps the LaTeX inside a string attribute,
14
+ // which esbuild accepts cleanly.
15
+ //
16
+ // Rendering: katex.renderToString() is called at SSR time — no client JS.
17
+ // `throwOnError: false` keeps a broken formula visible as an error span
18
+ // rather than crashing the page.
19
+
20
+ import katex from "katex";
21
+ import type { VNode } from "preact";
22
+
23
+ interface MathBlockProps {
24
+ /** Raw LaTeX source string. */
25
+ latex: string;
26
+ /** When true, renders as a block (display) equation; otherwise inline. */
27
+ block?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Server-rendered KaTeX math component.
32
+ *
33
+ * Block mode wraps the output in `<div class="math math-display">`;
34
+ * inline mode uses `<span class="math math-inline">`. The class names
35
+ * match the standard rehype-katex output so existing CSS (e.g. the
36
+ * KaTeX stylesheet) still applies.
37
+ */
38
+ export function MathBlock({ latex, block = false }: MathBlockProps): VNode {
39
+ const html = katex.renderToString(latex, {
40
+ displayMode: block,
41
+ // Never throw — malformed LaTeX renders a visible error span instead
42
+ // of crashing the entire page build.
43
+ throwOnError: false,
44
+ });
45
+
46
+ if (block) {
47
+ return (
48
+ <div
49
+ class="math math-display"
50
+ // eslint-disable-next-line react/no-danger
51
+ dangerouslySetInnerHTML={{ __html: html }}
52
+ />
53
+ );
54
+ }
55
+
56
+ return (
57
+ <span
58
+ class="math math-inline"
59
+ // eslint-disable-next-line react/no-danger
60
+ dangerouslySetInnerHTML={{ __html: html }}
61
+ />
62
+ );
63
+ }
@@ -0,0 +1,68 @@
1
+ // Shared helper — pick the right loadDocs() collection and category-meta dir
2
+ // for the active (locale, version) pair, applying the locale-first + EN-fallback
3
+ // merge that pages/[locale]/docs/[...slug].tsx uses in its own paths() pass so
4
+ // the sidebar tree mirrors what those pages enumerate.
5
+ //
6
+ // Used by _sidebar-with-defaults.tsx (desktop sidebar) and
7
+ // _header-with-defaults.tsx (mobile SidebarToggle) so both nav surfaces apply
8
+ // the same defaultLocaleOnlyPrefixes filter and stay in sync.
9
+
10
+ import { defaultLocale, type Locale } from "@/config/i18n";
11
+ import { settings } from "@/config/settings";
12
+ import { docsUrl, isDefaultLocaleOnlyPath } from "@/utils/base";
13
+ import { loadCategoryMeta, type CategoryMeta } from "@/utils/docs";
14
+ import type { DocsEntry } from "@/types/docs-entry";
15
+ import { loadDocs } from "../_data";
16
+
17
+ export type NavSourceDocs = {
18
+ docs: DocsEntry[];
19
+ categoryMeta: Map<string, CategoryMeta>;
20
+ };
21
+
22
+ /**
23
+ * Pick the right `loadDocs(...)` collection name and category-meta dir
24
+ * for the active (locale, version) pair, applying the same locale-first
25
+ * + EN-fallback merge that `pages/[locale]/docs/[...slug].tsx` performs
26
+ * in its own `paths()` so the sidebar tree mirrors what those pages
27
+ * enumerate.
28
+ */
29
+ export function loadNavSourceDocs(
30
+ lang: Locale,
31
+ currentVersion: string | undefined,
32
+ ): NavSourceDocs {
33
+ if (currentVersion) {
34
+ const collectionName = `docs-v-${currentVersion}`;
35
+ const versionConfig = settings.versions?.find((v) => v.slug === currentVersion);
36
+ const docs = loadDocs(collectionName).filter((d) => !d.data.draft);
37
+ const categoryMeta = loadCategoryMeta(versionConfig?.docsDir ?? settings.docsDir);
38
+ return { docs, categoryMeta };
39
+ }
40
+
41
+ if (lang === defaultLocale) {
42
+ const docs = loadDocs("docs").filter((d) => !d.data.draft);
43
+ const categoryMeta = loadCategoryMeta(settings.docsDir);
44
+ return { docs, categoryMeta };
45
+ }
46
+
47
+ // Non-default locale: locale-first merge with EN fallback so docs the
48
+ // active locale has not yet translated still appear in the tree.
49
+ const localeDocs = loadDocs(`docs-${lang}`).filter((d) => !d.data.draft);
50
+ const baseDocs = loadDocs("docs").filter((d) => !d.data.draft);
51
+ const localeSlugSet = new Set(localeDocs.map((d) => d.data.slug ?? d.id));
52
+ const fallbackDocs = baseDocs
53
+ .filter((d) => !localeSlugSet.has(d.data.slug ?? d.id))
54
+ .filter((d) => !isDefaultLocaleOnlyPath(docsUrl(d.data.slug ?? d.id)));
55
+ const allDocs = [...localeDocs, ...fallbackDocs];
56
+
57
+ const localeDir =
58
+ (settings.locales as Record<string, { dir?: string }>)[lang]?.dir ??
59
+ settings.docsDir;
60
+ // Base meta first, locale meta wins on overlapping keys — same merge
61
+ // order [locale]/docs/[...slug].tsx uses in its paths() pass.
62
+ const categoryMeta = new Map<string, CategoryMeta>([
63
+ ...loadCategoryMeta(settings.docsDir),
64
+ ...loadCategoryMeta(localeDir),
65
+ ]);
66
+
67
+ return { docs: allDocs, categoryMeta };
68
+ }
@@ -0,0 +1,81 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ // SSR fallback shell for the <PresetGenerator> interactive form.
4
+ //
5
+ // The real component (src/components/preset-generator.tsx) is a large
6
+ // client-only island. This file is imported transitively from page modules
7
+ // (pages/docs/[...slug].tsx → _mdx-components.ts → here), so zfb's island
8
+ // scanner walks the static import chain and registers PresetGenerator in the
9
+ // manifest. Without this import, the scanner never finds the component and
10
+ // client-side hydration never fires (orphan-component problem; same root
11
+ // cause fixed for body-end islands in _body-end-islands.tsx).
12
+ //
13
+ // The fallback renders all 8 section headings as static SSR HTML so:
14
+ // 1. Screen readers and search engines see the section structure (a11y/SEO).
15
+ // 2. Layout does not collapse to nothing while JS loads (no-JS layout).
16
+ // 3. The scanner traces this file → preset-generator.tsx via the Island child
17
+ // and registers PresetGenerator in the manifest for client-side mounting.
18
+ //
19
+ // Uses the canonical `<Island ssrFallback>` API (zfb) so the scanner can
20
+ // connect the import to the manifest entry and the hydration runtime can
21
+ // mount the real form into the skip-ssr placeholder on the client.
22
+
23
+ import type { VNode } from "preact";
24
+ import { HeadingH3 } from "@takazudo/zudo-doc/content";
25
+ import { Island } from "@takazudo/zfb";
26
+ import PresetGenerator from "@/components/preset-generator";
27
+
28
+ // Pin displayName so zfb's captureComponentName produces a stable marker
29
+ // name even after the SSR pipeline runs through a function-name-rewriting
30
+ // layer. Matches the data-zfb-island-skip-ssr attribute value the runtime
31
+ // queries. Mirrors the pattern in _body-end-islands.tsx.
32
+ (PresetGenerator as { displayName?: string }).displayName = "PresetGenerator";
33
+
34
+ // Heading text for each of the 8 sections — must match the original
35
+ // SectionHeading calls in src/components/preset-generator.tsx exactly
36
+ // so the SSR fallback and the real component render the same section labels.
37
+ // Order must mirror the JSX source order in preset-generator.tsx — do NOT
38
+ // sort alphabetically. The array drives the SSR fallback heading sequence
39
+ // shown to screen readers and visible before JS hydration.
40
+ const SECTION_HEADINGS = [
41
+ "Project Name",
42
+ "Default Language",
43
+ "Color Scheme Mode",
44
+ "Color Scheme",
45
+ "Features",
46
+ "Header right items",
47
+ "Markdown Options",
48
+ "Package Manager",
49
+ ] as const;
50
+
51
+ /**
52
+ * Static SSR fallback for the interactive PresetGenerator form.
53
+ *
54
+ * Renders all 8 section headings as static HTML for a11y/SEO and no-JS
55
+ * layout stability. Uses Island with ssrFallback so the zfb scanner traces
56
+ * this file → preset-generator.tsx and registers the real component in the
57
+ * island manifest for client-side mounting.
58
+ */
59
+ export function PresetGeneratorFallback(): VNode {
60
+ const fallback = (
61
+ <div class="zd-preset-gen-fallback">
62
+ {SECTION_HEADINGS.map((heading) => (
63
+ <section key={heading}>
64
+ <HeadingH3>{heading}</HeadingH3>
65
+ </section>
66
+ ))}
67
+ </div>
68
+ );
69
+
70
+ // Island with ssrFallback:
71
+ // - SSR emits the section headings as static HTML inside the skip-ssr div.
72
+ // - The scanner reads children.type = PresetGenerator → registers it in
73
+ // the manifest under "PresetGenerator".
74
+ // - The hydration runtime mounts the real interactive form into the
75
+ // skip-ssr placeholder on the client after load.
76
+ return Island({
77
+ when: "load",
78
+ ssrFallback: fallback,
79
+ children: <PresetGenerator />,
80
+ }) as unknown as VNode;
81
+ }
@@ -0,0 +1,388 @@
1
+ // Client-side script for the SiteSearch custom element.
2
+ //
3
+ // Port of the `<script>` block from the deleted
4
+ // `src/components/search.astro` (commit a4d9956). Rewritten as a plain
5
+ // JavaScript IIFE (no ES-module `import` statements) so it can be
6
+ // emitted via `dangerouslySetInnerHTML` without requiring bundler
7
+ // support for inline scripts.
8
+ //
9
+ // Differences from the original Astro script:
10
+ // - MiniSearch is NOT imported; a lightweight built-in search (fetch
11
+ // index + simple word-match scoring) is used instead. This avoids the
12
+ // inline-script bundling limitation. Full MiniSearch integration can be
13
+ // added in a follow-up topic once the bundle pipeline is in place.
14
+ // - The post-navigation rebinder pulls its event name from
15
+ // `AFTER_NAVIGATE_EVENT` in
16
+ // `@takazudo/zudo-doc/transitions` (today: `zfb:after-swap`)
17
+ // rather than a hard-coded `astro:*` literal. See
18
+ // zudolab/zudo-doc#1335 (E2 task 2 half B) for the vocabulary
19
+ // introduction and zudolab/zudo-doc#1523 for the W6B flip from
20
+ // `DOMContentLoaded` to the Strategy B SPA event name.
21
+
22
+ import { AFTER_NAVIGATE_EVENT } from "@takazudo/zudo-doc/transitions";
23
+
24
+ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
25
+ if (customElements.get("site-search")) return; // guard double-registration
26
+
27
+ var PAGE_SIZE = 10;
28
+
29
+ function escapeHtml(text) {
30
+ return String(text)
31
+ .replace(/&/g, "&amp;")
32
+ .replace(/</g, "&lt;")
33
+ .replace(/>/g, "&gt;")
34
+ .replace(/"/g, "&quot;")
35
+ .replace(/'/g, "&#39;");
36
+ }
37
+
38
+ function escapeRegExp(text) {
39
+ return text.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&");
40
+ }
41
+
42
+ function parseTerms(query) {
43
+ return query.trim().split(/\\s+/).filter(Boolean);
44
+ }
45
+
46
+ function scoreEntry(entry, terms) {
47
+ var score = 0;
48
+ var titleLower = (entry.title || "").toLowerCase();
49
+ var bodyLower = (entry.body || "").toLowerCase();
50
+ var descLower = (entry.description || "").toLowerCase();
51
+ for (var i = 0; i < terms.length; i++) {
52
+ var t = terms[i].toLowerCase();
53
+ if (titleLower.indexOf(t) !== -1) score += 3;
54
+ if (descLower.indexOf(t) !== -1) score += 2;
55
+ if (bodyLower.indexOf(t) !== -1) score += 1;
56
+ }
57
+ return score;
58
+ }
59
+
60
+ function highlightTerms(text, terms) {
61
+ if (!terms.length) return escapeHtml(text);
62
+ var escaped = terms.map(function(t) { return escapeRegExp(t); });
63
+ var pattern = new RegExp("(" + escaped.join("|") + ")", "gi");
64
+ return text.split(pattern).map(function(seg, i) {
65
+ return i % 2 === 1
66
+ ? "<mark>" + escapeHtml(seg) + "</mark>"
67
+ : escapeHtml(seg);
68
+ }).join("");
69
+ }
70
+
71
+ function truncate(text, query, max) {
72
+ max = max || 200;
73
+ if (text.length <= max) return text;
74
+ var terms = parseTerms(query);
75
+ var lower = text.toLowerCase();
76
+ var best = -1;
77
+ for (var i = 0; i < terms.length; i++) {
78
+ var idx = lower.indexOf(terms[i].toLowerCase());
79
+ if (idx !== -1 && (best === -1 || idx < best)) best = idx;
80
+ }
81
+ if (best === -1) return text.slice(0, max) + "\\u2026";
82
+ var half = Math.floor(max / 2);
83
+ var start = Math.max(0, best - half);
84
+ var end = start + max;
85
+ if (end > text.length) { end = text.length; start = Math.max(0, end - max); }
86
+ var result = text.slice(start, end);
87
+ if (start > 0) result = "\\u2026" + result;
88
+ if (end < text.length) result += "\\u2026";
89
+ return result;
90
+ }
91
+
92
+ customElements.define("site-search", class SiteSearch extends HTMLElement {
93
+ constructor() {
94
+ super();
95
+ this._dialog = null;
96
+ this._openBtn = null;
97
+ this._closeBtn = null;
98
+ this._input = null;
99
+ this._results = null;
100
+ this._countWide = null;
101
+ this._countNarrow = null;
102
+ this._entries = null;
103
+ this._loading = false;
104
+ this._debounce = null;
105
+ this._currentQuery = "";
106
+ this._allResults = [];
107
+ this._shownCount = 0;
108
+ this._shortcut = "";
109
+ this._resultCountTemplate = "";
110
+ this._keydownHandler = null;
111
+ this._observer = null;
112
+ this._sentinel = null;
113
+ this._isLoadingBatch = false;
114
+ // Snapshot of the initial results-area HTML (includes SSR placeholder).
115
+ // Captured in connectedCallback so we can restore it on input-clear
116
+ // without re-querying the DOM (the placeholder node is replaced once
117
+ // search results are rendered).
118
+ this._placeholderHtml = "";
119
+ // Held so we can remove the document-level after-navigate listener
120
+ // in disconnectedCallback. zudolab/zudo-doc#1523 — under Strategy B
121
+ // SPA navigation a non-persisted <site-search> element would leak
122
+ // one document listener per nav without this hook.
123
+ this._afterNavHandler = null;
124
+ }
125
+
126
+ connectedCallback() {
127
+ this._dialog = this.querySelector("[data-search-dialog]");
128
+ this._openBtn = this.querySelector("[data-open-search]");
129
+ this._closeBtn = this.querySelector("[data-close-search]");
130
+ this._input = this.querySelector("[data-search-input]");
131
+ this._results = this.querySelector("[data-search-results]");
132
+ this._countWide = this.querySelector("[data-search-count]");
133
+ this._countNarrow = this.querySelector("[data-search-count-narrow]");
134
+ this._resultCountTemplate = this.dataset.resultCountTemplate || "{count} results";
135
+ // Snapshot the placeholder HTML before any search renders overwrite it.
136
+ this._placeholderHtml = this._results ? this._results.innerHTML : "";
137
+
138
+ // Platform keyboard-shortcut label — injected into [data-kbd-shortcut]
139
+ var nav = navigator;
140
+ var isMac = /Mac|iPhone|iPad|iPod/.test(
141
+ (nav.userAgentData && nav.userAgentData.platform) || nav.userAgent
142
+ );
143
+ this._shortcut = isMac ? "\\u2318K" : "Ctrl+K";
144
+ var kbdEl = this.querySelector("[data-kbd-shortcut]");
145
+ if (kbdEl) kbdEl.textContent = this._shortcut;
146
+
147
+ // Wire open/close handlers
148
+ var self = this;
149
+ if (this._openBtn) {
150
+ this._openBtn.addEventListener("click", function() { self.openDialog(); });
151
+ }
152
+ if (this._closeBtn) {
153
+ this._closeBtn.addEventListener("click", function() { self.closeDialog(); });
154
+ }
155
+ if (this._dialog) {
156
+ this._dialog.addEventListener("close", function() {
157
+ document.documentElement.style.overflow = "";
158
+ });
159
+ this._dialog.addEventListener("click", function(e) {
160
+ if (e.target === self._dialog) self.closeDialog();
161
+ });
162
+ }
163
+ if (this._input) {
164
+ this._input.addEventListener("input", function() { self.handleInput(); });
165
+ }
166
+
167
+ // Global keyboard shortcut (⌘K / Ctrl+K to open)
168
+ this._keydownHandler = function(e) {
169
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
170
+ e.preventDefault();
171
+ self.openDialog();
172
+ }
173
+ };
174
+ document.addEventListener("keydown", this._keydownHandler);
175
+
176
+ // View-Transitions compat: re-run on the v2 after-navigate event.
177
+ // Stored on the instance so disconnectedCallback can detach it on
178
+ // body swap when this element is NOT persisted via
179
+ // data-zfb-transition-persist (zudolab/zudo-doc#1523).
180
+ this._afterNavHandler = function() {
181
+ var kbdEl2 = self.querySelector("[data-kbd-shortcut]");
182
+ if (kbdEl2) kbdEl2.textContent = self._shortcut;
183
+ };
184
+ document.addEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
185
+ }
186
+
187
+ disconnectedCallback() {
188
+ if (this._keydownHandler) {
189
+ document.removeEventListener("keydown", this._keydownHandler);
190
+ this._keydownHandler = null;
191
+ }
192
+ if (this._afterNavHandler) {
193
+ document.removeEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
194
+ this._afterNavHandler = null;
195
+ }
196
+ this.teardownSentinel();
197
+ }
198
+
199
+ openDialog() {
200
+ if (!this._dialog) return;
201
+ document.documentElement.style.overflow = "hidden";
202
+ this._dialog.showModal();
203
+ if (this._input) {
204
+ this._input.focus();
205
+ this._input.select();
206
+ }
207
+ if (!this._entries && !this._loading) {
208
+ this.loadIndex();
209
+ }
210
+ }
211
+
212
+ closeDialog() {
213
+ if (!this._dialog) return;
214
+ this._dialog.close();
215
+ document.documentElement.style.overflow = "";
216
+ }
217
+
218
+ handleInput() {
219
+ var self = this;
220
+ if (this._debounce) clearTimeout(this._debounce);
221
+ this._debounce = setTimeout(function() { self.search(); }, 150);
222
+ }
223
+
224
+ loadIndex() {
225
+ if (this._loading) return;
226
+ this._loading = true;
227
+ var self = this;
228
+ var base = this.dataset.base || "/";
229
+ fetch(base + "search-index.json")
230
+ .then(function(r) {
231
+ if (!r.ok) throw new Error("HTTP " + r.status);
232
+ return r.json();
233
+ })
234
+ .then(function(data) {
235
+ self._entries = Array.isArray(data) ? data : (data.entries || []);
236
+ self._loading = false;
237
+ // If user already typed, search now
238
+ if (self._input && self._input.value.trim()) {
239
+ self.search();
240
+ }
241
+ })
242
+ .catch(function() {
243
+ self._loading = false;
244
+ // Index unavailable — silently degrade
245
+ });
246
+ }
247
+
248
+ search() {
249
+ var query = this._input ? this._input.value.trim() : "";
250
+ this._currentQuery = query;
251
+
252
+ if (!query) {
253
+ this.teardownSentinel();
254
+ this._allResults = [];
255
+ this._shownCount = 0;
256
+ if (this._results) this._results.innerHTML = this.placeholderHtml();
257
+ this.updateCount();
258
+ return;
259
+ }
260
+
261
+ if (!this._entries) {
262
+ if (this._results) {
263
+ this._results.innerHTML = "<p class=\\"text-small text-muted\\">Loading search index\\u2026</p>";
264
+ }
265
+ if (!this._loading) this.loadIndex();
266
+ return;
267
+ }
268
+
269
+ var terms = parseTerms(query);
270
+ var scored = [];
271
+ for (var i = 0; i < this._entries.length; i++) {
272
+ var s = scoreEntry(this._entries[i], terms);
273
+ if (s > 0) scored.push({ entry: this._entries[i], score: s });
274
+ }
275
+ scored.sort(function(a, b) { return b.score - a.score; });
276
+ this._allResults = scored;
277
+ this._shownCount = 0;
278
+ this.teardownSentinel();
279
+ this.updateCount();
280
+
281
+ if (!scored.length) {
282
+ if (this._results) {
283
+ this._results.innerHTML = "<p class=\\"text-small text-muted\\">No results found.</p>";
284
+ }
285
+ return;
286
+ }
287
+
288
+ if (this._results) this._results.innerHTML = "";
289
+ this.loadMore();
290
+ if (this._shownCount < this._allResults.length) {
291
+ this.setupSentinel();
292
+ }
293
+ }
294
+
295
+ loadMore() {
296
+ if (this._isLoadingBatch) return;
297
+ if (this._shownCount >= this._allResults.length) return;
298
+ this._isLoadingBatch = true;
299
+ try {
300
+ var batch = this._allResults.slice(this._shownCount, this._shownCount + PAGE_SIZE);
301
+ var self = this;
302
+ for (var i = 0; i < batch.length; i++) {
303
+ var article = self.renderResult(batch[i].entry);
304
+ if (self._sentinel && self._sentinel.parentNode === self._results) {
305
+ self._results.insertBefore(article, self._sentinel);
306
+ } else if (self._results) {
307
+ self._results.appendChild(article);
308
+ }
309
+ }
310
+ this._shownCount += batch.length;
311
+ if (this._shownCount >= this._allResults.length) {
312
+ this.teardownSentinel();
313
+ }
314
+ } finally {
315
+ this._isLoadingBatch = false;
316
+ }
317
+ }
318
+
319
+ setupSentinel() {
320
+ this.teardownSentinel();
321
+ if (!this._results) return;
322
+ var sentinel = document.createElement("div");
323
+ sentinel.setAttribute("data-search-sentinel", "");
324
+ sentinel.setAttribute("aria-hidden", "true");
325
+ sentinel.style.height = "1px";
326
+ sentinel.style.width = "100%";
327
+ this._results.appendChild(sentinel);
328
+ this._sentinel = sentinel;
329
+ var self = this;
330
+ this._observer = new IntersectionObserver(function(entries) {
331
+ for (var i = 0; i < entries.length; i++) {
332
+ if (entries[i].isIntersecting) self.loadMore();
333
+ }
334
+ }, { root: this._results, rootMargin: "200px 0px" });
335
+ this._observer.observe(sentinel);
336
+ }
337
+
338
+ teardownSentinel() {
339
+ if (this._observer) { this._observer.disconnect(); this._observer = null; }
340
+ if (this._sentinel) { this._sentinel.remove(); this._sentinel = null; }
341
+ }
342
+
343
+ updateCount() {
344
+ var count = this._allResults.length;
345
+ var template = this._resultCountTemplate;
346
+ var text = count > 0 ? template.replace("{count}", String(count)) : "";
347
+ var show = !!text;
348
+ if (this._countWide) {
349
+ this._countWide.textContent = text;
350
+ this._countWide.classList.toggle("hidden", !show);
351
+ }
352
+ if (this._countNarrow) {
353
+ this._countNarrow.textContent = text;
354
+ this._countNarrow.classList.toggle("hidden", !show);
355
+ }
356
+ }
357
+
358
+ placeholderHtml() {
359
+ return this._placeholderHtml;
360
+ }
361
+
362
+ renderResult(entry) {
363
+ var article = document.createElement("article");
364
+ article.className = "-mx-hsp-lg border-b border-muted";
365
+ var link = document.createElement("a");
366
+ link.href = entry.url || "#";
367
+ link.className =
368
+ "group block px-hsp-lg py-vsp-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent";
369
+ var title = document.createElement("span");
370
+ title.className =
371
+ "font-semibold text-fg group-hover:text-accent group-hover:underline group-focus-visible:underline";
372
+ var terms = parseTerms(this._currentQuery);
373
+ title.innerHTML = highlightTerms(entry.title || "", terms);
374
+ link.appendChild(title);
375
+ var text = entry.description || entry.body;
376
+ if (text) {
377
+ var excerpt = document.createElement("p");
378
+ excerpt.className =
379
+ "mt-vsp-2xs text-caption text-muted leading-relaxed group-hover:underline group-focus-visible:underline decoration-muted";
380
+ var truncated = truncate(text, this._currentQuery, 200);
381
+ excerpt.innerHTML = highlightTerms(truncated, terms);
382
+ link.appendChild(excerpt);
383
+ }
384
+ article.appendChild(link);
385
+ return article;
386
+ }
387
+ });
388
+ })();`;