create-zudo-doc 0.2.0 → 0.2.1

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 (72) hide show
  1. package/dist/api.js +4 -1
  2. package/dist/cli.js +4 -6
  3. package/dist/preset.js +11 -0
  4. package/dist/prompts.js +2 -6
  5. package/dist/scaffold.js +15 -9
  6. package/dist/settings-gen.js +7 -7
  7. package/dist/utils.d.ts +8 -0
  8. package/dist/utils.js +25 -0
  9. package/dist/zfb-config-gen.js +11 -50
  10. package/package.json +1 -1
  11. package/templates/base/pages/_data.ts +10 -23
  12. package/templates/base/pages/docs/[[...slug]].tsx +27 -168
  13. package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
  14. package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
  15. package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
  16. package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
  17. package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
  18. package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
  19. package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
  20. package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
  21. package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
  22. package/templates/base/pages/lib/_header-with-defaults.tsx +51 -89
  23. package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
  24. package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
  25. package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
  26. package/templates/base/pages/lib/_search-widget-script.ts +32 -9
  27. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
  28. package/templates/base/pages/lib/locale-merge.ts +1 -1
  29. package/templates/base/pages/lib/route-enumerators.ts +11 -7
  30. package/templates/base/plugins/connect-adapter.mjs +30 -1
  31. package/templates/base/plugins/copy-public-plugin.mjs +10 -2
  32. package/templates/base/plugins/search-index-plugin.mjs +20 -8
  33. package/templates/base/src/components/sidebar-toggle.tsx +1 -1
  34. package/templates/base/src/components/sidebar-tree.tsx +10 -4
  35. package/templates/base/src/config/color-schemes.ts +4 -0
  36. package/templates/base/src/config/docs-schema.ts +94 -0
  37. package/templates/base/src/config/i18n.ts +10 -3
  38. package/templates/base/src/styles/global.css +14 -0
  39. package/templates/base/src/types/docs-entry.ts +8 -26
  40. package/templates/base/src/utils/base.ts +5 -3
  41. package/templates/base/src/utils/docs.ts +144 -169
  42. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
  43. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
  44. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
  45. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
  46. package/templates/features/docHistory/files/src/components/doc-history.tsx +28 -8
  47. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
  48. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
  49. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
  50. package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
  51. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
  52. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
  53. package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
  54. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
  55. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
  56. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
  57. package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
  58. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
  59. package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
  60. package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
  61. package/templates/base/src/components/content/heading-h3.tsx +0 -20
  62. package/templates/base/src/components/theme-toggle.tsx +0 -107
  63. package/templates/base/src/hooks/use-active-heading.ts +0 -133
  64. package/templates/base/src/plugins/docs-source-map.ts +0 -103
  65. package/templates/base/src/plugins/hast-utils.ts +0 -10
  66. package/templates/base/src/plugins/rehype-code-title.ts +0 -50
  67. package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
  68. package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
  69. package/templates/base/src/plugins/url-utils.ts +0 -4
  70. package/templates/base/src/utils/dedent.ts +0 -24
  71. package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
  72. package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
