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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/create-zudo-doc.js +2 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +13 -0
- package/dist/claude-md-gen.d.ts +2 -0
- package/dist/claude-md-gen.js +113 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +157 -0
- package/dist/compose.d.ts +95 -0
- package/dist/compose.js +206 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +224 -0
- package/dist/features/body-foot-util.d.ts +10 -0
- package/dist/features/body-foot-util.js +12 -0
- package/dist/features/claude-resources.d.ts +2 -0
- package/dist/features/claude-resources.js +6 -0
- package/dist/features/design-token-panel.d.ts +14 -0
- package/dist/features/design-token-panel.js +27 -0
- package/dist/features/doc-history.d.ts +9 -0
- package/dist/features/doc-history.js +11 -0
- package/dist/features/doc-tags.d.ts +19 -0
- package/dist/features/doc-tags.js +33 -0
- package/dist/features/footer-taglist.d.ts +14 -0
- package/dist/features/footer-taglist.js +17 -0
- package/dist/features/footer.d.ts +8 -0
- package/dist/features/footer.js +10 -0
- package/dist/features/i18n.d.ts +22 -0
- package/dist/features/i18n.js +41 -0
- package/dist/features/image-enlarge.d.ts +11 -0
- package/dist/features/image-enlarge.js +13 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.js +53 -0
- package/dist/features/llms-txt.d.ts +11 -0
- package/dist/features/llms-txt.js +13 -0
- package/dist/features/search.d.ts +9 -0
- package/dist/features/search.js +11 -0
- package/dist/features/sidebar-resizer.d.ts +14 -0
- package/dist/features/sidebar-resizer.js +16 -0
- package/dist/features/sidebar-toggle.d.ts +13 -0
- package/dist/features/sidebar-toggle.js +15 -0
- package/dist/features/tag-governance.d.ts +14 -0
- package/dist/features/tag-governance.js +16 -0
- package/dist/features/tauri-dev.d.ts +2 -0
- package/dist/features/tauri-dev.js +25 -0
- package/dist/features/tauri.d.ts +11 -0
- package/dist/features/tauri.js +52 -0
- package/dist/features/versioning.d.ts +27 -0
- package/dist/features/versioning.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +150 -0
- package/dist/preset.d.ts +37 -0
- package/dist/preset.js +156 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +248 -0
- package/dist/scaffold.d.ts +4 -0
- package/dist/scaffold.js +344 -0
- package/dist/settings-gen.d.ts +2 -0
- package/dist/settings-gen.js +237 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +34 -0
- package/dist/zfb-config-gen.d.ts +19 -0
- package/dist/zfb-config-gen.js +222 -0
- package/package.json +65 -0
- package/templates/base/.htmlvalidate.json +5 -0
- package/templates/base/.zfb/doc-history-meta.json +1 -0
- package/templates/base/pages/404.tsx +55 -0
- package/templates/base/pages/_data.ts +179 -0
- package/templates/base/pages/_mdx-components.ts +249 -0
- package/templates/base/pages/docs/[...slug].tsx +448 -0
- package/templates/base/pages/index.tsx +158 -0
- package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
- package/templates/base/pages/lib/_category-nav.tsx +148 -0
- package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
- package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
- package/templates/base/pages/lib/_details.tsx +30 -0
- package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
- package/templates/base/pages/lib/_extract-headings.ts +81 -0
- package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
- package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
- package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
- package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
- package/templates/base/pages/lib/_math-block.tsx +63 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
- package/templates/base/pages/lib/_preset-generator.tsx +81 -0
- package/templates/base/pages/lib/_search-widget-script.ts +388 -0
- package/templates/base/pages/lib/_search-widget.tsx +196 -0
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
- package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
- package/templates/base/pages/lib/locale-merge.ts +58 -0
- package/templates/base/pages/lib/route-enumerators.ts +302 -0
- package/templates/base/pages/sitemap.xml.tsx +51 -0
- package/templates/base/plugins/connect-adapter.mjs +144 -0
- package/templates/base/plugins/copy-public-plugin.mjs +50 -0
- package/templates/base/plugins/search-index-plugin.mjs +54 -0
- package/templates/base/scripts/run-b4push.sh +102 -0
- package/templates/base/src/components/ai-chat-modal.tsx +15 -0
- package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
- package/templates/base/src/components/content/component-map.ts +25 -0
- package/templates/base/src/components/content/content-blockquote.tsx +16 -0
- package/templates/base/src/components/content/content-code.tsx +117 -0
- package/templates/base/src/components/content/content-link.tsx +83 -0
- package/templates/base/src/components/content/content-ol.tsx +19 -0
- package/templates/base/src/components/content/content-paragraph.tsx +10 -0
- package/templates/base/src/components/content/content-strong.tsx +16 -0
- package/templates/base/src/components/content/content-table.tsx +18 -0
- package/templates/base/src/components/content/content-ul.tsx +18 -0
- package/templates/base/src/components/content/heading-h2.tsx +26 -0
- package/templates/base/src/components/content/heading-h3.tsx +26 -0
- package/templates/base/src/components/content/heading-h4.tsx +26 -0
- package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
- package/templates/base/src/components/doc-history.tsx +18 -0
- package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
- package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
- package/templates/base/src/components/html-preview/preflight.ts +112 -0
- package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
- package/templates/base/src/components/image-enlarge.tsx +19 -0
- package/templates/base/src/components/mobile-toc.tsx +94 -0
- package/templates/base/src/components/preset-generator.tsx +14 -0
- package/templates/base/src/components/sidebar-toggle.tsx +98 -0
- package/templates/base/src/components/sidebar-tree.tsx +543 -0
- package/templates/base/src/components/site-tree-nav.tsx +233 -0
- package/templates/base/src/components/theme-toggle.tsx +93 -0
- package/templates/base/src/components/toc.tsx +63 -0
- package/templates/base/src/components/tree-nav-shared.tsx +71 -0
- package/templates/base/src/config/color-scheme-utils.ts +182 -0
- package/templates/base/src/config/color-schemes.ts +128 -0
- package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
- package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
- package/templates/base/src/config/i18n.ts +225 -0
- package/templates/base/src/config/settings-types.ts +162 -0
- package/templates/base/src/config/sidebars.ts +66 -0
- package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
- package/templates/base/src/config/tag-vocabulary.ts +20 -0
- package/templates/base/src/hooks/use-active-heading.ts +133 -0
- package/templates/base/src/plugins/docs-source-map.ts +103 -0
- package/templates/base/src/plugins/hast-utils.ts +10 -0
- package/templates/base/src/plugins/rehype-code-title.ts +50 -0
- package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
- package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
- package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
- package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
- package/templates/base/src/plugins/remark-admonitions.ts +99 -0
- package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
- package/templates/base/src/plugins/url-utils.ts +4 -0
- package/templates/base/src/styles/global.css +1066 -0
- package/templates/base/src/types/docs-entry.ts +39 -0
- package/templates/base/src/types/heading.ts +5 -0
- package/templates/base/src/types/locale.ts +10 -0
- package/templates/base/src/utils/base.ts +139 -0
- package/templates/base/src/utils/content-files.ts +106 -0
- package/templates/base/src/utils/dedent.ts +24 -0
- package/templates/base/src/utils/docs.ts +335 -0
- package/templates/base/src/utils/git-info.ts +70 -0
- package/templates/base/src/utils/github.ts +19 -0
- package/templates/base/src/utils/header-right-items.ts +38 -0
- package/templates/base/src/utils/nav-scope.ts +63 -0
- package/templates/base/src/utils/sidebar.ts +104 -0
- package/templates/base/src/utils/slug.ts +10 -0
- package/templates/base/src/utils/smart-break.tsx +126 -0
- package/templates/base/src/utils/tags.ts +126 -0
- package/templates/base/tsconfig.json +36 -0
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
- package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
- package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
- package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
- package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
- package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
- package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
- package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
- package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
- package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
- package/templates/features/tauri/files/src-tauri/build.rs +3 -0
- package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
- package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
- package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
- package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
- package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
- package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
- package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
- package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
- package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
- package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
- package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
- package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
- package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
- package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
|
@@ -0,0 +1,543 @@
|
|
|
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.
|
|
7
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from "preact/hooks";
|
|
8
|
+
import type { NavNode } from "@/utils/docs";
|
|
9
|
+
import type { LocaleLink } from "@/types/locale";
|
|
10
|
+
import { INDENT, BASE_PAD, connectorLeft, ConnectorLines, CategoryLinkIcon } from "./tree-nav-shared";
|
|
11
|
+
import ThemeToggle from "@/components/theme-toggle";
|
|
12
|
+
import { smartBreakToHtml } from "@/utils/smart-break";
|
|
13
|
+
|
|
14
|
+
function ToggleChevron({ isExpanded, className }: { isExpanded: boolean; className?: string }) {
|
|
15
|
+
return (
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
className={`h-[0.625rem] w-[0.625rem] shrink-0 transition-transform duration-150 ${isExpanded ? "rotate-90" : ""} ${className ?? ""}`}
|
|
19
|
+
fill="none"
|
|
20
|
+
viewBox="0 0 24 24"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
strokeWidth={2}
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
>
|
|
25
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STORAGE_KEY = "zd-sidebar-open";
|
|
31
|
+
|
|
32
|
+
function padLeft(depth: number, forCategory: boolean): string {
|
|
33
|
+
if (depth === 0) return `calc(${BASE_PAD} + ${forCategory ? "0.15rem" : "0rem"})`;
|
|
34
|
+
return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getOpenSet(): Set<string> {
|
|
38
|
+
try {
|
|
39
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
40
|
+
if (!raw) return new Set();
|
|
41
|
+
const parsed: unknown = JSON.parse(raw);
|
|
42
|
+
return Array.isArray(parsed) ? new Set(parsed.filter((v): v is string => typeof v === "string")) : new Set();
|
|
43
|
+
} catch {
|
|
44
|
+
return new Set();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function saveOpenSet(set: Set<string>) {
|
|
49
|
+
try {
|
|
50
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify([...set]));
|
|
51
|
+
} catch {
|
|
52
|
+
// ignore
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizePath(p: string): string {
|
|
57
|
+
return p.replace(/\/$/, "") || "/";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Find the slug of the node whose href matches the given pathname */
|
|
61
|
+
function findActiveSlug(nodes: NavNode[], pathname: string): string | undefined {
|
|
62
|
+
for (const node of nodes) {
|
|
63
|
+
if (node.href && normalizePath(node.href) === pathname) return node.slug;
|
|
64
|
+
const found = findActiveSlug(node.children, pathname);
|
|
65
|
+
if (found) return found;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Track current active slug, updating on View Transition navigations */
|
|
71
|
+
function useActiveSlug(nodes: NavNode[], initial?: string): string | undefined {
|
|
72
|
+
const [slug, setSlug] = useState(initial);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const update = () => {
|
|
76
|
+
const pathname = normalizePath(window.location.pathname);
|
|
77
|
+
const found = findActiveSlug(nodes, pathname);
|
|
78
|
+
if (found !== undefined) setSlug(found);
|
|
79
|
+
};
|
|
80
|
+
update();
|
|
81
|
+
// zfb's `<ViewTransitions />` does a real page load on every
|
|
82
|
+
// navigation, so `DOMContentLoaded` is the post-navigate signal.
|
|
83
|
+
document.addEventListener("DOMContentLoaded", update);
|
|
84
|
+
return () => document.removeEventListener("DOMContentLoaded", update);
|
|
85
|
+
}, [nodes]);
|
|
86
|
+
|
|
87
|
+
return slug;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function filterTree(nodes: NavNode[], query: string): NavNode[] {
|
|
91
|
+
return nodes.reduce<NavNode[]>((acc, node) => {
|
|
92
|
+
const matchesLabel = node.label.toLowerCase().includes(query.toLowerCase());
|
|
93
|
+
const filteredChildren = node.children.length > 0
|
|
94
|
+
? filterTree(node.children, query)
|
|
95
|
+
: [];
|
|
96
|
+
|
|
97
|
+
if (matchesLabel || filteredChildren.length > 0) {
|
|
98
|
+
acc.push({
|
|
99
|
+
...node,
|
|
100
|
+
children: matchesLabel ? node.children : filteredChildren,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return acc;
|
|
104
|
+
}, []);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface RootMenuItem {
|
|
108
|
+
label: string;
|
|
109
|
+
href: string;
|
|
110
|
+
children?: RootMenuItem[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function RootMenuItemEntry({ item }: { item: RootMenuItem }) {
|
|
114
|
+
const [expanded, setExpanded] = useState(false);
|
|
115
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="border-t border-muted">
|
|
119
|
+
<div className="flex items-center">
|
|
120
|
+
<a
|
|
121
|
+
href={item.href}
|
|
122
|
+
className="flex flex-1 items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-small font-semibold text-fg hover:text-accent hover:underline break-words"
|
|
123
|
+
>
|
|
124
|
+
<CategoryLinkIcon className="w-[14px]" />
|
|
125
|
+
<span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(item.label) }} />
|
|
126
|
+
</a>
|
|
127
|
+
{hasChildren && (
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={() => setExpanded((prev) => !prev)}
|
|
131
|
+
className="flex items-center justify-center px-hsp-sm py-vsp-xs text-muted hover:text-fg"
|
|
132
|
+
aria-expanded={expanded}
|
|
133
|
+
aria-label={expanded ? `Collapse ${item.label}` : `Expand ${item.label}`}
|
|
134
|
+
>
|
|
135
|
+
<ToggleChevron isExpanded={expanded} className="text-muted" />
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
{hasChildren && expanded && (
|
|
140
|
+
<div className="pb-vsp-xs">
|
|
141
|
+
{item.children!.map((child) => (
|
|
142
|
+
<a
|
|
143
|
+
key={child.href}
|
|
144
|
+
href={child.href}
|
|
145
|
+
className="block pl-hsp-xl pr-hsp-sm py-vsp-2xs text-small text-muted hover:text-accent hover:underline break-words"
|
|
146
|
+
>
|
|
147
|
+
<span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(child.label) }} />
|
|
148
|
+
</a>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface SidebarTreeProps {
|
|
157
|
+
nodes: NavNode[];
|
|
158
|
+
currentSlug?: string;
|
|
159
|
+
rootMenuItems?: RootMenuItem[];
|
|
160
|
+
backToMenuLabel?: string;
|
|
161
|
+
localeLinks?: LocaleLink[];
|
|
162
|
+
themeDefaultMode?: "light" | "dark";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function SidebarFooter({ links, themeDefaultMode }: { links?: LocaleLink[]; themeDefaultMode?: "light" | "dark" }) {
|
|
166
|
+
if (!links && !themeDefaultMode) return null;
|
|
167
|
+
return (
|
|
168
|
+
// pb-[50vh] provides scroll room so the footer doesn't sit at the very bottom of the viewport
|
|
169
|
+
<div className="lg:hidden flex items-center gap-hsp-md border-t border-muted px-hsp-sm py-vsp-xs pb-[50vh] text-small">
|
|
170
|
+
{themeDefaultMode && <ThemeToggle defaultMode={themeDefaultMode} />}
|
|
171
|
+
{links && links.map((link, i) => (
|
|
172
|
+
<span key={link.href} className="flex items-center gap-hsp-xs">
|
|
173
|
+
{i > 0 && <span className="text-muted">/</span>}
|
|
174
|
+
{link.active ? (
|
|
175
|
+
<span aria-current="true" className="font-medium text-fg">{link.label}</span>
|
|
176
|
+
) : (
|
|
177
|
+
<a href={link.href} lang={link.code} className="text-muted hover:text-fg">
|
|
178
|
+
{link.label}
|
|
179
|
+
</a>
|
|
180
|
+
)}
|
|
181
|
+
</span>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default function SidebarTree({ nodes, currentSlug, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }: SidebarTreeProps) {
|
|
188
|
+
const activeSlug = useActiveSlug(nodes, currentSlug);
|
|
189
|
+
const [query, setQuery] = useState("");
|
|
190
|
+
const [showingRootMenu, setShowingRootMenu] = useState(false);
|
|
191
|
+
const filterRef = useRef<HTMLInputElement>(null);
|
|
192
|
+
const [filterPlaceholder, setFilterPlaceholder] = useState("Filter...");
|
|
193
|
+
|
|
194
|
+
// Detect OS to show appropriate keyboard shortcut in placeholder
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
const platform = (navigator as { userAgentData?: { platform: string } }).userAgentData?.platform ?? navigator.platform;
|
|
197
|
+
const isMac = /mac/i.test(platform);
|
|
198
|
+
setFilterPlaceholder(isMac ? "Filter... (\u2318 + /)" : "Filter... (Ctrl + /)");
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
// Global shortcut: Cmd+/ (Mac) or Ctrl+/ to focus the filter input
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
204
|
+
if (e.isComposing) return;
|
|
205
|
+
if (e.key === "/" && (e.metaKey || e.ctrlKey)) {
|
|
206
|
+
const el = filterRef.current;
|
|
207
|
+
if (!el || el.offsetParent === null) return; // skip if hidden
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
el.focus();
|
|
210
|
+
el.select();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
214
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
const filteredNodes = useMemo(
|
|
218
|
+
() => (query ? filterTree(nodes, query) : nodes),
|
|
219
|
+
[nodes, query],
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const footer = useMemo(
|
|
223
|
+
() => (localeLinks || themeDefaultMode) ? <SidebarFooter links={localeLinks} themeDefaultMode={themeDefaultMode} /> : null,
|
|
224
|
+
[localeLinks, themeDefaultMode],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Root menu view: show headerNav items as a simple list (Docusaurus-style)
|
|
228
|
+
if (showingRootMenu && rootMenuItems) {
|
|
229
|
+
return (
|
|
230
|
+
<nav>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => setShowingRootMenu(false)}
|
|
234
|
+
className="flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
|
|
235
|
+
>
|
|
236
|
+
<svg className="h-icon-sm w-icon-sm shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
237
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
238
|
+
</svg>
|
|
239
|
+
{backToMenuLabel ?? "Back to main menu"}
|
|
240
|
+
</button>
|
|
241
|
+
{rootMenuItems.map((item) => (
|
|
242
|
+
<RootMenuItemEntry key={item.href} item={item} />
|
|
243
|
+
))}
|
|
244
|
+
{footer}
|
|
245
|
+
</nav>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Top page: show only header nav links, no doc tree or filter.
|
|
250
|
+
// Derived from activeSlug (runtime-synced) so it stays correct across View Transitions.
|
|
251
|
+
if (!activeSlug && rootMenuItems) {
|
|
252
|
+
return (
|
|
253
|
+
<nav>
|
|
254
|
+
{rootMenuItems.map((item) => (
|
|
255
|
+
<RootMenuItemEntry key={item.href} item={item} />
|
|
256
|
+
))}
|
|
257
|
+
{footer}
|
|
258
|
+
</nav>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<nav>
|
|
264
|
+
{rootMenuItems && (
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={() => setShowingRootMenu(true)}
|
|
268
|
+
className="lg:hidden flex w-full items-center gap-hsp-xs px-hsp-sm py-vsp-xs text-left text-small text-muted hover:text-fg border-b border-muted"
|
|
269
|
+
>
|
|
270
|
+
<svg className="h-icon-sm w-icon-sm shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
271
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
272
|
+
</svg>
|
|
273
|
+
{backToMenuLabel ?? "Back to main menu"}
|
|
274
|
+
</button>
|
|
275
|
+
)}
|
|
276
|
+
<div className="px-hsp-sm py-vsp-xs">
|
|
277
|
+
<div className="flex items-center gap-hsp-xs bg-surface rounded px-hsp-sm py-vsp-2xs">
|
|
278
|
+
<svg className="h-[14px] w-[14px] text-muted shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
279
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
280
|
+
</svg>
|
|
281
|
+
<input
|
|
282
|
+
ref={filterRef}
|
|
283
|
+
type="text"
|
|
284
|
+
placeholder={filterPlaceholder}
|
|
285
|
+
value={query}
|
|
286
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
287
|
+
className="bg-transparent text-small outline-none w-full text-fg placeholder:text-muted"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
<NodeList
|
|
292
|
+
nodes={filteredNodes}
|
|
293
|
+
currentSlug={activeSlug}
|
|
294
|
+
depth={0}
|
|
295
|
+
forceOpen={!!query}
|
|
296
|
+
/>
|
|
297
|
+
{footer}
|
|
298
|
+
</nav>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function NodeList({
|
|
303
|
+
nodes,
|
|
304
|
+
currentSlug,
|
|
305
|
+
depth,
|
|
306
|
+
forceOpen,
|
|
307
|
+
}: {
|
|
308
|
+
nodes: NavNode[];
|
|
309
|
+
currentSlug?: string;
|
|
310
|
+
depth: number;
|
|
311
|
+
forceOpen: boolean;
|
|
312
|
+
}) {
|
|
313
|
+
return (
|
|
314
|
+
<>
|
|
315
|
+
{nodes.map((node, index) => {
|
|
316
|
+
const isLast = index === nodes.length - 1;
|
|
317
|
+
return node.children.length > 0 ? (
|
|
318
|
+
<CategoryNode
|
|
319
|
+
key={node.slug}
|
|
320
|
+
node={node}
|
|
321
|
+
currentSlug={currentSlug}
|
|
322
|
+
depth={depth}
|
|
323
|
+
isLast={isLast}
|
|
324
|
+
forceOpen={forceOpen}
|
|
325
|
+
/>
|
|
326
|
+
) : (
|
|
327
|
+
<LeafNode
|
|
328
|
+
key={node.slug}
|
|
329
|
+
node={node}
|
|
330
|
+
currentSlug={currentSlug}
|
|
331
|
+
depth={depth}
|
|
332
|
+
isLast={isLast}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
})}
|
|
336
|
+
</>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Check if currentSlug is anywhere in this node's subtree */
|
|
341
|
+
function subtreeContainsSlug(node: NavNode, slug?: string): boolean {
|
|
342
|
+
if (!slug) return false;
|
|
343
|
+
if (node.slug === slug) return true;
|
|
344
|
+
return node.children.some((child) => subtreeContainsSlug(child, slug));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function CategoryNode({
|
|
348
|
+
node,
|
|
349
|
+
currentSlug,
|
|
350
|
+
depth,
|
|
351
|
+
isLast,
|
|
352
|
+
forceOpen,
|
|
353
|
+
}: {
|
|
354
|
+
node: NavNode;
|
|
355
|
+
currentSlug?: string;
|
|
356
|
+
depth: number;
|
|
357
|
+
isLast: boolean;
|
|
358
|
+
forceOpen: boolean;
|
|
359
|
+
}) {
|
|
360
|
+
const containsCurrent = subtreeContainsSlug(node, currentSlug);
|
|
361
|
+
const isActive = node.slug === currentSlug;
|
|
362
|
+
|
|
363
|
+
// Initial state must match server render (no sessionStorage access)
|
|
364
|
+
// to avoid hydration mismatch. Stored state is restored in useEffect below.
|
|
365
|
+
const [open, setOpen] = useState(containsCurrent ? true : !node.collapsed);
|
|
366
|
+
|
|
367
|
+
// Restore open state from sessionStorage after hydration
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
const stored = getOpenSet();
|
|
370
|
+
if (stored.has(node.slug) && !open) {
|
|
371
|
+
setOpen(true);
|
|
372
|
+
}
|
|
373
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
374
|
+
|
|
375
|
+
// Auto-open category when navigation lands on a descendant
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (subtreeContainsSlug(node, currentSlug) && !open) {
|
|
378
|
+
setOpen(true);
|
|
379
|
+
const stored = getOpenSet();
|
|
380
|
+
stored.add(node.slug);
|
|
381
|
+
saveOpenSet(stored);
|
|
382
|
+
}
|
|
383
|
+
}, [currentSlug]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
384
|
+
|
|
385
|
+
// Sync auto-opened state to sessionStorage so it persists across View Transitions
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
if (open) {
|
|
388
|
+
const stored = getOpenSet();
|
|
389
|
+
if (!stored.has(node.slug)) {
|
|
390
|
+
stored.add(node.slug);
|
|
391
|
+
saveOpenSet(stored);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}, [open, node.slug]);
|
|
395
|
+
|
|
396
|
+
const toggle = useCallback(() => {
|
|
397
|
+
setOpen((prev) => {
|
|
398
|
+
const next = !prev;
|
|
399
|
+
const stored = getOpenSet();
|
|
400
|
+
if (next) {
|
|
401
|
+
stored.add(node.slug);
|
|
402
|
+
} else {
|
|
403
|
+
stored.delete(node.slug);
|
|
404
|
+
}
|
|
405
|
+
saveOpenSet(stored);
|
|
406
|
+
return next;
|
|
407
|
+
});
|
|
408
|
+
}, [node.slug]);
|
|
409
|
+
|
|
410
|
+
const isExpanded = forceOpen || open;
|
|
411
|
+
const paddingLeft = padLeft(depth, true);
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div className={`${depth === 0 ? "border-t border-muted" : ""} ${depth >= 1 && !isLast ? "relative" : ""}`}>
|
|
415
|
+
{depth >= 1 && !isLast && isExpanded && (
|
|
416
|
+
<div
|
|
417
|
+
className="absolute border-l border-solid border-muted z-10"
|
|
418
|
+
style={{
|
|
419
|
+
left: connectorLeft(depth),
|
|
420
|
+
top: 0,
|
|
421
|
+
bottom: 0,
|
|
422
|
+
}}
|
|
423
|
+
/>
|
|
424
|
+
)}
|
|
425
|
+
<div className="relative">
|
|
426
|
+
<ConnectorLines depth={depth} isLast={isLast} topPad="calc(0.15rem + var(--spacing-vsp-xs))" />
|
|
427
|
+
{node.href ? (
|
|
428
|
+
<div
|
|
429
|
+
className={`flex w-full items-center text-small font-semibold pt-[0.15rem] ${isActive ? "bg-fg text-bg" : "text-fg"}`}
|
|
430
|
+
>
|
|
431
|
+
<a
|
|
432
|
+
href={node.href}
|
|
433
|
+
aria-current={isActive ? "page" : undefined}
|
|
434
|
+
className={`flex-1 flex items-start gap-hsp-xs py-vsp-xs hover:underline focus:underline break-words ${isActive ? "text-bg" : "text-fg"}`}
|
|
435
|
+
style={{ paddingLeft }}
|
|
436
|
+
>
|
|
437
|
+
{depth === 0 && (
|
|
438
|
+
<span className="flex h-[1lh] items-center">
|
|
439
|
+
<CategoryLinkIcon className={`w-[14px] ${isActive ? "text-bg" : ""}`} />
|
|
440
|
+
</span>
|
|
441
|
+
)}
|
|
442
|
+
<span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(node.label) }} />
|
|
443
|
+
</a>
|
|
444
|
+
<button
|
|
445
|
+
type="button"
|
|
446
|
+
onClick={toggle}
|
|
447
|
+
className={`aspect-square flex items-center justify-center w-[1.5rem] border-y border-l hover:underline focus:underline ${isActive ? "border-bg/30" : "border-muted"}`}
|
|
448
|
+
aria-expanded={isExpanded}
|
|
449
|
+
aria-label={isExpanded ? `Collapse ${node.label}` : `Expand ${node.label}`}
|
|
450
|
+
>
|
|
451
|
+
<ToggleChevron isExpanded={isExpanded} className={isActive ? "text-bg" : "text-muted"} />
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
) : (
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
onClick={toggle}
|
|
458
|
+
className={`flex w-full items-center gap-hsp-md text-left text-small font-semibold py-vsp-xs text-fg hover:underline focus:underline break-words`}
|
|
459
|
+
style={{ paddingLeft }}
|
|
460
|
+
aria-expanded={isExpanded}
|
|
461
|
+
aria-label={isExpanded ? `Collapse ${node.label}` : `Expand ${node.label}`}
|
|
462
|
+
>
|
|
463
|
+
<span className="aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted">
|
|
464
|
+
<ToggleChevron isExpanded={isExpanded} className="text-muted" />
|
|
465
|
+
</span>
|
|
466
|
+
<span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(node.label) }} />
|
|
467
|
+
</button>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
{isExpanded && (
|
|
471
|
+
<div>
|
|
472
|
+
<NodeList
|
|
473
|
+
nodes={node.children}
|
|
474
|
+
currentSlug={currentSlug}
|
|
475
|
+
depth={depth + 1}
|
|
476
|
+
forceOpen={forceOpen}
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function LeafNode({
|
|
485
|
+
node,
|
|
486
|
+
currentSlug,
|
|
487
|
+
depth,
|
|
488
|
+
isLast,
|
|
489
|
+
}: {
|
|
490
|
+
node: NavNode;
|
|
491
|
+
currentSlug?: string;
|
|
492
|
+
depth: number;
|
|
493
|
+
isLast: boolean;
|
|
494
|
+
}) {
|
|
495
|
+
if (!node.href) return null;
|
|
496
|
+
const isActive = node.slug === currentSlug;
|
|
497
|
+
const isRoot = depth === 0;
|
|
498
|
+
const paddingLeft = padLeft(depth, isRoot);
|
|
499
|
+
|
|
500
|
+
// For nested last leaves, add visual breathing space as margin on the outer wrapper
|
|
501
|
+
// rather than padding on the anchor — padding would grow the row box and throw off
|
|
502
|
+
// the ConnectorLines geometry (which now uses topPad + 0.5lh of the row to land the
|
|
503
|
+
// horizontal connector at the first-line midpoint).
|
|
504
|
+
const outerClass = isRoot
|
|
505
|
+
? "border-t border-muted"
|
|
506
|
+
: !isRoot && isLast
|
|
507
|
+
? "pb-vsp-md"
|
|
508
|
+
: "";
|
|
509
|
+
|
|
510
|
+
const topPad = isRoot
|
|
511
|
+
? "calc(var(--spacing-vsp-xs) + 0.15rem)"
|
|
512
|
+
: "var(--spacing-vsp-2xs)";
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<div className={outerClass}>
|
|
516
|
+
<div className="relative">
|
|
517
|
+
<ConnectorLines depth={depth} isLast={isLast} topPad={topPad} />
|
|
518
|
+
<a
|
|
519
|
+
href={node.href}
|
|
520
|
+
aria-current={isActive ? "page" : undefined}
|
|
521
|
+
className={isRoot
|
|
522
|
+
? `flex items-start gap-hsp-xs py-[calc(var(--spacing-vsp-xs)+0.15rem)] pr-[4px] text-small font-semibold break-words ${
|
|
523
|
+
isActive ? "bg-fg text-bg" : "text-fg hover:underline focus:underline"
|
|
524
|
+
}`
|
|
525
|
+
: `block py-vsp-2xs pr-[4px] text-small break-words ${
|
|
526
|
+
isActive
|
|
527
|
+
? "bg-fg font-medium text-bg"
|
|
528
|
+
: "text-muted hover:underline focus:underline"
|
|
529
|
+
}`
|
|
530
|
+
}
|
|
531
|
+
style={{ paddingLeft }}
|
|
532
|
+
>
|
|
533
|
+
{isRoot && (
|
|
534
|
+
<span className="flex h-[1lh] items-center">
|
|
535
|
+
<CategoryLinkIcon className={`w-[14px] ${isActive ? "text-bg" : ""}`} />
|
|
536
|
+
</span>
|
|
537
|
+
)}
|
|
538
|
+
<span dangerouslySetInnerHTML={{ __html: smartBreakToHtml(node.label) }} />
|
|
539
|
+
</a>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|