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,102 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # b4push — local quality gate run before pushing.
5
+ #
6
+ # Step order (cheap → expensive):
7
+ # 1. Type checking (zfb check)
8
+ # 2. Build (zfb build)
9
+ # 3. HTML validation (html-validate dist/**/*.html)
10
+ # 4. Manual interactive smoke (operator-driven)
11
+ #
12
+ # Env overrides for non-interactive use:
13
+ # B4PUSH_SKIP_HTML_VALIDATE=1 — skip HTML validation (step 3)
14
+ # B4PUSH_SKIP_MANUAL_SMOKE=1 — skip the manual interactive smoke
15
+
16
+ START_TIME=$(date +%s)
17
+ FAILURES=()
18
+ TOTAL_STEPS=4
19
+ CURRENT_STEP=0
20
+
21
+ step() {
22
+ CURRENT_STEP=$((CURRENT_STEP + 1))
23
+ echo ""
24
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
25
+ echo "▶ Step $CURRENT_STEP/$TOTAL_STEPS: $1"
26
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
27
+ }
28
+
29
+ pass() { echo "✅ $1"; }
30
+ fail() { echo "❌ $1"; FAILURES+=("$1"); }
31
+ skip() { echo "⏭ $1 (skipped)"; }
32
+
33
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
34
+
35
+ # ── Step 1: Type checking ─────────────────────────────
36
+ step "Type checking (zfb check)"
37
+ if (cd "$ROOT_DIR" && pnpm check); then
38
+ pass "Type checking passed"
39
+ else
40
+ fail "Type checking"
41
+ fi
42
+
43
+ # ── Step 2: Build ─────────────────────────────────────
44
+ step "Build (zfb build)"
45
+ if (cd "$ROOT_DIR" && pnpm build); then
46
+ pass "Build passed"
47
+ else
48
+ fail "Build"
49
+ fi
50
+
51
+ # ── Step 3: HTML validation ───────────────────────────
52
+ step "HTML validation (html-validate)"
53
+ if [[ "${B4PUSH_SKIP_HTML_VALIDATE:-}" == "1" ]]; then
54
+ skip "HTML validation (B4PUSH_SKIP_HTML_VALIDATE=1)"
55
+ else
56
+ if (cd "$ROOT_DIR" && pnpm check:html); then
57
+ pass "HTML validation passed"
58
+ else
59
+ fail "HTML validation"
60
+ fi
61
+ fi
62
+
63
+ # ── Step 4: Manual interactive smoke ─────────────────
64
+ step "Manual interactive smoke"
65
+ if [[ "${B4PUSH_SKIP_MANUAL_SMOKE:-}" == "1" ]]; then
66
+ skip "Manual smoke (B4PUSH_SKIP_MANUAL_SMOKE=1)"
67
+ else
68
+ cat <<'MANUAL'
69
+ Run `pnpm preview` in another terminal and exercise:
70
+ • theme toggle (light/dark)
71
+ • mobile menu (narrow viewport)
72
+ • search dropdown (header search)
73
+ • code-block syntax highlighting
74
+
75
+ Press [Enter] when all flows look healthy, or Ctrl-C to abort.
76
+ MANUAL
77
+ if read -r _; then
78
+ pass "Manual smoke acknowledged"
79
+ else
80
+ fail "Manual smoke (aborted)"
81
+ fi
82
+ fi
83
+
84
+ # ── Summary ──────────────────────────────────────────
85
+ END_TIME=$(date +%s)
86
+ DURATION=$((END_TIME - START_TIME))
87
+
88
+ echo ""
89
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
90
+ echo " SUMMARY (${DURATION}s)"
91
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
92
+
93
+ if [ ${#FAILURES[@]} -eq 0 ]; then
94
+ echo "✅ All $TOTAL_STEPS checks passed (or skipped). Safe to push."
95
+ exit 0
96
+ else
97
+ echo "❌ ${#FAILURES[@]} check(s) failed:"
98
+ for f in "${FAILURES[@]}"; do
99
+ echo " - $f"
100
+ done
101
+ exit 1
102
+ fi
@@ -0,0 +1,15 @@
1
+ // W6A stub — no-op default export.
2
+ //
3
+ // The host (zudo-doc showcase) ships a full AI-chat modal island here. In
4
+ // generated downstream projects the AI assistant is gated behind a future
5
+ // settings flag; for now the file exists so unconditional page imports
6
+ // (`pages/lib/_body-end-islands.tsx`) resolve, and the component renders
7
+ // nothing. Wire a real implementation by replacing this file.
8
+ import type { JSX } from "preact";
9
+
10
+ function AiChatModal(): JSX.Element | null {
11
+ return null;
12
+ }
13
+ AiChatModal.displayName = "AiChatModal";
14
+
15
+ export default AiChatModal;
@@ -0,0 +1,14 @@
1
+ // W6A stub — no-op default export.
2
+ //
3
+ // The host installs a tiny client-side router bootstrap that re-runs
4
+ // island lifecycle code across zfb navigations. Generated projects ship
5
+ // the no-op so unconditional page imports (`pages/lib/_body-end-islands`)
6
+ // resolve without dragging the routing bridge into every scaffold.
7
+ import type { JSX } from "preact";
8
+
9
+ function ClientRouterBootstrap(): JSX.Element | null {
10
+ return null;
11
+ }
12
+ ClientRouterBootstrap.displayName = "ClientRouterBootstrap";
13
+
14
+ export default ClientRouterBootstrap;
@@ -0,0 +1,25 @@
1
+ import { HeadingH2 } from './heading-h2';
2
+ import { HeadingH3 } from './heading-h3';
3
+ import { HeadingH4 } from './heading-h4';
4
+ import { ContentParagraph } from './content-paragraph';
5
+ import { ContentLink } from './content-link';
6
+ import { ContentStrong } from './content-strong';
7
+ import { ContentBlockquote } from './content-blockquote';
8
+ import { ContentUl } from './content-ul';
9
+ import { ContentOl } from './content-ol';
10
+ import { ContentTable } from './content-table';
11
+ import { ContentCode } from './content-code';
12
+
13
+ export const htmlOverrides = {
14
+ h2: HeadingH2,
15
+ h3: HeadingH3,
16
+ h4: HeadingH4,
17
+ p: ContentParagraph,
18
+ a: ContentLink,
19
+ strong: ContentStrong,
20
+ blockquote: ContentBlockquote,
21
+ ul: ContentUl,
22
+ ol: ContentOl,
23
+ table: ContentTable,
24
+ code: ContentCode,
25
+ };
@@ -0,0 +1,16 @@
1
+ type Props = {
2
+ children?: React.ReactNode;
3
+ className?: string;
4
+ [key: string]: any;
5
+ };
6
+
7
+ export function ContentBlockquote({ children, className, ...rest }: Props) {
8
+ return (
9
+ <blockquote
10
+ className={`border-l-[3px] border-muted pl-hsp-lg text-muted italic${className ? ` ${className}` : ''}`}
11
+ {...rest}
12
+ >
13
+ {children}
14
+ </blockquote>
15
+ );
16
+ }
@@ -0,0 +1,117 @@
1
+ import { SmartBreak as SmartBreakImpl } from '@/utils/smart-break';
2
+
3
+ // Preact VNode vs React.ReactNode type mismatch under compat mode; cast so the
4
+ // content override type-checks. Runtime is fine since @astrojs/preact compat is on.
5
+ const SmartBreak = SmartBreakImpl as unknown as (props: {
6
+ children?: unknown;
7
+ }) => any;
8
+
9
+ type Props = {
10
+ children?: React.ReactNode;
11
+ className?: string;
12
+ [key: string]: any;
13
+ };
14
+
15
+ /**
16
+ * Override for inline `<code>` in MDX. Wraps text-only inline code with
17
+ * <SmartBreak> so that path/URL-like strings (e.g. `src/foo/bar.ts`) can
18
+ * break at delimiters on narrow viewports.
19
+ *
20
+ * Block code inside a fenced block is processed by Shiki, which emits a
21
+ * <code> element with class "language-*" containing a tree of <span>
22
+ * nodes (not a plain string). We detect those and render untouched so
23
+ * syntax highlighting is never disturbed.
24
+ *
25
+ * Astro 6 passes pure-text MDX children wrapped in a `StaticHtml` Preact
26
+ * component whose text lives in `props.value`, not as a string child.
27
+ * `extractText` unwraps that so the heuristic works regardless of MDX's
28
+ * internal wrapping.
29
+ */
30
+ export function ContentCode({ children, className, ...rest }: Props) {
31
+ const isShikiBlock =
32
+ typeof className === 'string' && /(^|\s)language-/.test(className);
33
+
34
+ const textFromChildren = isShikiBlock ? null : extractText(children);
35
+
36
+ if (textFromChildren === null) {
37
+ return (
38
+ <code className={className} {...rest}>
39
+ {children}
40
+ </code>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <code className={className} {...rest}>
46
+ <SmartBreak>{textFromChildren}</SmartBreak>
47
+ </code>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Walk a React/Preact children value and return a concatenated plain
53
+ * string when the entire subtree is text-only. Returns null if any
54
+ * non-text node (other than the StaticHtml wrapper Astro uses for pure
55
+ * text MDX content) is found — that signals inline markup inside the
56
+ * code span and means we must not inject <wbr>.
57
+ */
58
+ function extractText(children: unknown): string | null {
59
+ if (typeof children === 'string') return children;
60
+ if (typeof children === 'number') return String(children);
61
+ // Only accept a single VNode whose `props.value` is text (the Astro
62
+ // StaticHtml wrapper used for pure-text MDX children). Deliberately do
63
+ // NOT recurse into arbitrary VNode trees — that would match Shiki's
64
+ // <span>-based block code output and inject <wbr> into syntax-highlighted
65
+ // tokens, breaking block code rendering.
66
+ if (children && typeof children === 'object' && !Array.isArray(children)) {
67
+ const v = children as { props?: { value?: unknown } };
68
+ if (v.props && v.props.value != null) {
69
+ if (
70
+ typeof v.props.value === 'string' ||
71
+ v.props.value instanceof String ||
72
+ (typeof v.props.value === 'object' && typeof (v.props.value as object).toString === 'function')
73
+ ) {
74
+ const s = String(v.props.value);
75
+ if (!s || s.startsWith('[object')) return null;
76
+ // Shiki block-code output also arrives as a StaticHtml wrapper, but
77
+ // its value is escaped HTML (e.g. "&lt;span class=...&gt;"). Inline
78
+ // code's value is the plain text of the backtick span (no HTML
79
+ // markup). If the value looks like HTML, refuse to unwrap — the
80
+ // block path below falls through to a plain <code> passthrough.
81
+ if (looksLikeHtmlMarkup(s)) return null;
82
+ return decodeEntities(s);
83
+ }
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Detect whether a string is the HTML-escaped rendering of a Shiki block
91
+ * (with nested <span> tokens) rather than the plain text of an inline
92
+ * `<code>` span. The presence of an escaped HTML-tag opener (`&lt;span`,
93
+ * `&lt;code`, etc.) is a reliable signal; plain URL/path inline code
94
+ * never contains those escaped sequences.
95
+ */
96
+ function looksLikeHtmlMarkup(s: string): boolean {
97
+ return /&lt;(span|code|pre|div|br|i|b|em|strong)\b/i.test(s) || /<(span|code|pre)\s/i.test(s);
98
+ }
99
+
100
+ /**
101
+ * Minimal HTML entity decoder for the set MDX produces for inline text
102
+ * (`&amp;`, `&lt;`, `&gt;`, `&quot;`, `&#39;`, numeric). Keeps the
103
+ * result visually identical to the authored source so downstream
104
+ * SmartBreak operates on the original characters.
105
+ */
106
+ function decodeEntities(s: string): string {
107
+ return s
108
+ .replace(/&lt;/g, '<')
109
+ .replace(/&gt;/g, '>')
110
+ .replace(/&quot;/g, '"')
111
+ .replace(/&#39;/g, "'")
112
+ .replace(/&#x27;/gi, "'")
113
+ .replace(/&nbsp;/g, ' ')
114
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
115
+ .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)))
116
+ .replace(/&amp;/g, '&');
117
+ }
@@ -0,0 +1,83 @@
1
+ import { SmartBreak as SmartBreakBase } from '../../utils/smart-break';
2
+
3
+ // SmartBreak is defined with Preact's VNode return type, but content
4
+ // components type children as React.ReactNode. Cast to a React-compatible
5
+ // signature; at runtime Preact compat unifies the two.
6
+ const SmartBreak = SmartBreakBase as unknown as (props: {
7
+ children?: React.ReactNode;
8
+ }) => React.ReactElement;
9
+
10
+ type Props = {
11
+ href?: string;
12
+ className?: string;
13
+ children?: React.ReactNode;
14
+ [key: string]: any;
15
+ };
16
+
17
+ export function ContentLink({ href, className, children, ...rest }: Props) {
18
+ // Block links and hash-links (heading anchors) should render without content link styling
19
+ const classes = className ? className.split(' ') : [];
20
+ if (classes.includes('block') || classes.includes('hash-link')) {
21
+ return (
22
+ <a href={href} className={className} {...rest}>
23
+ {children}
24
+ </a>
25
+ );
26
+ }
27
+
28
+ // Astro 6 wraps pure-text MDX children in a `StaticHtml` Preact component
29
+ // whose text lives in `props.value`, not as a direct string child. Unwrap
30
+ // when possible so path-like text gets smart-break treatment.
31
+ const textFromChildren = extractText(children);
32
+ const content =
33
+ textFromChildren !== null ? (
34
+ <SmartBreak>{textFromChildren}</SmartBreak>
35
+ ) : (
36
+ children
37
+ );
38
+
39
+ return (
40
+ <a
41
+ href={href}
42
+ className={`text-accent underline hover:text-accent-hover${className ? ` ${className}` : ''}`}
43
+ {...rest}
44
+ >
45
+ {content}
46
+ </a>
47
+ );
48
+ }
49
+
50
+ function extractText(children: unknown): string | null {
51
+ if (typeof children === 'string') return children;
52
+ if (typeof children === 'number') return String(children);
53
+ // Only accept a single StaticHtml-like VNode (Astro's wrapper for pure-text
54
+ // MDX children). Arrays or VNodes with inline markup indicate mixed content
55
+ // that must not be flattened through SmartBreak.
56
+ if (children && typeof children === 'object' && !Array.isArray(children)) {
57
+ const v = children as { props?: { value?: unknown } };
58
+ if (v.props && v.props.value != null) {
59
+ if (
60
+ typeof v.props.value === 'string' ||
61
+ v.props.value instanceof String ||
62
+ (typeof v.props.value === 'object' && typeof (v.props.value as object).toString === 'function')
63
+ ) {
64
+ const s = String(v.props.value);
65
+ if (s && !s.startsWith('[object')) return decodeEntities(s);
66
+ }
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function decodeEntities(s: string): string {
73
+ return s
74
+ .replace(/&lt;/g, '<')
75
+ .replace(/&gt;/g, '>')
76
+ .replace(/&quot;/g, '"')
77
+ .replace(/&#39;/g, "'")
78
+ .replace(/&#x27;/gi, "'")
79
+ .replace(/&nbsp;/g, ' ')
80
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
81
+ .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)))
82
+ .replace(/&amp;/g, '&');
83
+ }
@@ -0,0 +1,19 @@
1
+ type Props = {
2
+ children?: React.ReactNode;
3
+ className?: string;
4
+ [key: string]: any;
5
+ };
6
+
7
+ // 2em indent: enough room for 2-digit markers like "66." (#244)
8
+ // Inline style — Tailwind v4 does not generate arbitrary values from these TSX files
9
+ export function ContentOl({ children, className, ...rest }: Props) {
10
+ return (
11
+ <ol
12
+ {...rest}
13
+ className={className || undefined}
14
+ style={{ paddingLeft: '2em', listStyleType: 'decimal' }}
15
+ >
16
+ {children}
17
+ </ol>
18
+ );
19
+ }
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ children?: React.ReactNode;
3
+ [key: string]: any;
4
+ };
5
+
6
+ // Passthrough: no custom styles needed for <p> (inherits from .zd-content base).
7
+ // Override claimed to enable future Tailwind utilities without CSS cascade conflicts.
8
+ export function ContentParagraph({ children, ...rest }: Props) {
9
+ return <p {...rest}>{children}</p>;
10
+ }
@@ -0,0 +1,16 @@
1
+ type Props = {
2
+ children?: React.ReactNode;
3
+ className?: string;
4
+ [key: string]: any;
5
+ };
6
+
7
+ export function ContentStrong({ children, className, ...rest }: Props) {
8
+ return (
9
+ <strong
10
+ className={`font-bold text-fg${className ? ` ${className}` : ''}`}
11
+ {...rest}
12
+ >
13
+ {children}
14
+ </strong>
15
+ );
16
+ }
@@ -0,0 +1,18 @@
1
+ type Props = {
2
+ children?: React.ReactNode;
3
+ className?: string;
4
+ [key: string]: any;
5
+ };
6
+
7
+ export function ContentTable({ children, className, ...rest }: Props) {
8
+ return (
9
+ <div className="overflow-x-auto">
10
+ <table
11
+ className={`w-full border-collapse text-small${className ? ` ${className}` : ''}`}
12
+ {...rest}
13
+ >
14
+ {children}
15
+ </table>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+ type Props = {
2
+ children?: React.ReactNode;
3
+ className?: string;
4
+ [key: string]: any;
5
+ };
6
+
7
+ // 2em indent via inline style — Tailwind v4 does not generate arbitrary values from these TSX files (#244)
8
+ export function ContentUl({ children, className, ...rest }: Props) {
9
+ return (
10
+ <ul
11
+ {...rest}
12
+ className={className || undefined}
13
+ style={{ paddingLeft: '2em', listStyleType: 'disc' }}
14
+ >
15
+ {children}
16
+ </ul>
17
+ );
18
+ }
@@ -0,0 +1,26 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { ComponentChildren } from 'preact';
3
+
4
+ type Props = {
5
+ id?: string;
6
+ className?: string;
7
+ children?: ComponentChildren;
8
+ [key: string]: any;
9
+ };
10
+
11
+ export function HeadingH2({ id, children, className, ...rest }: Props) {
12
+ return (
13
+ <h2
14
+ id={id}
15
+ className={`text-title font-bold leading-tight pt-vsp-sm border-t-[3px] border-transparent${className ? ` ${className}` : ''}`}
16
+ style={
17
+ {
18
+ borderImage: 'linear-gradient(to right, var(--color-fg), transparent) 1',
19
+ } as CSSProperties
20
+ }
21
+ {...rest}
22
+ >
23
+ {children}
24
+ </h2>
25
+ );
26
+ }
@@ -0,0 +1,26 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { ComponentChildren } from 'preact';
3
+
4
+ type Props = {
5
+ id?: string;
6
+ className?: string;
7
+ children?: ComponentChildren;
8
+ [key: string]: any;
9
+ };
10
+
11
+ export function HeadingH3({ id, children, className, ...rest }: Props) {
12
+ return (
13
+ <h3
14
+ id={id}
15
+ className={`text-body font-bold leading-snug pt-vsp-xs border-t-[2px] border-transparent${className ? ` ${className}` : ''}`}
16
+ style={
17
+ {
18
+ borderImage: 'linear-gradient(to right, var(--color-muted), transparent) 1',
19
+ } as CSSProperties
20
+ }
21
+ {...rest}
22
+ >
23
+ {children}
24
+ </h3>
25
+ );
26
+ }
@@ -0,0 +1,26 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { ComponentChildren } from 'preact';
3
+
4
+ type Props = {
5
+ id?: string;
6
+ className?: string;
7
+ children?: ComponentChildren;
8
+ [key: string]: any;
9
+ };
10
+
11
+ export function HeadingH4({ id, children, className, ...rest }: Props) {
12
+ return (
13
+ <h4
14
+ id={id}
15
+ className={`text-body font-semibold leading-snug pt-vsp-xs border-t border-transparent${className ? ` ${className}` : ''}`}
16
+ style={
17
+ {
18
+ borderImage: 'linear-gradient(to right, var(--color-muted), transparent) 1',
19
+ } as CSSProperties
20
+ }
21
+ {...rest}
22
+ >
23
+ {children}
24
+ </h4>
25
+ );
26
+ }
@@ -0,0 +1,15 @@
1
+ // W6A stub — no-op default Preact wrapper.
2
+ //
3
+ // When the designTokenPanel feature is enabled, the feature template
4
+ // overwrites this file with the real wrapper that side-effect-imports
5
+ // `@/lib/design-token-panel-bootstrap` (which calls configurePanel and
6
+ // mounts zdtp). Generated projects without the feature ship the no-op
7
+ // so the unconditional `pages/lib/_body-end-islands` import resolves.
8
+ import type { JSX } from "preact";
9
+
10
+ function DesignTokenPanelBootstrap(): JSX.Element | null {
11
+ return null;
12
+ }
13
+ DesignTokenPanelBootstrap.displayName = "DesignTokenPanelBootstrap";
14
+
15
+ export default DesignTokenPanelBootstrap;
@@ -0,0 +1,15 @@
1
+ // W6A stub — no-op default export.
2
+ //
3
+ // When the sidebarToggle feature is enabled, the feature template
4
+ // overwrites this file with the real desktop sidebar-toggle island.
5
+ // Generated projects without the feature ship the no-op so the
6
+ // unconditional `pages/lib/_body-end-islands` (or sidebar wrapper)
7
+ // import resolves at typecheck time.
8
+ import type { JSX } from "preact";
9
+
10
+ function DesktopSidebarToggle(): JSX.Element | null {
11
+ return null;
12
+ }
13
+ DesktopSidebarToggle.displayName = "DesktopSidebarToggle";
14
+
15
+ export default DesktopSidebarToggle;
@@ -0,0 +1,18 @@
1
+ // W6A stub — no-op default + DocHistory named exports.
2
+ //
3
+ // When the docHistory feature is enabled, the feature template
4
+ // overwrites this file with the real island. Generated projects
5
+ // without the feature ship the no-op so the unconditional
6
+ // `pages/lib/_doc-history-area` import resolves. The host module also
7
+ // exposes `DocHistory` as a named export, so the stub mirrors both
8
+ // shapes to keep the import surface stable.
9
+ import type { JSX } from "preact";
10
+
11
+ function DocHistoryComponent(): JSX.Element | null {
12
+ return null;
13
+ }
14
+ DocHistoryComponent.displayName = "DocHistory";
15
+
16
+ export default DocHistoryComponent;
17
+
18
+ export const DocHistory = DocHistoryComponent;
@@ -0,0 +1,74 @@
1
+ import { useEffect, useState } from "preact/compat";
2
+ import type { HighlighterCore } from "shiki";
3
+
4
+ let highlighterPromise: Promise<HighlighterCore> | null = null;
5
+
6
+ function getHighlighter(): Promise<HighlighterCore> {
7
+ if (!highlighterPromise) {
8
+ highlighterPromise = import("shiki")
9
+ .then(({ createHighlighter }) =>
10
+ createHighlighter({
11
+ themes: ["catppuccin-latte", "vitesse-dark"],
12
+ langs: ["html", "css", "javascript"],
13
+ }),
14
+ )
15
+ .catch((err) => {
16
+ // Clear cached rejection so next call retries
17
+ highlighterPromise = null;
18
+ throw err;
19
+ });
20
+ }
21
+ return highlighterPromise;
22
+ }
23
+
24
+ interface HighlightedCodeProps {
25
+ code: string;
26
+ language: string;
27
+ }
28
+
29
+ export default function HighlightedCode({
30
+ code,
31
+ language,
32
+ }: HighlightedCodeProps) {
33
+ const [html, setHtml] = useState<string | null>(null);
34
+
35
+ useEffect(() => {
36
+ let cancelled = false;
37
+ getHighlighter()
38
+ .then((highlighter) => {
39
+ if (cancelled) return;
40
+ const lang = highlighter.getLoadedLanguages().includes(language)
41
+ ? language
42
+ : "text";
43
+ const result = highlighter.codeToHtml(code, {
44
+ lang,
45
+ themes: { light: "catppuccin-latte", dark: "vitesse-dark" },
46
+ defaultColor: false,
47
+ });
48
+ setHtml(result);
49
+ })
50
+ .catch(() => {
51
+ // Shiki failed to load — keep showing the plain-text fallback
52
+ });
53
+ return () => {
54
+ cancelled = true;
55
+ };
56
+ }, [code, language]);
57
+
58
+ if (!html) {
59
+ return (
60
+ <pre className="m-0 p-hsp-md bg-code-bg text-caption leading-relaxed overflow-x-auto">
61
+ <code className="font-mono whitespace-pre">
62
+ {code}
63
+ </code>
64
+ </pre>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <div
70
+ className="zd-html-preview-code"
71
+ dangerouslySetInnerHTML={{ __html: html }}
72
+ />
73
+ );
74
+ }