create-zudo-doc 0.2.0 → 0.2.2
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/compose.d.ts +2 -3
- package/dist/compose.js +7 -4
- package/dist/features/tauri.d.ts +10 -5
- package/dist/features/tauri.js +49 -6
- package/dist/preset.js +11 -0
- package/dist/prompts.js +2 -6
- package/dist/scaffold.js +15 -9
- package/dist/settings-gen.js +9 -6
- 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/_body-end-islands.tsx +3 -0
- 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 +54 -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/ai-chat-modal.tsx +2 -0
- package/templates/base/src/components/doc-history.tsx +2 -0
- package/templates/base/src/components/image-enlarge.tsx +2 -0
- package/templates/base/src/components/sidebar-toggle.tsx +1 -1
- package/templates/base/src/components/sidebar-tree.tsx +11 -5
- package/templates/base/src/components/theme-toggle.tsx +18 -102
- 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/base/zfb-shim.d.ts +167 -0
- 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 +30 -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/imageEnlarge/files/src/components/image-enlarge.tsx +2 -0
- 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/tauri/files/src/components/find-in-page-init.tsx +9 -3
- 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/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
|
@@ -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);
|
|
@@ -35,14 +35,13 @@ import type { JSX } from "preact";
|
|
|
35
35
|
// reaches the rendered tree).
|
|
36
36
|
import { Island } from "@takazudo/zfb";
|
|
37
37
|
import SidebarTree from "@/components/sidebar-tree";
|
|
38
|
-
import { settings } from "@/config/settings";
|
|
39
38
|
import { defaultLocale, locales, t, type Locale } from "@/config/i18n";
|
|
40
|
-
import { buildLocaleLinks, navHref, versionedDocsUrl } from "@/utils/base";
|
|
41
39
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
buildRootMenuItems,
|
|
41
|
+
buildLocaleLinksForNav,
|
|
42
|
+
buildSidebarNodes,
|
|
43
|
+
getThemeDefaultMode,
|
|
44
|
+
} from "./_nav-data-prep";
|
|
46
45
|
|
|
47
46
|
export interface SidebarWithDefaultsProps {
|
|
48
47
|
/** Slug of the active doc page, used to highlight the current entry. */
|
|
@@ -61,34 +60,6 @@ export interface SidebarWithDefaultsProps {
|
|
|
61
60
|
currentPath?: string;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
/**
|
|
65
|
-
* Walk the nav tree and rewrite each node's `href` to its versioned form.
|
|
66
|
-
*
|
|
67
|
-
* `buildNavTree` always emits hrefs via `docsUrl()`; when the active route
|
|
68
|
-
* lives under `/v/{version}/...` we need the same nodes pointing at the
|
|
69
|
-
* versioned URL so internal nav clicks stay inside the version. Skips
|
|
70
|
-
* nodes without an href (link-only or category placeholders).
|
|
71
|
-
*/
|
|
72
|
-
function remapVersionedHrefs(
|
|
73
|
-
nodes: NavNode[],
|
|
74
|
-
version: string,
|
|
75
|
-
nodeLang: Locale,
|
|
76
|
-
): NavNode[] {
|
|
77
|
-
return nodes.map((node) => {
|
|
78
|
-
const children =
|
|
79
|
-
node.children.length > 0
|
|
80
|
-
? remapVersionedHrefs(node.children, version, nodeLang)
|
|
81
|
-
: node.children;
|
|
82
|
-
|
|
83
|
-
if (!node.href || node.slug.startsWith("__link__")) {
|
|
84
|
-
return children !== node.children ? { ...node, children } : node;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const newHref = versionedDocsUrl(node.slug, version, nodeLang);
|
|
88
|
-
return { ...node, href: newHref, children };
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
63
|
/**
|
|
93
64
|
* Default-bearing host wrapper that performs sidebar data prep, then wraps
|
|
94
65
|
* the project's `<SidebarTree>`
|
|
@@ -117,35 +88,21 @@ export function SidebarWithDefaults(
|
|
|
117
88
|
currentPath = "",
|
|
118
89
|
} = props;
|
|
119
90
|
|
|
120
|
-
// Root-menu items
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
const rootMenuItems = settings.headerNav.map((item) => ({
|
|
125
|
-
label: item.labelKey
|
|
126
|
-
? t(item.labelKey as Parameters<typeof t>[0], lang)
|
|
127
|
-
: item.label,
|
|
128
|
-
href: navHref(item.path, lang, currentVersion),
|
|
129
|
-
children: item.children?.map((child) => ({
|
|
130
|
-
label: child.labelKey
|
|
131
|
-
? t(child.labelKey as Parameters<typeof t>[0], lang)
|
|
132
|
-
: child.label,
|
|
133
|
-
href: navHref(child.path, lang, currentVersion),
|
|
134
|
-
})),
|
|
135
|
-
}));
|
|
91
|
+
// Root-menu items, sidebar nodes, locale links, and theme mode — all
|
|
92
|
+
// delegated to the shared _nav-data-prep helpers so header and sidebar
|
|
93
|
+
// wrappers stay in sync without duplicating the logic.
|
|
94
|
+
const rootMenuItems = buildRootMenuItems(lang, currentVersion);
|
|
136
95
|
|
|
137
96
|
const backToMenuLabel = navSection ? t("nav.backToMenu", lang) : undefined;
|
|
138
97
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
: rawNodes;
|
|
98
|
+
// emptyWhenUnsectioned=false: the desktop sidebar falls back to the FULL
|
|
99
|
+
// tree for pages whose slug matches no headerNav categoryMatch (legacy
|
|
100
|
+
// behavior) — only the header's mobile drawer collapses to root menu.
|
|
101
|
+
const nodes = buildSidebarNodes(lang, navSection, currentVersion, false);
|
|
144
102
|
|
|
145
103
|
// Locale-switcher links are only meaningful when more than one locale is
|
|
146
104
|
// configured — matches the Astro template's guard.
|
|
147
|
-
const localeLinks =
|
|
148
|
-
locales.length > 1 ? buildLocaleLinks(currentPath, lang) : undefined;
|
|
105
|
+
const localeLinks = buildLocaleLinksForNav(currentPath, lang, locales.length);
|
|
149
106
|
|
|
150
107
|
// Wrap <SidebarTree> directly in <Island when="load">. SSR emits the
|
|
151
108
|
// `data-zfb-island="SidebarTree"` marker around the rendered tree, with
|
|
@@ -164,9 +121,7 @@ export function SidebarWithDefaults(
|
|
|
164
121
|
rootMenuItems={rootMenuItems}
|
|
165
122
|
backToMenuLabel={backToMenuLabel}
|
|
166
123
|
localeLinks={localeLinks}
|
|
167
|
-
themeDefaultMode={
|
|
168
|
-
settings.colorMode ? settings.colorMode.defaultMode : undefined
|
|
169
|
-
}
|
|
124
|
+
themeDefaultMode={getThemeDefaultMode()}
|
|
170
125
|
/>
|
|
171
126
|
),
|
|
172
127
|
}) as unknown as JSX.Element;
|
|
@@ -94,7 +94,7 @@ export interface MergeLocaleDocsResult<T extends DocsEntry = DocsEntry> {
|
|
|
94
94
|
* Useful for callers that need to determine whether a page is a fallback
|
|
95
95
|
* (i.e. `isFallback = !localeSlugSet.has(slug)`).
|
|
96
96
|
*/
|
|
97
|
-
localeSlugSet:
|
|
97
|
+
localeSlugSet: ReadonlySet<string>;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
@@ -71,10 +71,10 @@ export function enumerateDocsRoutes(locale: string): string[] {
|
|
|
71
71
|
// safety re-application of the same rule (idempotent on the already-
|
|
72
72
|
// stripped id); kept so this enumerator's slug derivation reads as a
|
|
73
73
|
// single explicit call to the canonical helper.
|
|
74
|
-
urls.push(docsUrl(doc.data.slug ?? toRouteSlug(doc.id), locale as
|
|
74
|
+
urls.push(docsUrl(doc.data.slug ?? toRouteSlug(doc.id), locale as Locale));
|
|
75
75
|
}
|
|
76
76
|
for (const node of collectAutoIndexNodes(tree)) {
|
|
77
|
-
urls.push(docsUrl(node.slug, locale as
|
|
77
|
+
urls.push(docsUrl(node.slug, locale as Locale));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
return [...new Set(urls)];
|
|
@@ -127,10 +127,14 @@ export function enumerateTagsRoutes(locale: string): string[] {
|
|
|
127
127
|
const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
|
|
128
128
|
|
|
129
129
|
for (const tag of tagMap.keys()) {
|
|
130
|
+
// Tag segment URL-encoded — these URLs feed the sitemap, which must
|
|
131
|
+
// carry well-formed encoded URLs (e.g. "type:guide" → "type%3Aguide").
|
|
132
|
+
// Route params (the page paths() functions) stay raw.
|
|
133
|
+
const encoded = encodeURIComponent(tag);
|
|
130
134
|
const tagPath =
|
|
131
135
|
locale === defaultLocale
|
|
132
|
-
? `/docs/tags/${
|
|
133
|
-
: `/${locale}/docs/tags/${
|
|
136
|
+
? `/docs/tags/${encoded}`
|
|
137
|
+
: `/${locale}/docs/tags/${encoded}`;
|
|
134
138
|
urls.push(withBase(tagPath));
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -194,10 +198,10 @@ export function enumerateVersionedRoutes(
|
|
|
194
198
|
// category_no_page index.mdx → no route, no sitemap URL (see paths()).
|
|
195
199
|
if (doc.data.category_no_page === true) continue;
|
|
196
200
|
const slug = doc.data.slug ?? toRouteSlug(doc.id);
|
|
197
|
-
urls.push(versionedDocsUrl(slug, version.slug, locale as
|
|
201
|
+
urls.push(versionedDocsUrl(slug, version.slug, locale as Locale));
|
|
198
202
|
}
|
|
199
203
|
for (const node of collectAutoIndexNodes(tree)) {
|
|
200
|
-
urls.push(versionedDocsUrl(node.slug, version.slug, locale as
|
|
204
|
+
urls.push(versionedDocsUrl(node.slug, version.slug, locale as Locale));
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
|
|
@@ -222,7 +226,7 @@ export function enumerateVersionedRoutes(
|
|
|
222
226
|
* slash). The sitemap renderer prefixes each with settings.siteUrl.
|
|
223
227
|
*/
|
|
224
228
|
export function enumerateAllRoutes(): Map<string, string> {
|
|
225
|
-
const today = new Date().toISOString().split("T")[0];
|
|
229
|
+
const today = new Date().toISOString().split("T")[0] ?? "";
|
|
226
230
|
const routes = new Map<string, string>();
|
|
227
231
|
|
|
228
232
|
function add(url: string): void {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
// Adapter from Connect-style middleware (`(req, res, next) => void`) to
|
|
2
3
|
// the request-response shape zfb's `devMiddleware` lifecycle hook
|
|
3
4
|
// expects (`(req: ZfbDevMiddlewareRequest) => Promise<ZfbDevMiddlewareResponse | undefined>`).
|
|
@@ -16,8 +17,19 @@
|
|
|
16
17
|
// the v2 integration package keeps its Connect-style API surface so
|
|
17
18
|
// non-zfb embedders (Astro, plain Vite, a unit test) continue to work.
|
|
18
19
|
|
|
20
|
+
/** @import { ZfbDevMiddlewareRequest, ZfbDevMiddlewareResponse } from "@takazudo/zfb/plugins" */
|
|
21
|
+
|
|
19
22
|
import { Buffer } from "node:buffer";
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Connect-style middleware: `(req, res, next) => void`.
|
|
26
|
+
* The req/res parameters are typed as `any` here because this adapter
|
|
27
|
+
* intentionally passes a plain-object shim that structurally satisfies the
|
|
28
|
+
* subset of `IncomingMessage` / `ServerResponse` that the v2 integration
|
|
29
|
+
* middlewares actually access — not the full Node.js types.
|
|
30
|
+
* @typedef {(req: any, res: any, next: (err?: unknown) => void) => void} ConnectMiddleware
|
|
31
|
+
*/
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* Convert a Connect-style middleware to a zfb devMiddleware handler.
|
|
23
35
|
* The returned async function takes a `ZfbDevMiddlewareRequest` and
|
|
@@ -37,6 +49,9 @@ import { Buffer } from "node:buffer";
|
|
|
37
49
|
* - Binary bodies (Buffer / Uint8Array) → encoded as base64 and
|
|
38
50
|
* flagged `bodyEncoding: "base64"` so the JSON envelope round-trip
|
|
39
51
|
* stays loss-less.
|
|
52
|
+
*
|
|
53
|
+
* @param {ConnectMiddleware} middleware
|
|
54
|
+
* @returns {(zfbReq: ZfbDevMiddlewareRequest) => Promise<ZfbDevMiddlewareResponse | undefined>}
|
|
40
55
|
*/
|
|
41
56
|
export function connectToZfbHandler(middleware) {
|
|
42
57
|
return (zfbReq) => {
|
|
@@ -57,9 +72,14 @@ export function connectToZfbHandler(middleware) {
|
|
|
57
72
|
// today (`statusCode`, `setHeader`, `getHeader`, `end`) — extend
|
|
58
73
|
// here if a future middleware needs more.
|
|
59
74
|
let statusCode = 200;
|
|
75
|
+
/** @type {Record<string, string>} */
|
|
60
76
|
const headers = {};
|
|
61
77
|
let settled = false;
|
|
62
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @param {string | Buffer | Uint8Array | null | undefined} body
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
63
83
|
const finish = (body) => {
|
|
64
84
|
if (settled) return;
|
|
65
85
|
settled = true;
|
|
@@ -67,6 +87,7 @@ export function connectToZfbHandler(middleware) {
|
|
|
67
87
|
// axum's expectation (`Record<string, string>` of arbitrary
|
|
68
88
|
// case). Last-wins on collision; with `setHeader` callers this
|
|
69
89
|
// shouldn't happen.
|
|
90
|
+
/** @type {Record<string, string>} */
|
|
70
91
|
const normalisedHeaders = {};
|
|
71
92
|
for (const [k, v] of Object.entries(headers)) {
|
|
72
93
|
normalisedHeaders[k.toLowerCase()] = String(v);
|
|
@@ -93,12 +114,18 @@ export function connectToZfbHandler(middleware) {
|
|
|
93
114
|
get statusCode() {
|
|
94
115
|
return statusCode;
|
|
95
116
|
},
|
|
117
|
+
/** @param {number} v */
|
|
96
118
|
set statusCode(v) {
|
|
97
119
|
statusCode = v;
|
|
98
120
|
},
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} name
|
|
123
|
+
* @param {string | number | readonly string[]} value
|
|
124
|
+
*/
|
|
99
125
|
setHeader(name, value) {
|
|
100
|
-
headers[name] = value;
|
|
126
|
+
headers[name] = String(value);
|
|
101
127
|
},
|
|
128
|
+
/** @param {string} name */
|
|
102
129
|
getHeader(name) {
|
|
103
130
|
// Header lookup is case-insensitive in Node's real
|
|
104
131
|
// ServerResponse — mirror that so middlewares that probe an
|
|
@@ -113,11 +140,13 @@ export function connectToZfbHandler(middleware) {
|
|
|
113
140
|
get headersSent() {
|
|
114
141
|
return settled;
|
|
115
142
|
},
|
|
143
|
+
/** @param {string | Buffer | Uint8Array | null | undefined} [body] */
|
|
116
144
|
end(body) {
|
|
117
145
|
finish(body);
|
|
118
146
|
},
|
|
119
147
|
};
|
|
120
148
|
|
|
149
|
+
/** @param {unknown} [err] */
|
|
121
150
|
const next = (err) => {
|
|
122
151
|
if (settled) return;
|
|
123
152
|
if (err) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
// zfb plugin module: copy-public.
|
|
2
3
|
//
|
|
3
4
|
// Workaround for upstream zfb gap — `zfb build` does not copy `public/`
|
|
@@ -21,15 +22,22 @@
|
|
|
21
22
|
// `zfb.config.ts`. The `base` option is intentionally unused — see
|
|
22
23
|
// rationale above.
|
|
23
24
|
|
|
25
|
+
/** @import { ZfbBuildHookContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
|
|
26
|
+
|
|
24
27
|
import { cp } from "node:fs/promises";
|
|
25
28
|
import { resolve } from "node:path";
|
|
26
29
|
|
|
30
|
+
/** @type {ZfbPlugin} */
|
|
27
31
|
export default {
|
|
28
32
|
name: "copy-public",
|
|
29
33
|
|
|
34
|
+
/** @param {ZfbBuildHookContext} ctx */
|
|
30
35
|
async postBuild(ctx) {
|
|
31
36
|
const { publicDir: publicDirOption } = ctx.options;
|
|
32
|
-
const publicDir = resolve(
|
|
37
|
+
const publicDir = resolve(
|
|
38
|
+
ctx.projectRoot,
|
|
39
|
+
typeof publicDirOption === "string" ? publicDirOption : "public",
|
|
40
|
+
);
|
|
33
41
|
const dest = ctx.outDir;
|
|
34
42
|
|
|
35
43
|
ctx.logger.info(`copying ${publicDir} → ${dest}`);
|
|
@@ -38,7 +46,7 @@ export default {
|
|
|
38
46
|
recursive: true,
|
|
39
47
|
force: true,
|
|
40
48
|
errorOnExist: false,
|
|
41
|
-
}).catch((err) => {
|
|
49
|
+
}).catch((/** @type {NodeJS.ErrnoException} */ err) => {
|
|
42
50
|
if (err.code === "ENOENT") {
|
|
43
51
|
// publicDir does not exist or is empty — treat as no-op.
|
|
44
52
|
ctx.logger.info("public/ not found — skipping copy");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
// zfb plugin module: search-index.
|
|
2
3
|
//
|
|
3
4
|
// Wires two lifecycle hooks for the search-index integration:
|
|
@@ -16,25 +17,30 @@
|
|
|
16
17
|
// Inline functions are not supported by zfb's plugin runtime; see the
|
|
17
18
|
// sibling `doc-history-plugin.mjs` for the rationale.
|
|
18
19
|
|
|
20
|
+
/** @import { ZfbBuildHookContext, ZfbDevMiddlewareContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
|
|
21
|
+
/** @import { SearchIndexBuildOptions, SearchIndexConfig } from "@takazudo/zudo-doc/integrations/search-index" */
|
|
22
|
+
|
|
19
23
|
import { emitSearchIndex, createSearchIndexDevMiddleware } from "@takazudo/zudo-doc/integrations/search-index";
|
|
20
24
|
import { connectToZfbHandler } from "./connect-adapter.mjs";
|
|
21
25
|
|
|
26
|
+
/** @type {ZfbPlugin} */
|
|
22
27
|
export default {
|
|
23
28
|
name: "search-index",
|
|
24
29
|
|
|
30
|
+
/** @param {ZfbBuildHookContext} ctx */
|
|
25
31
|
postBuild(ctx) {
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
emitSearchIndex(/** @type {SearchIndexBuildOptions} */ (/** @type {unknown} */ ({
|
|
33
|
+
...ctx.options,
|
|
28
34
|
outDir: ctx.outDir,
|
|
29
|
-
docsDir,
|
|
30
|
-
locales,
|
|
31
|
-
base,
|
|
32
35
|
logger: ctx.logger,
|
|
33
|
-
});
|
|
36
|
+
})));
|
|
34
37
|
},
|
|
35
38
|
|
|
39
|
+
/** @param {ZfbDevMiddlewareContext} ctx */
|
|
36
40
|
devMiddleware(ctx) {
|
|
37
|
-
const middleware = createSearchIndexDevMiddleware(
|
|
41
|
+
const middleware = createSearchIndexDevMiddleware(
|
|
42
|
+
/** @type {SearchIndexConfig} */ (/** @type {unknown} */ (ctx.options)),
|
|
43
|
+
);
|
|
38
44
|
// zfb's `register(path, handler)` matches against the FULL request
|
|
39
45
|
// URL (no base-stripping). For a non-root base (e.g. "/my-docs/"),
|
|
40
46
|
// requests arrive as `/my-docs/search-index.json`, so we register
|
|
@@ -43,11 +49,17 @@ export default {
|
|
|
43
49
|
// middleware itself is base-tolerant (matches via
|
|
44
50
|
// `endsWith("/search-index.json")`), so it does not need a
|
|
45
51
|
// separate base-stripping pass.
|
|
46
|
-
const basePrefix = stripTrailingSlash(
|
|
52
|
+
const basePrefix = stripTrailingSlash(
|
|
53
|
+
typeof ctx.options["base"] === "string" ? ctx.options["base"] : "",
|
|
54
|
+
);
|
|
47
55
|
ctx.register(`${basePrefix}/search-index.json`, connectToZfbHandler(middleware));
|
|
48
56
|
},
|
|
49
57
|
};
|
|
50
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} s
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
51
63
|
function stripTrailingSlash(s) {
|
|
52
64
|
if (typeof s !== "string" || s.length === 0) return "";
|
|
53
65
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Use preact hook entrypoints directly — the "react" → "preact/compat" alias
|
|
4
4
|
// lets us consume React-typed components in this Preact app (configured
|
|
5
5
|
// project-wide). Same pattern as src/components/sidebar-tree.tsx and
|
|
6
|
-
// packages/zudo-doc/src/theme
|
|
6
|
+
// packages/zudo-doc/src/theme-toggle/index.tsx. Type references via
|
|
7
7
|
// the global React namespace still resolve via @types/react.
|
|
8
8
|
import { useState, useEffect } from "preact/hooks";
|
|
9
9
|
import clsx from "clsx";
|