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.
- package/dist/api.js +4 -1
- package/dist/cli.js +4 -6
- package/dist/preset.js +11 -0
- package/dist/prompts.js +2 -6
- package/dist/scaffold.js +15 -9
- package/dist/settings-gen.js +7 -7
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +25 -0
- package/dist/zfb-config-gen.js +11 -50
- package/package.json +1 -1
- package/templates/base/pages/_data.ts +10 -23
- package/templates/base/pages/docs/[[...slug]].tsx +27 -168
- package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
- package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
- package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
- package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
- package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
- package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
- package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
- package/templates/base/pages/lib/_header-with-defaults.tsx +51 -89
- package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
- package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
- package/templates/base/pages/lib/_search-widget-script.ts +32 -9
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
- package/templates/base/pages/lib/locale-merge.ts +1 -1
- package/templates/base/pages/lib/route-enumerators.ts +11 -7
- package/templates/base/plugins/connect-adapter.mjs +30 -1
- package/templates/base/plugins/copy-public-plugin.mjs +10 -2
- package/templates/base/plugins/search-index-plugin.mjs +20 -8
- package/templates/base/src/components/sidebar-toggle.tsx +1 -1
- package/templates/base/src/components/sidebar-tree.tsx +10 -4
- package/templates/base/src/config/color-schemes.ts +4 -0
- package/templates/base/src/config/docs-schema.ts +94 -0
- package/templates/base/src/config/i18n.ts +10 -3
- package/templates/base/src/styles/global.css +14 -0
- package/templates/base/src/types/docs-entry.ts +8 -26
- package/templates/base/src/utils/base.ts +5 -3
- package/templates/base/src/utils/docs.ts +144 -169
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
- package/templates/features/docHistory/files/src/components/doc-history.tsx +28 -8
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
- package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
- package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
- package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
- package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
- package/templates/base/src/components/content/heading-h3.tsx +0 -20
- package/templates/base/src/components/theme-toggle.tsx +0 -107
- package/templates/base/src/hooks/use-active-heading.ts +0 -133
- package/templates/base/src/plugins/docs-source-map.ts +0 -103
- package/templates/base/src/plugins/hast-utils.ts +0 -10
- package/templates/base/src/plugins/rehype-code-title.ts +0 -50
- package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
- package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
- package/templates/base/src/plugins/url-utils.ts +0 -4
- package/templates/base/src/utils/dedent.ts +0 -24
- package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
|
@@ -30,8 +30,8 @@ import { defaultLocale } from "@/config/i18n";
|
|
|
30
30
|
import { tagVocabulary } from "@/config/tag-vocabulary";
|
|
31
31
|
import { collectTags } from "@/utils/tags";
|
|
32
32
|
import { toRouteSlug } from "@/utils/slug";
|
|
33
|
-
import { loadDocs } from "../_data";
|
|
34
33
|
import { mergeLocaleDocs } from "./locale-merge";
|
|
34
|
+
import { stableDocs, memoizeDerived } from "./_nav-source-cache";
|
|
35
35
|
import type { DocsEntry } from "@/types/docs-entry";
|
|
36
36
|
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
@@ -51,12 +51,18 @@ function localizeHref(href: string, lang: string): string {
|
|
|
51
51
|
return resolveHref(href);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Build the base-prefixed tag detail page href for the given locale.
|
|
56
|
+
* The tag segment is URL-encoded at the href site only — route params stay
|
|
57
|
+
* raw, so the built output dir keeps the raw tag name and the server decodes
|
|
58
|
+
* the percent-encoded href back to it (e.g. "type:guide" → "type%3Aguide").
|
|
59
|
+
*/
|
|
55
60
|
function tagHref(tag: string, lang: string): string {
|
|
61
|
+
const encoded = encodeURIComponent(tag);
|
|
56
62
|
const path =
|
|
57
63
|
lang === defaultLocale
|
|
58
|
-
? `/docs/tags/${
|
|
59
|
-
: `/${lang}/docs/tags/${
|
|
64
|
+
? `/docs/tags/${encoded}`
|
|
65
|
+
: `/${lang}/docs/tags/${encoded}`;
|
|
60
66
|
return withBase(path);
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -110,15 +116,22 @@ export function FooterWithDefaults({
|
|
|
110
116
|
let tagColumns: FooterTagColumn[] = [];
|
|
111
117
|
|
|
112
118
|
if (taglist?.enabled) {
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
(
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// Memoize docs loading and tag aggregation on the snapshot-anchored stable
|
|
120
|
+
// docs identity — same pattern as _nav-source-cache — so this block runs
|
|
121
|
+
// once per locale per build, not once per page render.
|
|
122
|
+
const allTags = (() => {
|
|
123
|
+
if (lang === defaultLocale) {
|
|
124
|
+
const baseDocs = stableDocs("docs");
|
|
125
|
+
return memoizeDerived([baseDocs], "footerTaglist;default", () => {
|
|
126
|
+
// category_no_page index files build no route — drop them so the
|
|
127
|
+
// footer taglist matches the tag-route pages (which all now filter too).
|
|
128
|
+
const docs: DocsEntry[] = baseDocs.filter(
|
|
129
|
+
(d) => !d.data.draft && !d.data.unlisted && !d.data.category_no_page,
|
|
130
|
+
);
|
|
131
|
+
const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
|
|
132
|
+
return [...tagMap.values()].sort((a, b) => a.tag.localeCompare(b.tag, lang));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
122
135
|
// Apply the default-locale-only filter so the footer taglist only counts
|
|
123
136
|
// tags that have a locale-routable tag page — matching the tag-route
|
|
124
137
|
// pages ([tag].tsx / tags/index.tsx) and enumerateTagsRoutes, which all
|
|
@@ -127,21 +140,19 @@ export function FooterWithDefaults({
|
|
|
127
140
|
// prefix pages. category_no_page is dropped for the same reason — AFTER
|
|
128
141
|
// the merge, so a locale override carrying the flag first wins the merge
|
|
129
142
|
// (pre-merge filtering would let the unflagged base doc resurface).
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
const baseDocs = stableDocs("docs");
|
|
144
|
+
const localeDocs = stableDocs(`docs-${lang}`);
|
|
145
|
+
return memoizeDerived([baseDocs, localeDocs], `footerTaglist;${lang}`, () => {
|
|
146
|
+
const result = mergeLocaleDocs({
|
|
147
|
+
baseDocs: baseDocs.filter((d) => !d.data.draft),
|
|
148
|
+
localeDocs: localeDocs.filter((d) => !d.data.draft),
|
|
149
|
+
applyDefaultLocaleOnlyFilter: true,
|
|
150
|
+
});
|
|
151
|
+
const docs: DocsEntry[] = result.docs.filter((d) => !d.data.category_no_page);
|
|
152
|
+
const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
|
|
153
|
+
return [...tagMap.values()].sort((a, b) => a.tag.localeCompare(b.tag, lang));
|
|
134
154
|
});
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const tagMap = collectTags(
|
|
139
|
-
docs,
|
|
140
|
-
(id, data) => data.slug ?? toRouteSlug(id),
|
|
141
|
-
);
|
|
142
|
-
const allTags = [...tagMap.values()].sort((a, b) =>
|
|
143
|
-
a.tag.localeCompare(b.tag, lang),
|
|
144
|
-
);
|
|
155
|
+
})();
|
|
145
156
|
|
|
146
157
|
const vocabularyActive =
|
|
147
158
|
Boolean(settings.tagVocabulary) && settings.tagGovernance !== "off";
|
|
@@ -20,16 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
import type { JSX } from "preact";
|
|
22
22
|
import { OgTags, TwitterCard } from "@takazudo/zudo-doc/head";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
// barrel also re-exports
|
|
27
|
-
//
|
|
28
|
-
// modules
|
|
29
|
-
//
|
|
30
|
-
// package exposes a dedicated `./theme/color-scheme-provider` subpath whose
|
|
31
|
-
// only output is the SSR-only ColorSchemeProvider component, keeping this
|
|
32
|
-
// head emission free of the panel-module dependency chain.
|
|
23
|
+
import { SIDEBAR_RESIZER_RESTORE_SCRIPT } from "@takazudo/zudo-doc/sidebar-resizer";
|
|
24
|
+
// Import ColorSchemeProvider from the dedicated
|
|
25
|
+
// `./theme/color-scheme-provider` subpath rather than the
|
|
26
|
+
// "@takazudo/zudo-doc/theme" barrel — the barrel also re-exports the
|
|
27
|
+
// ColorTweakExportModal island and the design-token SerDe/iframe-bridge
|
|
28
|
+
// modules, which this SSR-only head emission does not need in its zfb
|
|
29
|
+
// esbuild graph.
|
|
33
30
|
import ColorSchemeProvider from "@takazudo/zudo-doc/theme/color-scheme-provider";
|
|
34
31
|
import { composeMetaTitle } from "./_compose-meta-title";
|
|
35
32
|
import { withBase, absoluteUrl } from "@/utils/base";
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
// hides it on pages with hide_sidebar). When `navSection` is defined the
|
|
26
26
|
// panel gets the full section tree; when undefined (home, 404, tags,
|
|
27
27
|
// versions) nodes=[] so the panel shows only rootMenuItems.
|
|
28
|
-
// -
|
|
29
|
-
// Header.themeToggle so the ThemeToggle island marker appears in the
|
|
28
|
+
// - The bare package ThemeToggle (wrapped in Island below) is always passed
|
|
29
|
+
// to Header.themeToggle so the ThemeToggle island marker appears in the
|
|
30
30
|
// header on every page — matching the documented header contract.
|
|
31
31
|
//
|
|
32
32
|
// Locale switcher strategy (refs #1453):
|
|
@@ -45,68 +45,30 @@ import {
|
|
|
45
45
|
VersionSwitcher,
|
|
46
46
|
type VersionSwitcherLabels,
|
|
47
47
|
} from "@takazudo/zudo-doc/i18n-version";
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
// local ThemeToggle (already on `preact/hooks`) and wrap it in Island here
|
|
54
|
-
// so the SSG output still emits the `data-zfb-island="ThemeToggle"` marker.
|
|
55
|
-
import ThemeToggle from "@/components/theme-toggle";
|
|
48
|
+
// BARE (non-island-wrapped) ThemeToggle from the dedicated subpath
|
|
49
|
+
// (#2012 E2). The `./theme` barrel exports an Island-wrapped variant;
|
|
50
|
+
// this wrapper composes its own Island below, and the bare subpath
|
|
51
|
+
// avoids nesting an island inside an island.
|
|
52
|
+
import { ThemeToggle } from "@takazudo/zudo-doc/theme-toggle";
|
|
56
53
|
import SidebarToggle from "@/components/sidebar-toggle";
|
|
57
54
|
import { settings } from "@/config/settings";
|
|
58
55
|
import { defaultLocale, locales, t, type Locale } from "@/config/i18n";
|
|
59
56
|
import { buildGitHubRepoUrl } from "@/utils/github";
|
|
60
57
|
import {
|
|
61
|
-
buildLocaleLinks,
|
|
62
58
|
docsUrl,
|
|
63
59
|
navHref,
|
|
64
60
|
stripBase,
|
|
65
61
|
versionedDocsUrl,
|
|
66
62
|
withBase,
|
|
67
63
|
} from "@/utils/base";
|
|
68
|
-
import {
|
|
69
|
-
type NavNode,
|
|
70
|
-
} from "@/utils/docs";
|
|
71
|
-
import { buildSidebarForSection } from "@/utils/sidebar";
|
|
72
64
|
import { filterHeaderRightItems } from "@takazudo/zudo-doc/header";
|
|
73
65
|
import { SearchWidget } from "./_search-widget";
|
|
74
|
-
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Walk the nav tree and rewrite each node's `href` to its versioned form.
|
|
82
|
-
*
|
|
83
|
-
* `buildNavTree` always emits hrefs via `docsUrl()`; when the active route
|
|
84
|
-
* lives under `/v/{version}/...` we need the same nodes pointing at the
|
|
85
|
-
* versioned URL so internal nav clicks stay inside the version. Skips
|
|
86
|
-
* nodes without an href (link-only or category placeholders).
|
|
87
|
-
*
|
|
88
|
-
* Intentionally kept as a local copy in this module (not extracted) —
|
|
89
|
-
* T2 only dedupes loadNavSourceDocs; remapVersionedHrefs is out of scope.
|
|
90
|
-
*/
|
|
91
|
-
function remapVersionedHrefs(
|
|
92
|
-
nodes: NavNode[],
|
|
93
|
-
version: string,
|
|
94
|
-
nodeLang: Locale,
|
|
95
|
-
): NavNode[] {
|
|
96
|
-
return nodes.map((node) => {
|
|
97
|
-
const children =
|
|
98
|
-
node.children.length > 0
|
|
99
|
-
? remapVersionedHrefs(node.children, version, nodeLang)
|
|
100
|
-
: node.children;
|
|
101
|
-
|
|
102
|
-
if (!node.href || node.slug.startsWith("__link__")) {
|
|
103
|
-
return children !== node.children ? { ...node, children } : node;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const newHref = versionedDocsUrl(node.slug, version, nodeLang);
|
|
107
|
-
return { ...node, href: newHref, children };
|
|
108
|
-
});
|
|
109
|
-
}
|
|
66
|
+
import {
|
|
67
|
+
buildRootMenuItems,
|
|
68
|
+
buildLocaleLinksForNav,
|
|
69
|
+
buildSidebarNodes,
|
|
70
|
+
getThemeDefaultMode,
|
|
71
|
+
} from "./_nav-data-prep";
|
|
110
72
|
|
|
111
73
|
// ---------------------------------------------------------------------------
|
|
112
74
|
// Component
|
|
@@ -114,7 +76,7 @@ function remapVersionedHrefs(
|
|
|
114
76
|
|
|
115
77
|
export interface HeaderWithDefaultsProps {
|
|
116
78
|
/** Active locale; defaults to the configured defaultLocale. */
|
|
117
|
-
lang?: Locale;
|
|
79
|
+
lang?: Locale | string;
|
|
118
80
|
/**
|
|
119
81
|
* Current page URL path (as the layout passes from Astro.url.pathname or
|
|
120
82
|
* the zfb equivalent). Used by the Header to compute the active nav item
|
|
@@ -151,27 +113,21 @@ export function HeaderWithDefaults(
|
|
|
151
113
|
props: HeaderWithDefaultsProps,
|
|
152
114
|
): JSX.Element {
|
|
153
115
|
const {
|
|
154
|
-
lang = defaultLocale,
|
|
116
|
+
lang: langProp = defaultLocale,
|
|
155
117
|
currentPath = "",
|
|
156
118
|
currentVersion,
|
|
157
119
|
currentSlug,
|
|
158
120
|
navSection,
|
|
159
121
|
} = props;
|
|
122
|
+
// Route params arrive as `string`; cast to Locale since keys of settings.locales
|
|
123
|
+
// are always valid locale codes. The prop accepts `Locale | string` so callers
|
|
124
|
+
// without a Locale variable don't need to cast (e.g. _tag-pages.tsx).
|
|
125
|
+
const lang = langProp as Locale;
|
|
160
126
|
|
|
161
|
-
// Root-menu items
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
? t(item.labelKey as Parameters<typeof t>[0], lang)
|
|
166
|
-
: item.label,
|
|
167
|
-
href: navHref(item.path, lang, currentVersion),
|
|
168
|
-
children: item.children?.map((child) => ({
|
|
169
|
-
label: child.labelKey
|
|
170
|
-
? t(child.labelKey as Parameters<typeof t>[0], lang)
|
|
171
|
-
: child.label,
|
|
172
|
-
href: navHref(child.path, lang, currentVersion),
|
|
173
|
-
})),
|
|
174
|
-
}));
|
|
127
|
+
// Root-menu items, locale links, sidebar nodes, and theme mode — all
|
|
128
|
+
// delegated to the shared _nav-data-prep helpers so header and sidebar
|
|
129
|
+
// wrappers stay in sync without duplicating the logic.
|
|
130
|
+
const rootMenuItems = buildRootMenuItems(lang, currentVersion);
|
|
175
131
|
|
|
176
132
|
// Build the mobile sidebar toggle unconditionally — SidebarToggle is rendered
|
|
177
133
|
// on every page (refs #1453); the host CSS hides it where unneeded. When navSection is
|
|
@@ -182,21 +138,11 @@ export function HeaderWithDefaults(
|
|
|
182
138
|
|
|
183
139
|
// Locale-switcher links in the mobile sidebar footer — only when
|
|
184
140
|
// multiple locales are configured (mirrors _sidebar-with-defaults.tsx).
|
|
185
|
-
const localeLinks =
|
|
186
|
-
locales.length > 1 ? buildLocaleLinks(currentPath, lang) : undefined;
|
|
141
|
+
const localeLinks = buildLocaleLinksForNav(currentPath, lang, locales.length);
|
|
187
142
|
|
|
188
|
-
const themeDefaultMode =
|
|
189
|
-
? settings.colorMode.defaultMode
|
|
190
|
-
: undefined;
|
|
143
|
+
const themeDefaultMode = getThemeDefaultMode();
|
|
191
144
|
|
|
192
|
-
|
|
193
|
-
if (navSection !== undefined) {
|
|
194
|
-
const { navDocs, categoryMeta } = loadNavSourceDocs(lang, currentVersion);
|
|
195
|
-
const rawNodes = buildSidebarForSection(navDocs, lang, navSection, categoryMeta);
|
|
196
|
-
sidebarNodes = currentVersion
|
|
197
|
-
? remapVersionedHrefs(rawNodes, currentVersion, lang)
|
|
198
|
-
: rawNodes;
|
|
199
|
-
}
|
|
145
|
+
const sidebarNodes = buildSidebarNodes(lang, navSection, currentVersion);
|
|
200
146
|
|
|
201
147
|
// Wrap SidebarToggle (hamburger button + slide-in aside + SidebarTree) in
|
|
202
148
|
// Island so the SSG output carries the full tree HTML AND the
|
|
@@ -211,8 +157,25 @@ export function HeaderWithDefaults(
|
|
|
211
157
|
// nested as a JSX child its data was dropped during hydration and
|
|
212
158
|
// SidebarToggle re-rendered with `children=undefined`, wiping the SSR
|
|
213
159
|
// tree DOM. zudolab/zudo-doc#1355 wave 13.5.
|
|
160
|
+
//
|
|
161
|
+
// C4 — media-gated hydration. zfb only supports load|idle|visible
|
|
162
|
+
// strategies (no "media" strategy; matchMedia inside the component is too
|
|
163
|
+
// late — props are already emitted, bundle already downloaded).
|
|
164
|
+
// Upstream feature request: Takazudo/zudo-front-builder#969.
|
|
165
|
+
//
|
|
166
|
+
// Best achievable downstream: when="visible" + all SidebarToggle children
|
|
167
|
+
// are lg:hidden, so on desktop the Island wrapper div has zero rendered
|
|
168
|
+
// dimensions. IntersectionObserver fires isIntersecting=false on desktop
|
|
169
|
+
// (zero-size element) → Preact hydrate() is never called. On mobile (and
|
|
170
|
+
// on desktop→mobile resize) the children become visible, the element gains
|
|
171
|
+
// size, IO fires isIntersecting=true, and hydration completes normally.
|
|
172
|
+
//
|
|
173
|
+
// Residual: data-props JSON (~2.4 KB) is still emitted in the SSR HTML on
|
|
174
|
+
// every page regardless of viewport, because it is serialised at build time
|
|
175
|
+
// and not gated by media. Eliminating it requires a zfb "media" hydration
|
|
176
|
+
// strategy — tracked in Takazudo/zudo-front-builder#969.
|
|
214
177
|
const sidebarToggle = Island({
|
|
215
|
-
when: "
|
|
178
|
+
when: "visible",
|
|
216
179
|
children: (
|
|
217
180
|
<SidebarToggle
|
|
218
181
|
nodes={sidebarNodes}
|
|
@@ -225,15 +188,12 @@ export function HeaderWithDefaults(
|
|
|
225
188
|
),
|
|
226
189
|
}) as unknown as VNode;
|
|
227
190
|
|
|
228
|
-
// Wrap the
|
|
191
|
+
// Wrap the bare ThemeToggle in Island({when:"load"}) so the SSG
|
|
229
192
|
// output emits a data-zfb-island="ThemeToggle" marker the hydration
|
|
230
|
-
// runtime can find — matching the documented header contract.
|
|
231
|
-
// package's <ThemeToggle> already does this internally, but importing it
|
|
232
|
-
// forces the v2 theme barrel into the bundle (see import note at the top
|
|
233
|
-
// of this file).
|
|
193
|
+
// runtime can find — matching the documented header contract.
|
|
234
194
|
const themeToggle = Island({
|
|
235
195
|
when: "load",
|
|
236
|
-
children: <ThemeToggle />,
|
|
196
|
+
children: <ThemeToggle defaultMode={themeDefaultMode} />,
|
|
237
197
|
}) as unknown as VNode;
|
|
238
198
|
|
|
239
199
|
// Locale-aware search widget. Renders the full dialog markup in SSR
|
|
@@ -269,7 +229,9 @@ export function HeaderWithDefaults(
|
|
|
269
229
|
);
|
|
270
230
|
// "Latest" entry links to the current page in the latest (unversioned)
|
|
271
231
|
// docs when a slug is available, or falls back to the versions index page.
|
|
272
|
-
|
|
232
|
+
// Null check, not truthiness: "" is the canonical root-index slug (#1891)
|
|
233
|
+
// and must produce real per-version root URLs.
|
|
234
|
+
const latestUrl = currentSlug != null
|
|
273
235
|
? docsUrl(currentSlug, lang)
|
|
274
236
|
: versionsPageUrl;
|
|
275
237
|
|
|
@@ -278,7 +240,7 @@ export function HeaderWithDefaults(
|
|
|
278
240
|
// index — matching the documented version-switcher contract.
|
|
279
241
|
const versionUrls: Record<string, string> = {};
|
|
280
242
|
for (const v of settings.versions) {
|
|
281
|
-
versionUrls[v.slug] = currentSlug
|
|
243
|
+
versionUrls[v.slug] = currentSlug != null
|
|
282
244
|
? versionedDocsUrl(currentSlug, v.slug, lang)
|
|
283
245
|
: versionsPageUrl;
|
|
284
246
|
}
|
|
@@ -51,13 +51,14 @@ export function buildInlineVersionSwitcher(
|
|
|
51
51
|
const versionsPageUrl = withBase(
|
|
52
52
|
isNonDefaultLocale ? `/${locale}/docs/versions` : "/docs/versions",
|
|
53
53
|
);
|
|
54
|
-
|
|
54
|
+
// The docs root index has the canonical empty slug "" (#1891), and
|
|
55
|
+
// docsUrl("")/versionedDocsUrl("") resolve to the per-version docs roots —
|
|
56
|
+
// so an empty slug must NOT fall back to the versions page.
|
|
57
|
+
const latestUrl = docsUrl(slug, locale);
|
|
55
58
|
|
|
56
59
|
const versionUrls: Record<string, string> = {};
|
|
57
60
|
for (const v of settings.versions) {
|
|
58
|
-
versionUrls[v.slug] = slug
|
|
59
|
-
? versionedDocsUrl(slug, v.slug, locale)
|
|
60
|
-
: versionsPageUrl;
|
|
61
|
+
versionUrls[v.slug] = versionedDocsUrl(slug, v.slug, locale);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const labels: VersionSwitcherLabels = {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Shared nav data-prep utilities used by both _header-with-defaults.tsx
|
|
2
|
+
// and _sidebar-with-defaults.tsx.
|
|
3
|
+
//
|
|
4
|
+
// Extracted to avoid maintaining four near-identical copies: the two host
|
|
5
|
+
// modules above plus their template mirrors under
|
|
6
|
+
// packages/create-zudo-doc/templates/base/pages/lib/.
|
|
7
|
+
|
|
8
|
+
import { settings } from "@/config/settings";
|
|
9
|
+
import { t, type Locale } from "@/config/i18n";
|
|
10
|
+
import {
|
|
11
|
+
buildLocaleLinks,
|
|
12
|
+
navHref,
|
|
13
|
+
versionedDocsUrl,
|
|
14
|
+
} from "@/utils/base";
|
|
15
|
+
import { type NavNode } from "@/utils/docs";
|
|
16
|
+
import { buildSidebarForSection } from "@/utils/sidebar";
|
|
17
|
+
import { loadNavSourceDocs } from "./_nav-source-docs";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// remapVersionedHrefs
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Walk the nav tree and rewrite each node's `href` to its versioned form.
|
|
25
|
+
*
|
|
26
|
+
* `buildNavTree` always emits hrefs via `docsUrl()`; when the active route
|
|
27
|
+
* lives under `/v/{version}/...` we need the same nodes pointing at the
|
|
28
|
+
* versioned URL so internal nav clicks stay inside the version. Skips
|
|
29
|
+
* nodes without an href (link-only or category placeholders).
|
|
30
|
+
*/
|
|
31
|
+
export function remapVersionedHrefs(
|
|
32
|
+
nodes: NavNode[],
|
|
33
|
+
version: string,
|
|
34
|
+
nodeLang: Locale,
|
|
35
|
+
): NavNode[] {
|
|
36
|
+
return nodes.map((node) => {
|
|
37
|
+
const children =
|
|
38
|
+
node.children.length > 0
|
|
39
|
+
? remapVersionedHrefs(node.children, version, nodeLang)
|
|
40
|
+
: node.children;
|
|
41
|
+
|
|
42
|
+
if (!node.href || node.slug.startsWith("__link__")) {
|
|
43
|
+
return children !== node.children ? { ...node, children } : node;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const newHref = versionedDocsUrl(node.slug, version, nodeLang);
|
|
47
|
+
return { ...node, href: newHref, children };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// buildRootMenuItems
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Root-menu items derived from settings.headerNav (mobile "back to menu" list).
|
|
57
|
+
*
|
|
58
|
+
* Used by both header and sidebar wrappers — the same nav data feeds both the
|
|
59
|
+
* mobile SidebarToggle (header) and the desktop SidebarTree (sidebar).
|
|
60
|
+
*/
|
|
61
|
+
export function buildRootMenuItems(
|
|
62
|
+
lang: Locale,
|
|
63
|
+
currentVersion?: string,
|
|
64
|
+
) {
|
|
65
|
+
return settings.headerNav.map((item) => ({
|
|
66
|
+
label: item.labelKey
|
|
67
|
+
? t(item.labelKey as Parameters<typeof t>[0], lang)
|
|
68
|
+
: item.label,
|
|
69
|
+
href: navHref(item.path, lang, currentVersion),
|
|
70
|
+
children: item.children?.map((child) => ({
|
|
71
|
+
label: child.labelKey
|
|
72
|
+
? t(child.labelKey as Parameters<typeof t>[0], lang)
|
|
73
|
+
: child.label,
|
|
74
|
+
href: navHref(child.path, lang, currentVersion),
|
|
75
|
+
})),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// buildLocaleLinksForNav
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Locale-switcher links for the mobile sidebar footer and language switcher.
|
|
85
|
+
* Returns `undefined` when only one locale is configured (single-locale guard).
|
|
86
|
+
*/
|
|
87
|
+
export function buildLocaleLinksForNav(
|
|
88
|
+
currentPath: string,
|
|
89
|
+
lang: Locale,
|
|
90
|
+
localeCount: number,
|
|
91
|
+
) {
|
|
92
|
+
return localeCount > 1 ? buildLocaleLinks(currentPath, lang) : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// buildSidebarNodes
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build the resolved sidebar node list for a given section + version.
|
|
101
|
+
*
|
|
102
|
+
* Loads the nav source, filters to the active section, then optionally
|
|
103
|
+
* remaps hrefs for versioned routes.
|
|
104
|
+
*
|
|
105
|
+
* `emptyWhenUnsectioned` controls the `navSection === undefined` case —
|
|
106
|
+
* the two legacy call sites deliberately disagreed: the header's mobile
|
|
107
|
+
* drawer returned `[]` (root menu only), while the desktop sidebar fell
|
|
108
|
+
* through to `buildSidebarForSection(..., undefined)` = the FULL tree
|
|
109
|
+
* (pages whose slug matches no headerNav categoryMatch still get a
|
|
110
|
+
* sidebar). Collapsing both to `[]` shipped an empty desktop sidebar for
|
|
111
|
+
* unsectioned pages — keep the divergence explicit here.
|
|
112
|
+
*/
|
|
113
|
+
export function buildSidebarNodes(
|
|
114
|
+
lang: Locale,
|
|
115
|
+
navSection: string | undefined,
|
|
116
|
+
currentVersion?: string,
|
|
117
|
+
emptyWhenUnsectioned = true,
|
|
118
|
+
): NavNode[] {
|
|
119
|
+
if (navSection === undefined && emptyWhenUnsectioned) return [];
|
|
120
|
+
const { navDocs, categoryMeta } = loadNavSourceDocs(lang, currentVersion);
|
|
121
|
+
const rawNodes = buildSidebarForSection(navDocs, lang, navSection, categoryMeta);
|
|
122
|
+
return currentVersion
|
|
123
|
+
? remapVersionedHrefs(rawNodes, currentVersion, lang)
|
|
124
|
+
: rawNodes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// themeDefaultMode
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract the configured default color mode from settings.
|
|
133
|
+
* Returns `undefined` when color mode is not configured (single-scheme projects).
|
|
134
|
+
*/
|
|
135
|
+
export function getThemeDefaultMode() {
|
|
136
|
+
return settings.colorMode ? settings.colorMode.defaultMode : undefined;
|
|
137
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// - route-enumerators.ts (sitemap) and the MDX nav wrappers
|
|
22
22
|
// each picking the `NavSourceVariant` matching its filter needs.
|
|
23
23
|
|
|
24
|
-
import { defaultLocale, type Locale } from "@/config/i18n";
|
|
24
|
+
import { defaultLocale, getLocaleConfig, type Locale } from "@/config/i18n";
|
|
25
25
|
import { settings } from "@/config/settings";
|
|
26
26
|
import {
|
|
27
27
|
loadCategoryMeta,
|
|
@@ -84,7 +84,7 @@ export type NavSourceDocs = {
|
|
|
84
84
|
categoryMeta: Map<string, CategoryMeta>;
|
|
85
85
|
/** Slugs that came from the locale collection (for isFallback). Empty for
|
|
86
86
|
* default-locale / single-collection cases. */
|
|
87
|
-
localeSlugSet:
|
|
87
|
+
localeSlugSet: ReadonlySet<string>;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
/**
|
|
@@ -124,7 +124,11 @@ export function resolveNavSource(
|
|
|
124
124
|
// pages in sync. Otherwise (default locale, or the version not configured
|
|
125
125
|
// for this locale) fall back to the version's EN base collection.
|
|
126
126
|
if (currentVersion) {
|
|
127
|
-
|
|
127
|
+
// `versions` is `VersionConfig[] | false` — `false?.find` would throw
|
|
128
|
+
// (optional chaining only short-circuits on null/undefined).
|
|
129
|
+
const versionConfig = Array.isArray(settings.versions)
|
|
130
|
+
? settings.versions.find((v) => v.slug === currentVersion)
|
|
131
|
+
: undefined;
|
|
128
132
|
const localeDir = versionConfig?.locales?.[lang]?.dir;
|
|
129
133
|
if (lang !== defaultLocale && localeDir) {
|
|
130
134
|
return resolveVersionedLocaleSource(
|
|
@@ -138,7 +142,7 @@ export function resolveNavSource(
|
|
|
138
142
|
const docs = stableDocs(`docs-v-${currentVersion}`);
|
|
139
143
|
const categoryMeta = loadCategoryMeta(versionConfig?.docsDir ?? settings.docsDir);
|
|
140
144
|
const navDocs = stableNavDocs(docs);
|
|
141
|
-
return { docs, navDocs, categoryMeta, localeSlugSet: EMPTY_SLUG_SET
|
|
145
|
+
return { docs, navDocs, categoryMeta, localeSlugSet: EMPTY_SLUG_SET };
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
// --- Default locale: the "docs" collection directly.
|
|
@@ -146,7 +150,7 @@ export function resolveNavSource(
|
|
|
146
150
|
const docs = stableDocs("docs");
|
|
147
151
|
const categoryMeta = loadCategoryMeta(settings.docsDir);
|
|
148
152
|
const navDocs = stableNavDocs(docs);
|
|
149
|
-
return { docs, navDocs, categoryMeta, localeSlugSet: EMPTY_SLUG_SET
|
|
153
|
+
return { docs, navDocs, categoryMeta, localeSlugSet: EMPTY_SLUG_SET };
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
// --- Non-default locale: locale-first merge with EN fallback.
|
|
@@ -163,7 +167,7 @@ export function resolveNavSource(
|
|
|
163
167
|
);
|
|
164
168
|
const docs = merged.docs;
|
|
165
169
|
|
|
166
|
-
const localeDir =
|
|
170
|
+
const localeDir = getLocaleConfig(lang)?.dir ?? settings.docsDir;
|
|
167
171
|
const categoryMeta = stableMergeCategoryMeta(settings.docsDir, localeDir);
|
|
168
172
|
const navDocs = stableNavDocs(docs);
|
|
169
173
|
|
|
@@ -41,20 +41,35 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
|
|
|
41
41
|
return query.trim().split(/\\s+/).filter(Boolean);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// scoreEntry reads pre-lowercased fields (_titleLc, _descLc, _bodyLc)
|
|
45
|
+
// set by prepareLc() at index-load time. Terms arrive already lowercased
|
|
46
|
+
// from search() so no per-call toLowerCase() is needed.
|
|
44
47
|
function scoreEntry(entry, terms) {
|
|
45
48
|
var score = 0;
|
|
46
|
-
var
|
|
47
|
-
var
|
|
48
|
-
var
|
|
49
|
+
var titleLc = entry._titleLc;
|
|
50
|
+
var descLc = entry._descLc;
|
|
51
|
+
var bodyLc = entry._bodyLc;
|
|
49
52
|
for (var i = 0; i < terms.length; i++) {
|
|
50
|
-
var t = terms[i]
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
53
|
+
var t = terms[i];
|
|
54
|
+
if (titleLc.indexOf(t) !== -1) score += 3;
|
|
55
|
+
if (descLc.indexOf(t) !== -1) score += 2;
|
|
56
|
+
if (bodyLc.indexOf(t) !== -1) score += 1;
|
|
54
57
|
}
|
|
55
58
|
return score;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
// Pre-lowercase the searched fields on each entry once at load time so that
|
|
62
|
+
// scoreEntry() does not re-lowercase the entire ~162 KB index on every
|
|
63
|
+
// debounced keystroke. Original-case fields are preserved for display.
|
|
64
|
+
function prepareLc(entries) {
|
|
65
|
+
for (var i = 0; i < entries.length; i++) {
|
|
66
|
+
var e = entries[i];
|
|
67
|
+
e._titleLc = (e.title || "").toLowerCase();
|
|
68
|
+
e._descLc = (e.description || "").toLowerCase();
|
|
69
|
+
e._bodyLc = (e.body || "").toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
function highlightTerms(text, terms) {
|
|
59
74
|
if (!terms.length) return escapeHtml(text);
|
|
60
75
|
var escaped = terms.map(function(t) { return escapeRegExp(t); });
|
|
@@ -99,6 +114,7 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
|
|
|
99
114
|
this._countNarrow = null;
|
|
100
115
|
this._entries = null;
|
|
101
116
|
this._loading = false;
|
|
117
|
+
this._indexUnavailable = false;
|
|
102
118
|
this._debounce = null;
|
|
103
119
|
this._currentQuery = "";
|
|
104
120
|
this._allResults = [];
|
|
@@ -231,6 +247,7 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
|
|
|
231
247
|
})
|
|
232
248
|
.then(function(data) {
|
|
233
249
|
self._entries = Array.isArray(data) ? data : (data.entries || []);
|
|
250
|
+
prepareLc(self._entries);
|
|
234
251
|
self._loading = false;
|
|
235
252
|
// If user already typed, search now
|
|
236
253
|
if (self._input && self._input.value.trim()) {
|
|
@@ -239,7 +256,10 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
|
|
|
239
256
|
})
|
|
240
257
|
.catch(function() {
|
|
241
258
|
self._loading = false;
|
|
242
|
-
|
|
259
|
+
self._indexUnavailable = true;
|
|
260
|
+
if (self._results) {
|
|
261
|
+
self._results.innerHTML = "<p class=\\"text-small text-muted\\">Search unavailable</p>";
|
|
262
|
+
}
|
|
243
263
|
});
|
|
244
264
|
}
|
|
245
265
|
|
|
@@ -264,7 +284,10 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
|
|
|
264
284
|
return;
|
|
265
285
|
}
|
|
266
286
|
|
|
267
|
-
|
|
287
|
+
// Lowercase the query terms once here so scoreEntry() can do plain
|
|
288
|
+
// indexOf() against pre-lowercased entry fields without repeating
|
|
289
|
+
// toLowerCase() across the entire index on every keystroke.
|
|
290
|
+
var terms = parseTerms(query).map(function(t) { return t.toLowerCase(); });
|
|
268
291
|
var scored = [];
|
|
269
292
|
for (var i = 0; i < this._entries.length; i++) {
|
|
270
293
|
var s = scoreEntry(this._entries[i], terms);
|