@@ -1,53 +0,0 @@
1
- import type { Root, Element } from "hast";
2
- import GithubSlugger from "github-slugger";
3
- import { visit } from "unist-util-visit";
4
- import { extractText } from "./hast-utils";
5
-
6
- /**
7
- * Rehype plugin that adds Docusaurus-style anchor links to headings (h2-h6).
8
- *
9
- * Generates heading IDs (via github-slugger) and appends:
10
- * <a href="#id" class="hash-link" aria-label="Direct link to ..."></a>
11
- *
12
- * The "#" symbol is rendered via CSS ::after to avoid polluting
13
- * heading text extraction (used for TOC).
14
- *
15
- * Sets heading IDs itself (via github-slugger); skips headings that already
16
- * have an ID assigned upstream.
17
- */
18
-
19
- const headingTags = new Set(["h2", "h3", "h4", "h5", "h6"]);
20
-
21
- export function rehypeHeadingLinks() {
22
- return (tree: Root) => {
23
- const slugger = new GithubSlugger();
24
-
25
- visit(tree, "element", (node: Element) => {
26
- if (!headingTags.has(node.tagName)) return;
27
-
28
- const text = node.children
29
- .map((c) => extractText(c))
30
- .join("");
31
-
32
- const id =
33
- (node.properties?.id as string | undefined) || slugger.slug(text);
34
-
35
- // Set the id if not already present
36
- if (!node.properties) node.properties = {};
37
- if (!node.properties.id) node.properties.id = id;
38
-
39
- const link: Element = {
40
- type: "element",
41
- tagName: "a",
42
- properties: {
43
- href: `#${id}`,
44
- className: ["hash-link"],
45
- "aria-label": `Direct link to ${text}`,
46
- },
47
- children: [],
48
- };
49
-
50
- node.children.push(link);
51
- });
52
- };
53
- }
@@ -1,41 +0,0 @@
1
- import type { Root, Element } from "hast";
2
- import { visit } from "unist-util-visit";
3
- import { extractText } from "./hast-utils";
4
-
5
- /**
6
- * Rehype plugin that transforms mermaid code blocks into renderable containers.
7
- *
8
- * After Shiki processes code blocks, mermaid blocks become:
9
- * <pre data-language="mermaid"><code><span>...</span></code></pre>
10
- *
11
- * This plugin converts them to:
12
- * <div class="mermaid" data-mermaid>graph LR; A-->B</div>
13
- */
14
-
15
- export function rehypeMermaid() {
16
- return (tree: Root) => {
17
- visit(tree, "element", (node: Element, index, parent) => {
18
- if (
19
- node.tagName !== "pre" ||
20
- !parent ||
21
- index === undefined
22
- ) return;
23
-
24
- // Match Shiki-processed mermaid blocks (data-language="mermaid")
25
- if (node.properties?.dataLanguage !== "mermaid") return;
26
-
27
- // Extract all text content recursively from the code/span tree
28
- const text = node.children
29
- .map((c) => extractText(c))
30
- .join("");
31
-
32
- // Replace the <pre> with a <div class="mermaid">
33
- (parent as Element).children[index] = {
34
- type: "element",
35
- tagName: "div",
36
- properties: { className: ["mermaid"], "data-mermaid": true },
37
- children: [{ type: "text", value: text }],
38
- };
39
- });
40
- };
41
- }
@@ -1,4 +0,0 @@
1
- /** Check if a URL is external (has a protocol scheme). */
2
- export function isExternal(url: string): boolean {
3
- return /^[a-z][a-z0-9+.-]*:/i.test(url);
4
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Strip common leading whitespace from all lines of a template literal string.
3
- * Similar to Python's textwrap.dedent().
4
- */
5
- export function dedent(text: string): string {
6
- const lines = text.split('\n');
7
-
8
- // Find minimum indentation (ignoring empty/whitespace-only lines)
9
- let minIndent = Infinity;
10
- for (const line of lines) {
11
- if (line.trim().length === 0) continue;
12
- const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
13
- if (indent < minIndent) minIndent = indent;
14
- }
15
-
16
- if (minIndent === 0 || minIndent === Infinity) {
17
- return text.trim();
18
- }
19
-
20
- return lines
21
- .map((line) => (line.trim().length === 0 ? '' : line.slice(minIndent)))
22
- .join('\n')
23
- .trim();
24
- }
@@ -1,180 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import path from "node:path";
3
- import type { DocHistoryEntry, DocHistoryData } from "@/types/doc-history";
4
- import { collectMdFiles } from "./content-files";
5
-
6
- /** Shared options to suppress git stderr noise */
7
- const QUIET: { encoding: "utf-8"; stdio: ["pipe", "pipe", "pipe"] } = {
8
- encoding: "utf-8",
9
- stdio: ["pipe", "pipe", "pipe"],
10
- };
11
-
12
- /** Cache the repo root to avoid repeated git calls */
13
- let repoRootCache: string | null = null;
14
-
15
- function getRepoRoot(): string {
16
- if (repoRootCache) return repoRootCache;
17
- repoRootCache = execFileSync("git", ["rev-parse", "--show-toplevel"], {
18
- encoding: "utf-8",
19
- }).trim();
20
- return repoRootCache;
21
- }
22
-
23
- /** Convert an absolute path to a repo-relative path for git commands */
24
- function toRepoRelative(absolutePath: string): string {
25
- return path.relative(getRepoRoot(), absolutePath);
26
- }
27
-
28
- /**
29
- * Get the list of commit hashes that touched a file, newest first.
30
- * Uses --follow to track renames.
31
- * Limits to maxEntries commits (default 50).
32
- */
33
- export function getFileCommits(
34
- filePath: string,
35
- maxEntries = 50,
36
- ): string[] {
37
- try {
38
- const output = execFileSync(
39
- "git",
40
- [
41
- "log",
42
- "--follow",
43
- "--format=%H",
44
- "-n",
45
- String(maxEntries),
46
- "--",
47
- filePath,
48
- ],
49
- QUIET,
50
- ).trim();
51
- return output ? [...new Set(output.split("\n"))] : [];
52
- } catch {
53
- return [];
54
- }
55
- }
56
-
57
- /**
58
- * Get metadata for a specific commit on a file.
59
- * Returns { hash, date, author, message } with full hash for unique identification.
60
- */
61
- export function getCommitInfo(
62
- hash: string,
63
- filePath: string,
64
- ): Omit<DocHistoryEntry, "content"> {
65
- try {
66
- const output = execFileSync(
67
- "git",
68
- ["log", "-1", "--format=%H%n%aI%n%aN%n%s", hash, "--", filePath],
69
- QUIET,
70
- ).trim();
71
- const lines = output.split("\n");
72
- return {
73
- hash: lines[0] ?? hash,
74
- date: lines[1] ?? "",
75
- author: lines[2] ?? "",
76
- message: lines[3] ?? "",
77
- };
78
- } catch {
79
- return { hash, date: "", author: "", message: "" };
80
- }
81
- }
82
-
83
- /**
84
- * Get the file content at a specific commit.
85
- * Accepts absolute paths and converts to repo-relative for git show.
86
- * Handles renamed files by falling back to the old path via git log --follow.
87
- */
88
- export function getFileAtCommit(hash: string, filePath: string): string {
89
- const relPath = path.isAbsolute(filePath)
90
- ? toRepoRelative(filePath)
91
- : filePath;
92
-
93
- try {
94
- return execFileSync("git", ["show", `${hash}:${relPath}`], QUIET);
95
- } catch {
96
- // File may have been renamed — find the old path at this commit
97
- try {
98
- const oldPath = execFileSync(
99
- "git",
100
- [
101
- "log",
102
- "-1",
103
- "--follow",
104
- "--diff-filter=R",
105
- "--format=",
106
- "--name-only",
107
- hash,
108
- "--",
109
- relPath,
110
- ],
111
- QUIET,
112
- ).trim();
113
- if (oldPath) {
114
- return execFileSync("git", ["show", `${hash}:${oldPath}`], QUIET);
115
- }
116
- } catch {
117
- // ignore
118
- }
119
-
120
- // Last resort: use git log --follow to find the path at this revision
121
- try {
122
- const followOutput = execFileSync(
123
- "git",
124
- [
125
- "log",
126
- "--follow",
127
- "--format=%H",
128
- "--name-only",
129
- "--diff-filter=AMRC",
130
- "--",
131
- relPath,
132
- ],
133
- QUIET,
134
- ).trim();
135
- const lines = followOutput.split("\n").filter(Boolean);
136
- // Lines alternate: hash, filename, hash, filename...
137
- for (let i = 0; i < lines.length - 1; i += 2) {
138
- if (lines[i] === hash && lines[i + 1]) {
139
- return execFileSync(
140
- "git",
141
- ["show", `${hash}:${lines[i + 1]}`],
142
- QUIET,
143
- );
144
- }
145
- }
146
- } catch {
147
- // ignore
148
- }
149
-
150
- return "";
151
- }
152
- }
153
-
154
- /**
155
- * Get the complete history for a document file.
156
- * Returns DocHistoryData with all entries populated.
157
- */
158
- export function getDocHistory(
159
- filePath: string,
160
- slug: string,
161
- maxEntries = 50,
162
- ): DocHistoryData {
163
- const commits = getFileCommits(filePath, maxEntries);
164
- const entries: DocHistoryEntry[] = commits.map((hash) => {
165
- const info = getCommitInfo(hash, filePath);
166
- const content = getFileAtCommit(hash, filePath);
167
- return { ...info, content };
168
- });
169
- return { slug, filePath, entries };
170
- }
171
-
172
- /**
173
- * Collect all MDX/md files in a content directory.
174
- * Delegates to the shared collectMdFiles utility from content-files.ts.
175
- */
176
- export function collectContentFiles(
177
- contentDir: string,
178
- ): Array<{ filePath: string; slug: string }> {
179
- return collectMdFiles(contentDir);
180
- }
@@ -1,198 +0,0 @@
1
- export function initSidebarResizer() {
2
- const sidebar = document.getElementById("desktop-sidebar");
3
- if (!sidebar || sidebar.querySelector("[data-sidebar-resizer]")) return;
4
- // Only attach to the real fixed desktop panel. On hide_sidebar pages the
5
- // aside renders sr-only (position:absolute) purely for the ARIA landmark; a
6
- // position:fixed handle would escape sr-only's clip and show a stray strip
7
- // (below lg the panel is display:none but still fixed — handle appended, not rendered). zudolab/zudo-doc#1821
8
- if (getComputedStyle(sidebar).position !== "fixed") return;
9
-
10
- // Resizer allows a wider range (192–448px) than the CSS default
11
- // (clamp(14rem, 20vw, 22rem) = 224–352px at 16px base).
12
- // CSS provides the responsive initial width; the resizer lets users
13
- // go beyond that range when explicitly dragging or using keyboard arrows.
14
- const MIN_W = 192;
15
- const MAX_W = 448;
16
- const STEP = 10;
17
- const LS_KEY = "zudo-doc-sidebar-width";
18
- const CSS_PROP = "--zd-sidebar-w";
19
- const ACCENT_BG = "var(--zd-accent, rgba(128,128,128,0.3))";
20
- const ACCENT_OUTLINE = "2px solid var(--zd-accent, rgba(128,128,128,0.5))";
21
- const ACCENT_GHOST = "var(--zd-accent, rgba(128,128,128,0.5))";
22
-
23
- function readCurrentWidth(): number {
24
- const raw = getComputedStyle(document.documentElement).getPropertyValue(CSS_PROP);
25
- return raw ? parseFloat(raw) || MIN_W : MIN_W;
26
- }
27
-
28
- let cachedWidth = readCurrentWidth();
29
-
30
- const handle = document.createElement("div");
31
- handle.setAttribute("data-sidebar-resizer", "");
32
- handle.setAttribute("tabindex", "0");
33
- handle.setAttribute("role", "separator");
34
- handle.setAttribute("aria-orientation", "vertical");
35
- handle.setAttribute("aria-label", "Resize sidebar");
36
- handle.setAttribute("aria-valuemin", String(MIN_W));
37
- handle.setAttribute("aria-valuemax", String(MAX_W));
38
- handle.setAttribute("aria-valuenow", String(Math.round(cachedWidth)));
39
- // position:fixed (not absolute) pins the handle to the viewport so it spans
40
- // the sidebar's full height even while #desktop-sidebar scrolls. As an
41
- // absolute child of the overflow-y:auto sidebar the handle scrolled away with
42
- // the content and its height:100% only resolved to the visible padding box,
43
- // so the bottom of the sidebar lost its grab strip once scrolled. zudolab/zudo-doc#1821
44
- //
45
- // top:3.5rem + left:calc mirror the doc-layout #desktop-sidebar geometry
46
- // (top-[3.5rem], left:0, width:var(--zd-sidebar-w)) — those layout constants
47
- // live in the same package's doc-layout.tsx. 20px is wider than every common
48
- // native y-scrollbar (~12-17px on Win/Linux classic; 0 on macOS overlay) so a
49
- // draggable strip always remains visible to the LEFT of the scrollbar when
50
- // sidebar content overflows. zudolab/zudo-doc#1660
51
- Object.assign(handle.style, {
52
- position: "fixed",
53
- top: "3.5rem",
54
- bottom: "0",
55
- left: "calc(var(--zd-sidebar-w) - 20px)",
56
- width: "20px",
57
- cursor: "col-resize",
58
- zIndex: "10",
59
- transition: "background 0.15s",
60
- });
61
-
62
- let dragging = false;
63
-
64
- function applyWidth(w: number) {
65
- cachedWidth = Math.max(MIN_W, Math.min(MAX_W, w));
66
- document.documentElement.style.setProperty(CSS_PROP, cachedWidth + "px");
67
- try { localStorage.setItem(LS_KEY, String(Math.round(cachedWidth))); } catch {}
68
- handle.setAttribute("aria-valuenow", String(Math.round(cachedWidth)));
69
- }
70
-
71
- let focused = false;
72
-
73
- function updateHandleVisual() {
74
- if (dragging || focused) {
75
- handle.style.background = ACCENT_BG;
76
- } else {
77
- handle.style.background = "";
78
- }
79
- handle.style.outline = focused && !dragging ? ACCENT_OUTLINE : "";
80
- handle.style.outlineOffset = focused && !dragging ? "1px" : "";
81
- }
82
-
83
- handle.addEventListener("focus", () => {
84
- focused = true;
85
- updateHandleVisual();
86
- });
87
- handle.addEventListener("blur", () => {
88
- focused = false;
89
- updateHandleVisual();
90
- });
91
-
92
- handle.addEventListener("keydown", (e: KeyboardEvent) => {
93
- let w = cachedWidth;
94
- switch (e.key) {
95
- case "ArrowLeft":
96
- w = Math.max(MIN_W, w - STEP);
97
- break;
98
- case "ArrowRight":
99
- w = Math.min(MAX_W, w + STEP);
100
- break;
101
- case "Home":
102
- w = MIN_W;
103
- break;
104
- case "End":
105
- w = MAX_W;
106
- break;
107
- default:
108
- return;
109
- }
110
- e.preventDefault();
111
- applyWidth(w);
112
- });
113
-
114
- handle.addEventListener("mouseenter", () => {
115
- if (!dragging && !focused) handle.style.background = ACCENT_BG;
116
- });
117
- handle.addEventListener("mouseleave", () => {
118
- if (!dragging && !focused) handle.style.background = "";
119
- });
120
-
121
- handle.addEventListener("pointerdown", (e: PointerEvent) => {
122
- e.preventDefault();
123
- handle.setPointerCapture(e.pointerId);
124
- dragging = true;
125
- updateHandleVisual();
126
- document.documentElement.style.cursor = "col-resize";
127
- document.documentElement.style.userSelect = "none";
128
-
129
- // Ghost line — cheap to move (no reflow), shows target position
130
- const ghost = document.createElement("div");
131
- Object.assign(ghost.style, {
132
- position: "fixed",
133
- top: "0",
134
- width: "2px",
135
- height: "100vh",
136
- background: ACCENT_GHOST,
137
- pointerEvents: "none",
138
- zIndex: "9999",
139
- });
140
- const sidebarRect = sidebar.getBoundingClientRect();
141
- const sidebarLeft = sidebarRect.left;
142
- ghost.style.left = (sidebarLeft + sidebarRect.width) + "px";
143
- document.body.appendChild(ghost);
144
- let targetWidth = 0;
145
- let cleaned = false;
146
-
147
- const onMove = (ev: PointerEvent) => {
148
- targetWidth = Math.max(MIN_W, Math.min(MAX_W, ev.clientX - sidebarLeft));
149
- ghost.style.left = (sidebarLeft + targetWidth) + "px";
150
- };
151
-
152
- const cleanup = () => {
153
- if (cleaned) return;
154
- cleaned = true;
155
- dragging = false;
156
- updateHandleVisual();
157
- document.documentElement.style.cursor = "";
158
- document.documentElement.style.userSelect = "";
159
- ghost.remove();
160
- handle.removeEventListener("pointermove", onMove);
161
- handle.removeEventListener("pointerup", onUp);
162
- handle.removeEventListener("pointercancel", onCancel);
163
- handle.removeEventListener("lostpointercapture", onLost);
164
- };
165
-
166
- const commit = () => {
167
- if (targetWidth > 0) applyWidth(targetWidth);
168
- };
169
-
170
- // pointerup: normal end-of-drag. Commit, then teardown.
171
- const onUp = () => {
172
- commit();
173
- cleanup();
174
- };
175
-
176
- // lostpointercapture: per spec fires AFTER pointerup, but browsers reorder
177
- // these in edge cases (cursor near y-scrollbar, fast drags, OS handoff).
178
- // Commit here too so a real drag still applies if pointerup is dropped.
179
- // Idempotent with onUp via the `cleaned` guard.
180
- const onLost = () => {
181
- commit();
182
- cleanup();
183
- };
184
-
185
- // pointercancel: actual user/OS cancellation (touch interrupted, etc.).
186
- // Do NOT commit — caller intent was to abort.
187
- const onCancel = () => {
188
- cleanup();
189
- };
190
-
191
- handle.addEventListener("pointermove", onMove);
192
- handle.addEventListener("pointerup", onUp);
193
- handle.addEventListener("pointercancel", onCancel);
194
- handle.addEventListener("lostpointercapture", onLost);
195
- });
196
-
197
- sidebar.appendChild(handle);
198
- }