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.
Files changed (83) hide show
  1. package/dist/api.js +4 -1
  2. package/dist/cli.js +4 -6
  3. package/dist/compose.d.ts +2 -3
  4. package/dist/compose.js +7 -4
  5. package/dist/features/tauri.d.ts +10 -5
  6. package/dist/features/tauri.js +49 -6
  7. package/dist/preset.js +11 -0
  8. package/dist/prompts.js +2 -6
  9. package/dist/scaffold.js +15 -9
  10. package/dist/settings-gen.js +9 -6
  11. package/dist/utils.d.ts +8 -0
  12. package/dist/utils.js +25 -0
  13. package/dist/zfb-config-gen.js +11 -50
  14. package/package.json +1 -1
  15. package/templates/base/pages/_data.ts +10 -23
  16. package/templates/base/pages/docs/[[...slug]].tsx +27 -168
  17. package/templates/base/pages/lib/_body-end-islands.tsx +3 -0
  18. package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
  19. package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
  20. package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
  21. package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
  22. package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
  23. package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
  24. package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
  25. package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
  26. package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
  27. package/templates/base/pages/lib/_header-with-defaults.tsx +54 -89
  28. package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
  29. package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
  30. package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
  31. package/templates/base/pages/lib/_search-widget-script.ts +32 -9
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
  33. package/templates/base/pages/lib/locale-merge.ts +1 -1
  34. package/templates/base/pages/lib/route-enumerators.ts +11 -7
  35. package/templates/base/plugins/connect-adapter.mjs +30 -1
  36. package/templates/base/plugins/copy-public-plugin.mjs +10 -2
  37. package/templates/base/plugins/search-index-plugin.mjs +20 -8
  38. package/templates/base/src/components/ai-chat-modal.tsx +2 -0
  39. package/templates/base/src/components/doc-history.tsx +2 -0
  40. package/templates/base/src/components/image-enlarge.tsx +2 -0
  41. package/templates/base/src/components/sidebar-toggle.tsx +1 -1
  42. package/templates/base/src/components/sidebar-tree.tsx +11 -5
  43. package/templates/base/src/components/theme-toggle.tsx +18 -102
  44. package/templates/base/src/config/color-schemes.ts +4 -0
  45. package/templates/base/src/config/docs-schema.ts +94 -0
  46. package/templates/base/src/config/i18n.ts +10 -3
  47. package/templates/base/src/styles/global.css +14 -0
  48. package/templates/base/src/types/docs-entry.ts +8 -26
  49. package/templates/base/src/utils/base.ts +5 -3
  50. package/templates/base/src/utils/docs.ts +144 -169
  51. package/templates/base/zfb-shim.d.ts +167 -0
  52. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
  53. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
  54. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
  55. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
  56. package/templates/features/docHistory/files/src/components/doc-history.tsx +30 -8
  57. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
  58. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
  59. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
  60. package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
  61. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
  62. package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
  63. package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
  64. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +2 -0
  65. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
  66. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
  67. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +9 -3
  68. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
  69. package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
  70. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
  71. package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
  72. package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
  73. package/templates/base/src/components/content/heading-h3.tsx +0 -20
  74. package/templates/base/src/hooks/use-active-heading.ts +0 -133
  75. package/templates/base/src/plugins/docs-source-map.ts +0 -103
  76. package/templates/base/src/plugins/hast-utils.ts +0 -10
  77. package/templates/base/src/plugins/rehype-code-title.ts +0 -50
  78. package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
  79. package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
  80. package/templates/base/src/plugins/url-utils.ts +0 -4
  81. package/templates/base/src/utils/dedent.ts +0 -24
  82. package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
  83. 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: Set<string>;
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
- const versionConfig = settings.versions?.find((v) => v.slug === currentVersion);
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 as Set<string> };
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 as Set<string> };
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 = settings.locales[lang]?.dir ?? settings.docsDir;
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 titleLower = (entry.title || "").toLowerCase();
47
- var bodyLower = (entry.body || "").toLowerCase();
48
- var descLower = (entry.description || "").toLowerCase();
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].toLowerCase();
51
- if (titleLower.indexOf(t) !== -1) score += 3;
52
- if (descLower.indexOf(t) !== -1) score += 2;
53
- if (bodyLower.indexOf(t) !== -1) score += 1;
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
- // Index unavailable — silently degrade
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
- var terms = parseTerms(query);
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
- type NavNode,
43
- } from "@/utils/docs";
44
- import { buildSidebarForSection } from "@/utils/sidebar";
45
- import { loadNavSourceDocs } from "./_nav-source-docs";
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 derived from headerNav (mobile back-to-menu list).
121
- // The Astro template fed labelKey through `t(...)` and computed hrefs
122
- // with `navHref()`; mirror that exactly so the rendered list stays
123
- // identical between the A and B sites.
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
- const { navDocs, categoryMeta } = loadNavSourceDocs(lang, currentVersion);
140
- const rawNodes = buildSidebarForSection(navDocs, lang, navSection, categoryMeta);
141
- const nodes = currentVersion
142
- ? remapVersionedHrefs(rawNodes, currentVersion, lang)
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: Set<string>;
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 string));
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 string));
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/${tag}`
133
- : `/${locale}/docs/tags/${tag}`;
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 string));
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 string));
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(ctx.projectRoot, publicDirOption ?? "public");
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
- const { docsDir, locales, base } = ctx.options;
27
- emitSearchIndex({
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(ctx.options);
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(ctx.options.base ?? "");
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;
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  // W6A stub — no-op default export.
2
4
  //
3
5
  // The host (zudo-doc showcase) ships a full AI-chat modal island here. In
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  // W6A stub — no-op default + DocHistory named exports.
2
4
  //
3
5
  // When the docHistory feature is enabled, the feature template
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  // W6A stub — no-op default + ImageEnlargeSsrFallback named exports.
2
4
  //
3
5
  // When the imageEnlarge feature is enabled, the feature template
@@ -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/theme-toggle.tsx. Type references via
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";