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,233 @@
1
+ "use client";
2
+
3
+ // Use preact hook entrypoints directly — zfb's esbuild step doesn't alias
4
+ // "react" to "preact/compat" the way Astro's `@astrojs/preact` integration
5
+ // did, so importing from "react" here would fail to resolve at SSR/island
6
+ // bundle time. Same pattern as src/components/sidebar-tree.tsx.
7
+ import { useState } from "preact/hooks";
8
+ import type { NavNode } from "@/utils/docs";
9
+ import { INDENT, connectorLeft, ConnectorLines, CategoryLinkIcon } from "./tree-nav-shared";
10
+
11
+ // site-tree-nav uses wider padding than the narrow sidebar
12
+ const SITE_BASE_PAD = "clamp(0.5rem, 0.8vw, 1rem)";
13
+
14
+ function padLeft(depth: number): string {
15
+ if (depth === 0) return SITE_BASE_PAD;
16
+ return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
17
+ }
18
+
19
+ function reorderTree(tree: NavNode[], order: string[]): NavNode[] {
20
+ const map = new Map(tree.map((node) => [node.slug, node]));
21
+ const ordered: NavNode[] = [];
22
+ for (const slug of order) {
23
+ const node = map.get(slug);
24
+ if (node) {
25
+ ordered.push(node);
26
+ map.delete(slug);
27
+ }
28
+ }
29
+ // append unmatched nodes at end
30
+ for (const node of map.values()) {
31
+ ordered.push(node);
32
+ }
33
+ return ordered;
34
+ }
35
+
36
+ interface SiteTreeNavProps {
37
+ tree: NavNode[];
38
+ ariaLabel?: string;
39
+ categoryOrder?: string[];
40
+ categoryIgnore?: string[];
41
+ }
42
+
43
+ export default function SiteTreeNav({
44
+ tree,
45
+ ariaLabel = "Site index",
46
+ categoryOrder,
47
+ categoryIgnore,
48
+ }: SiteTreeNavProps) {
49
+ let processedTree = tree;
50
+ if (categoryIgnore) {
51
+ const ignoreSet = new Set(categoryIgnore);
52
+ processedTree = processedTree.filter((node) => !ignoreSet.has(node.slug));
53
+ }
54
+ if (categoryOrder) {
55
+ processedTree = reorderTree(processedTree, categoryOrder);
56
+ }
57
+ return (
58
+ <nav
59
+ aria-label={ariaLabel}
60
+ data-site-nav
61
+ className="grid gap-vsp-md"
62
+ style={{
63
+ gridTemplateColumns: "repeat(auto-fill, minmax(min(18rem, 100%), 1fr))",
64
+ }}
65
+ >
66
+ {processedTree.map((node) => (
67
+ <div key={node.slug} className="min-w-0 border border-muted pl-hsp-sm py-vsp-2xs">
68
+ {node.children.length > 0 ? (
69
+ <CategoryNode node={node} depth={0} isLast={true} />
70
+ ) : (
71
+ <LeafNode node={node} depth={0} isLast={true} />
72
+ )}
73
+ </div>
74
+ ))}
75
+ </nav>
76
+ );
77
+ }
78
+
79
+ function NodeList({ nodes, depth }: { nodes: NavNode[]; depth: number }) {
80
+ return (
81
+ <>
82
+ {nodes.map((node, index) => {
83
+ const isLast = index === nodes.length - 1;
84
+ return node.children.length > 0 ? (
85
+ <CategoryNode
86
+ key={node.slug}
87
+ node={node}
88
+ depth={depth}
89
+ isLast={isLast}
90
+ />
91
+ ) : (
92
+ <LeafNode
93
+ key={node.slug}
94
+ node={node}
95
+ depth={depth}
96
+ isLast={isLast}
97
+ />
98
+ );
99
+ })}
100
+ </>
101
+ );
102
+ }
103
+
104
+ function CategoryNode({
105
+ node,
106
+ depth,
107
+ isLast,
108
+ }: {
109
+ node: NavNode;
110
+ depth: number;
111
+ isLast: boolean;
112
+ }) {
113
+ const [open, setOpen] = useState(true);
114
+ const toggle = () => setOpen((prev) => !prev);
115
+ const paddingLeft = padLeft(depth);
116
+
117
+ return (
118
+ <div className={`${depth >= 1 && !isLast ? "relative" : ""}`}>
119
+ {depth >= 1 && !isLast && open && (
120
+ <div
121
+ className="absolute border-l border-dashed border-muted z-10"
122
+ style={{
123
+ left: connectorLeft(depth),
124
+ top: 0,
125
+ bottom: 0,
126
+ }}
127
+ />
128
+ )}
129
+ <div className="relative">
130
+ <ConnectorLines
131
+ depth={depth}
132
+ isLast={isLast}
133
+ widthScale={2}
134
+ topPad="calc(0.15rem + var(--spacing-vsp-xs))"
135
+ />
136
+ <div
137
+ className="flex w-full items-center justify-between text-small font-semibold pt-[0.15rem] text-fg"
138
+ style={{ paddingLeft }}
139
+ >
140
+ {node.href ? (
141
+ <a
142
+ href={node.href}
143
+ className="flex-1 flex items-start gap-hsp-xs py-vsp-xs text-fg hover:text-accent hover:underline focus:underline"
144
+ >
145
+ {depth === 0 && (
146
+ <span className="flex h-[1lh] items-center">
147
+ <CategoryLinkIcon className="w-[18px] 2xl:w-[24px]" />
148
+ </span>
149
+ )}
150
+ {node.label}
151
+ </a>
152
+ ) : (
153
+ <button
154
+ type="button"
155
+ onClick={toggle}
156
+ className="flex-1 py-vsp-xs text-left hover:text-accent hover:underline focus:underline"
157
+ >
158
+ {node.label}
159
+ </button>
160
+ )}
161
+ <button
162
+ type="button"
163
+ onClick={toggle}
164
+ className="aspect-square flex items-center justify-center w-[1.75rem] border-y border-l border-muted hover:underline focus:underline"
165
+ aria-expanded={open}
166
+ aria-label={open ? `Collapse ${node.label}` : `Expand ${node.label}`}
167
+ >
168
+ <svg
169
+ xmlns="http://www.w3.org/2000/svg"
170
+ className={`h-icon-xs w-icon-xs transition-transform duration-150 ${open ? "rotate-90" : ""} text-muted`}
171
+ fill="none"
172
+ viewBox="0 0 24 24"
173
+ stroke="currentColor"
174
+ strokeWidth={2}
175
+ >
176
+ <path
177
+ strokeLinecap="round"
178
+ strokeLinejoin="round"
179
+ d="M9 5l7 7-7 7"
180
+ />
181
+ </svg>
182
+ </button>
183
+ </div>
184
+ </div>
185
+ {open && (
186
+ <div>
187
+ <NodeList nodes={node.children} depth={depth + 1} />
188
+ </div>
189
+ )}
190
+ </div>
191
+ );
192
+ }
193
+
194
+ function LeafNode({
195
+ node,
196
+ depth,
197
+ isLast,
198
+ }: {
199
+ node: NavNode;
200
+ depth: number;
201
+ isLast: boolean;
202
+ }) {
203
+ if (!node.href) return null;
204
+ const isRoot = depth === 0;
205
+ const paddingLeft = padLeft(depth);
206
+
207
+ const topPad = isRoot
208
+ ? "calc(var(--spacing-vsp-xs) + 0.15rem)"
209
+ : "var(--spacing-vsp-2xs)";
210
+
211
+ return (
212
+ <div>
213
+ <div className="relative">
214
+ <ConnectorLines depth={depth} isLast={isLast} widthScale={2} topPad={topPad} />
215
+ <a
216
+ href={node.href}
217
+ className={isRoot
218
+ ? "flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)+0.15rem)] text-small font-semibold text-fg hover:text-accent hover:underline focus:underline"
219
+ : `block py-vsp-2xs ${isLast ? "pb-vsp-xs" : ""} text-small text-fg hover:text-accent hover:underline focus:underline`
220
+ }
221
+ style={{ paddingLeft }}
222
+ >
223
+ {isRoot && (
224
+ <span className="flex h-[1lh] items-center">
225
+ <CategoryLinkIcon className="w-[18px] 2xl:w-[24px]" />
226
+ </span>
227
+ )}
228
+ {node.label}
229
+ </a>
230
+ </div>
231
+ </div>
232
+ );
233
+ }
@@ -0,0 +1,93 @@
1
+ import { useState, useEffect } from "preact/compat";
2
+
3
+ const STORAGE_KEY = "zudo-doc-theme";
4
+
5
+ function SunIcon() {
6
+ return (
7
+ <svg
8
+ aria-hidden="true"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="20"
11
+ height="20"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ strokeWidth="2"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ >
19
+ <circle cx="12" cy="12" r="5" />
20
+ <line x1="12" y1="1" x2="12" y2="3" />
21
+ <line x1="12" y1="21" x2="12" y2="23" />
22
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
23
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
24
+ <line x1="1" y1="12" x2="3" y2="12" />
25
+ <line x1="21" y1="12" x2="23" y2="12" />
26
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
27
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
28
+ </svg>
29
+ );
30
+ }
31
+
32
+ function MoonIcon() {
33
+ return (
34
+ <svg
35
+ aria-hidden="true"
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ width="20"
38
+ height="20"
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ strokeWidth="2"
43
+ strokeLinecap="round"
44
+ strokeLinejoin="round"
45
+ >
46
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
47
+ </svg>
48
+ );
49
+ }
50
+
51
+ interface ThemeToggleProps {
52
+ defaultMode?: "light" | "dark";
53
+ }
54
+
55
+ export default function ThemeToggle({ defaultMode = "dark" }: ThemeToggleProps) {
56
+ // Initial state must match server render to avoid hydration mismatch.
57
+ // Actual theme is synced from DOM in useEffect below.
58
+ const [mode, setMode] = useState<"light" | "dark">(defaultMode);
59
+
60
+ useEffect(() => {
61
+ const actual =
62
+ (document.documentElement.getAttribute("data-theme") as
63
+ | "light"
64
+ | "dark") || defaultMode;
65
+ if (actual !== mode) {
66
+ setMode(actual);
67
+ }
68
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
69
+
70
+ function toggle() {
71
+ const next = mode === "dark" ? "light" : "dark";
72
+ setMode(next);
73
+ document.documentElement.setAttribute("data-theme", next);
74
+ document.documentElement.style.colorScheme = next;
75
+ localStorage.setItem(STORAGE_KEY, next);
76
+ // Clear both v1 and v2 tweak state so the new scheme's palette takes effect.
77
+ localStorage.removeItem("zudo-doc-tweak-state");
78
+ localStorage.removeItem("zudo-doc-tweak-state-v2");
79
+ window.dispatchEvent(new CustomEvent("color-scheme-changed"));
80
+ }
81
+
82
+ const nextMode = mode === "dark" ? "light" : "dark";
83
+
84
+ return (
85
+ <button
86
+ onClick={toggle}
87
+ aria-label={`Switch to ${nextMode} mode`}
88
+ className="text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
89
+ >
90
+ {mode === "dark" ? <SunIcon /> : <MoonIcon />}
91
+ </button>
92
+ );
93
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "preact/hooks";
4
+ import { useActiveHeading } from "@/hooks/use-active-heading";
5
+ import type { Heading } from "@/types/heading";
6
+ import { SmartBreak } from "@/utils/smart-break";
7
+ import clsx from "clsx";
8
+
9
+ interface TocProps {
10
+ headings: Heading[];
11
+ }
12
+
13
+ export function Toc({ headings }: TocProps) {
14
+ const filtered = useMemo(
15
+ () => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
16
+ [headings],
17
+ );
18
+ const { activeId, activate } = useActiveHeading(filtered);
19
+
20
+ if (filtered.length === 0) return <nav className="hidden" />;
21
+
22
+ return (
23
+ <nav
24
+ aria-label="Table of contents"
25
+ className={clsx(
26
+ "hidden xl:flex flex-col",
27
+ "w-[280px] shrink-0",
28
+ "sticky top-[3.5rem] self-start z-10",
29
+ "pt-vsp-xl lg:pt-vsp-2xl",
30
+ "h-[calc(100vh-3.5rem)]",
31
+ )}
32
+ >
33
+ <ul className="border-l border-muted pl-hsp-lg overflow-y-auto flex-1 min-h-0">
34
+ {filtered.map((heading, index) => {
35
+ const isActive = heading.slug === activeId;
36
+ return (
37
+ <li
38
+ key={`${heading.slug}-${index}`}
39
+ className={clsx(
40
+ heading.depth === 3 && "ml-hsp-lg",
41
+ heading.depth === 4 && "ml-hsp-2xl",
42
+ )}
43
+ >
44
+ <a
45
+ href={`#${heading.slug}`}
46
+ onClick={() => activate(heading.slug)}
47
+ aria-current={isActive ? "true" : undefined}
48
+ className={clsx(
49
+ "block py-vsp-2xs text-small leading-snug transition-colors",
50
+ isActive
51
+ ? "bg-fg text-bg font-medium"
52
+ : "text-muted hover:underline focus:underline",
53
+ )}
54
+ >
55
+ <SmartBreak>{heading.text}</SmartBreak>
56
+ </a>
57
+ </li>
58
+ );
59
+ })}
60
+ </ul>
61
+ </nav>
62
+ );
63
+ }
@@ -0,0 +1,71 @@
1
+ // Shared constants and primitives for sidebar-tree and site-tree-nav
2
+
3
+ // Indentation — fluid clamp values
4
+ export const INDENT = "clamp(0.8rem, 1.2vw, 1.625rem)";
5
+ export const CONNECTOR_OFFSET = "clamp(0.2rem, 0.3vw, 0.5rem)";
6
+ export const CONNECTOR_WIDTH = "clamp(0.4rem, 0.6vw, 1rem)";
7
+ export const BASE_PAD = "clamp(0.4rem, 0.8vw, 1.3rem)";
8
+
9
+ export function connectorLeft(depth: number): string {
10
+ return `calc(${depth} * ${INDENT} + ${CONNECTOR_OFFSET})`;
11
+ }
12
+
13
+ const CATEGORY_LINK_PATH =
14
+ "M5.746 5.74 0 11.49l20.987 20.96C34.126 45.572 41.963 53.45 41.948 53.523c-.012.062-9.456 9.544-20.986 21.07L0 95.55l5.714 5.715c3.142 3.143 5.748 5.715 5.79 5.715s2.63-2.563 5.75-5.696l17.939-18.001c21.867-21.94 29.443-29.599 29.443-29.768 0-.114-.665-.804-5.084-5.275C51.872 40.47 11.71.125 11.565.036 11.525.01 8.906 2.578 5.746 5.74m38.345-.066c-3.132 3.13-5.696 5.71-5.696 5.732-.001.022 2.16 2.185 4.8 4.807 2.641 2.623 8.382 8.338 12.758 12.702 15.38 15.337 23.763 23.641 24.314 24.086.19.153.346.336.346.405 0 .07-1.738 1.847-3.887 3.976a17515 17515 0 0 0-20.35 20.264 19555 19555 0 0 1-17.223 17.158c-.416.409-.757.77-.757.8 0 .083 11.415 11.485 11.457 11.445.235-.22 53.542-53.528 53.542-53.543C103.395 53.472 49.891.02 49.837 0c-.028-.01-2.613 2.543-5.746 5.674";
15
+
16
+ export function CategoryLinkIcon({ className }: { className?: string }) {
17
+ return (
18
+ <svg
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ fill="currentColor"
21
+ viewBox="0 0 103.395 107.049"
22
+ aria-hidden="true"
23
+ className={`shrink-0 ${className ?? ""}`}
24
+ >
25
+ <path d={CATEGORY_LINK_PATH} />
26
+ </svg>
27
+ );
28
+ }
29
+
30
+ export function ConnectorLines({
31
+ depth,
32
+ isLast,
33
+ widthScale = 1,
34
+ topPad = "0px",
35
+ }: {
36
+ depth: number;
37
+ isLast: boolean;
38
+ widthScale?: number;
39
+ /**
40
+ * Padding-top of the inner content row that holds the link text. The horizontal
41
+ * connector and (when `isLast`) the vertical clip both anchor to the first-line
42
+ * midpoint computed as `topPad + 0.5lh`, so multi-line labels keep the connector
43
+ * aligned with the first line instead of the row's vertical center.
44
+ */
45
+ topPad?: string;
46
+ }) {
47
+ if (depth === 0) return null;
48
+ const left = connectorLeft(depth);
49
+ const width = widthScale === 1 ? CONNECTOR_WIDTH : `calc(${CONNECTOR_WIDTH} * ${widthScale})`;
50
+ const firstLineMid = `calc(${topPad} + 0.5lh)`;
51
+ return (
52
+ <>
53
+ <div
54
+ className="absolute border-l border-dashed border-muted"
55
+ style={{
56
+ left,
57
+ top: 0,
58
+ bottom: isLast ? `calc(100% - ${firstLineMid})` : 0,
59
+ }}
60
+ />
61
+ <div
62
+ className="absolute border-t border-dashed border-muted"
63
+ style={{
64
+ left,
65
+ width,
66
+ top: firstLineMid,
67
+ }}
68
+ />
69
+ </>
70
+ );
71
+ }
@@ -0,0 +1,182 @@
1
+ import { colorSchemes, type ColorScheme, type ColorRef } from "./color-schemes";
2
+ import { settings } from "./settings";
3
+
4
+ /** Default mapping: semantic token name → palette index */
5
+ export const SEMANTIC_DEFAULTS: Record<string, number> = {
6
+ surface: 0,
7
+ muted: 8,
8
+ accent: 5,
9
+ accentHover: 14,
10
+ codeBg: 10,
11
+ codeFg: 11,
12
+ success: 2,
13
+ danger: 1,
14
+ warning: 3,
15
+ info: 4,
16
+ mermaidNodeBg: 9,
17
+ mermaidText: 11,
18
+ mermaidLine: 8,
19
+ mermaidLabelBg: 10,
20
+ mermaidNoteBg: 0,
21
+ chatUserBg: 5,
22
+ chatUserText: 9,
23
+ chatAssistantBg: 9,
24
+ chatAssistantText: 11,
25
+ imageOverlayBg: 0,
26
+ imageOverlayFg: 11,
27
+ matchedKeywordBg: 3,
28
+ matchedKeywordFg: 15,
29
+ };
30
+
31
+ export const SEMANTIC_CSS_NAMES: Record<string, string> = {
32
+ surface: "--zd-surface",
33
+ muted: "--zd-muted",
34
+ accent: "--zd-accent",
35
+ accentHover: "--zd-accent-hover",
36
+ codeBg: "--zd-code-bg",
37
+ codeFg: "--zd-code-fg",
38
+ success: "--zd-success",
39
+ danger: "--zd-danger",
40
+ warning: "--zd-warning",
41
+ info: "--zd-info",
42
+ mermaidNodeBg: "--zd-mermaid-node-bg",
43
+ mermaidText: "--zd-mermaid-text",
44
+ mermaidLine: "--zd-mermaid-line",
45
+ mermaidLabelBg: "--zd-mermaid-label-bg",
46
+ mermaidNoteBg: "--zd-mermaid-note-bg",
47
+ chatUserBg: "--zd-chat-user-bg",
48
+ chatUserText: "--zd-chat-user-text",
49
+ chatAssistantBg: "--zd-chat-assistant-bg",
50
+ chatAssistantText: "--zd-chat-assistant-text",
51
+ imageOverlayBg: "--zd-image-overlay-bg",
52
+ imageOverlayFg: "--zd-image-overlay-fg",
53
+ matchedKeywordBg: "--zd-matched-keyword-bg",
54
+ matchedKeywordFg: "--zd-matched-keyword-fg",
55
+ };
56
+
57
+ export const lightDarkPairings = [
58
+ { light: "Default Light", dark: "Default Dark", label: "Default" },
59
+ ];
60
+
61
+ export function getActiveScheme(): ColorScheme {
62
+ const scheme = colorSchemes[settings.colorScheme];
63
+ if (!scheme) {
64
+ throw new Error(`Unknown color scheme: "${settings.colorScheme}". Available: ${Object.keys(colorSchemes).join(", ")}`);
65
+ }
66
+ return scheme;
67
+ }
68
+
69
+ /** Resolve a ColorRef to a concrete color string.
70
+ * - number → palette[value]
71
+ * - string → used as-is
72
+ * - undefined → fallback */
73
+ export function resolveColor(
74
+ value: ColorRef | undefined,
75
+ palette: string[],
76
+ fallback: string,
77
+ ): string {
78
+ if (value === undefined) return fallback;
79
+ if (typeof value === "number") return palette[value] ?? fallback;
80
+ return value;
81
+ }
82
+
83
+ /** Resolve semantic colors with fallbacks to default palette slots */
84
+ export function resolveSemanticColors(scheme: ColorScheme) {
85
+ const p = scheme.palette;
86
+ return {
87
+ surface: resolveColor(scheme.semantic?.surface, p, p[0]),
88
+ muted: resolveColor(scheme.semantic?.muted, p, p[8]),
89
+ accent: resolveColor(scheme.semantic?.accent, p, p[5]),
90
+ accentHover: resolveColor(scheme.semantic?.accentHover, p, p[14]),
91
+ codeBg: resolveColor(scheme.semantic?.codeBg, p, p[10]),
92
+ codeFg: resolveColor(scheme.semantic?.codeFg, p, p[11]),
93
+ success: resolveColor(scheme.semantic?.success, p, p[2]),
94
+ danger: resolveColor(scheme.semantic?.danger, p, p[1]),
95
+ warning: resolveColor(scheme.semantic?.warning, p, p[3]),
96
+ info: resolveColor(scheme.semantic?.info, p, p[4]),
97
+ mermaidNodeBg: resolveColor(scheme.semantic?.mermaidNodeBg, p, p[9]),
98
+ mermaidText: resolveColor(scheme.semantic?.mermaidText, p, p[11]),
99
+ mermaidLine: resolveColor(scheme.semantic?.mermaidLine, p, p[8]),
100
+ mermaidLabelBg: resolveColor(scheme.semantic?.mermaidLabelBg, p, p[10]),
101
+ mermaidNoteBg: resolveColor(scheme.semantic?.mermaidNoteBg, p, p[0]),
102
+ chatUserBg: resolveColor(scheme.semantic?.chatUserBg, p, p[5]),
103
+ chatUserText: resolveColor(scheme.semantic?.chatUserText, p, p[9]),
104
+ chatAssistantBg: resolveColor(scheme.semantic?.chatAssistantBg, p, p[9]),
105
+ chatAssistantText: resolveColor(scheme.semantic?.chatAssistantText, p, p[11]),
106
+ imageOverlayBg: resolveColor(scheme.semantic?.imageOverlayBg, p, p[0]),
107
+ imageOverlayFg: resolveColor(scheme.semantic?.imageOverlayFg, p, p[11]),
108
+ matchedKeywordBg: resolveColor(scheme.semantic?.matchedKeywordBg, p, p[3]),
109
+ matchedKeywordFg: resolveColor(scheme.semantic?.matchedKeywordFg, p, p[15]),
110
+ };
111
+ }
112
+
113
+ export function schemeToCssPairs(scheme: ColorScheme): [string, string][] {
114
+ const p = scheme.palette;
115
+ const sem = resolveSemanticColors(scheme);
116
+ return [
117
+ ["--zd-bg", resolveColor(scheme.background, p, p[0])],
118
+ ["--zd-fg", resolveColor(scheme.foreground, p, p[15])],
119
+ ["--zd-cursor", resolveColor(scheme.cursor, p, p[6])],
120
+ ["--zd-sel-bg", resolveColor(scheme.selectionBg, p, resolveColor(scheme.background, p, p[0]))],
121
+ ["--zd-sel-fg", resolveColor(scheme.selectionFg, p, resolveColor(scheme.foreground, p, p[15]))],
122
+ ...p.map((color, i) => [`--zd-${i}`, color] as [string, string]),
123
+ ["--zd-surface", sem.surface],
124
+ ["--zd-muted", sem.muted],
125
+ ["--zd-accent", sem.accent],
126
+ ["--zd-accent-hover", sem.accentHover],
127
+ ["--zd-code-bg", sem.codeBg],
128
+ ["--zd-code-fg", sem.codeFg],
129
+ ["--zd-success", sem.success],
130
+ ["--zd-danger", sem.danger],
131
+ ["--zd-warning", sem.warning],
132
+ ["--zd-info", sem.info],
133
+ ["--zd-mermaid-node-bg", sem.mermaidNodeBg],
134
+ ["--zd-mermaid-text", sem.mermaidText],
135
+ ["--zd-mermaid-line", sem.mermaidLine],
136
+ ["--zd-mermaid-label-bg", sem.mermaidLabelBg],
137
+ ["--zd-mermaid-note-bg", sem.mermaidNoteBg],
138
+ ["--zd-chat-user-bg", sem.chatUserBg],
139
+ ["--zd-chat-user-text", sem.chatUserText],
140
+ ["--zd-chat-assistant-bg", sem.chatAssistantBg],
141
+ ["--zd-chat-assistant-text", sem.chatAssistantText],
142
+ ["--zd-image-overlay-bg", sem.imageOverlayBg],
143
+ ["--zd-image-overlay-fg", sem.imageOverlayFg],
144
+ ["--zd-matched-keyword-bg", sem.matchedKeywordBg],
145
+ ["--zd-matched-keyword-fg", sem.matchedKeywordFg],
146
+ ];
147
+ }
148
+
149
+ export function generateCssCustomProperties(): string {
150
+ const scheme = getActiveScheme();
151
+ const pairs = schemeToCssPairs(scheme);
152
+ const lines = [":root {", ...pairs.map(([prop, value]) => ` ${prop}: ${value};`), "}"];
153
+ return lines.join("\n");
154
+ }
155
+
156
+ export function generateLightDarkCssProperties(): string {
157
+ if (!settings.colorMode) {
158
+ throw new Error("colorMode is not configured");
159
+ }
160
+ const { lightScheme, darkScheme } = settings.colorMode;
161
+ const light = colorSchemes[lightScheme];
162
+ const dark = colorSchemes[darkScheme];
163
+ if (!light) throw new Error(`Unknown light scheme: "${lightScheme}"`);
164
+ if (!dark) throw new Error(`Unknown dark scheme: "${darkScheme}"`);
165
+
166
+ const lightPairs = schemeToCssPairs(light);
167
+ const darkPairs = schemeToCssPairs(dark);
168
+
169
+ if (lightPairs.length !== darkPairs.length) {
170
+ throw new Error(`Light scheme has ${lightPairs.length} properties but dark scheme has ${darkPairs.length}`);
171
+ }
172
+
173
+ const lines = [":root {", " color-scheme: light dark;"];
174
+ for (let i = 0; i < lightPairs.length; i++) {
175
+ const prop = lightPairs[i][0];
176
+ const lightVal = lightPairs[i][1];
177
+ const darkVal = darkPairs[i][1];
178
+ lines.push(` ${prop}: light-dark(${lightVal}, ${darkVal});`);
179
+ }
180
+ lines.push("}");
181
+ return lines.join("\n");
182
+ }