create-zudo-doc 0.2.1 → 0.2.3

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/compose.d.ts CHANGED
@@ -78,9 +78,8 @@ export declare function validateDependencies(features: FeatureDefinition[], allS
78
78
  * - `pages/_mdx-components.ts` — image-enlarge.ts injects the
79
79
  * EnlargeableParagraph p-override (ENLARGE_SVG, EnlargeableParagraph def,
80
80
  * `p:` map entry) when imageEnlarge is enabled.
81
- *
82
- * The .tsx anchor form is still supported in `ANCHOR_LINE_RE` for forward
83
- * compatibility.
81
+ * - `pages/lib/_body-end-islands.tsx` — tauri.ts injects the FindInPageInit
82
+ * island (import, displayName, Island mount) when tauri is enabled.
84
83
  */
85
84
  export declare const ANCHOR_FILES: string[];
86
85
  /**
package/dist/compose.js CHANGED
@@ -164,11 +164,14 @@ export function validateDependencies(features, allSelectedNames) {
164
164
  * - `pages/_mdx-components.ts` — image-enlarge.ts injects the
165
165
  * EnlargeableParagraph p-override (ENLARGE_SVG, EnlargeableParagraph def,
166
166
  * `p:` map entry) when imageEnlarge is enabled.
167
- *
168
- * The .tsx anchor form is still supported in `ANCHOR_LINE_RE` for forward
169
- * compatibility.
167
+ * - `pages/lib/_body-end-islands.tsx` — tauri.ts injects the FindInPageInit
168
+ * island (import, displayName, Island mount) when tauri is enabled.
170
169
  */
171
- export const ANCHOR_FILES = ["src/styles/global.css", "pages/_mdx-components.ts"];
170
+ export const ANCHOR_FILES = [
171
+ "src/styles/global.css",
172
+ "pages/_mdx-components.ts",
173
+ "pages/lib/_body-end-islands.tsx",
174
+ ];
172
175
  /**
173
176
  * Main composition entry point. Orchestrates the full feature composition
174
177
  * pipeline for a generated project.
@@ -2,10 +2,15 @@ import type { FeatureModule } from "../compose.js";
2
2
  /**
3
3
  * Tauri feature.
4
4
  *
5
- * W7A (#1736): post-cutover, the FindInPage island is mounted by the
6
- * pages/lib body-end wrapper. The find-match highlight CSS is unconditional
7
- * in `templates/base/src/styles/global.css` (matches host). Only the
8
- * postProcess hooks (Cargo.toml / tauri.conf.json / .gitignore patches)
9
- * remain feature-scoped.
5
+ * #2052: the FindInPageInit island (Cmd/Ctrl+F find bar for the Tauri
6
+ * WebView, where the browser-native find UI is unavailable) is wired into
7
+ * `pages/lib/_body-end-islands.tsx` via the three injections below — import,
8
+ * displayName, and Island mount. zfb's island scanner only registers
9
+ * components reachable through static import chains (page → wrapper →
10
+ * component), so without this injection the feature-copied component files
11
+ * are orphaned dead code that never hydrates. The find-match highlight CSS
12
+ * is unconditional in `templates/base/src/styles/global.css` (matches host);
13
+ * the component runtime-gates itself (renders null unless
14
+ * `window.__TAURI_INTERNALS__` exists), so no settings field is needed.
10
15
  */
11
16
  export declare const tauriFeature: FeatureModule;
@@ -3,15 +3,58 @@ import path from "path";
3
3
  /**
4
4
  * Tauri feature.
5
5
  *
6
- * W7A (#1736): post-cutover, the FindInPage island is mounted by the
7
- * pages/lib body-end wrapper. The find-match highlight CSS is unconditional
8
- * in `templates/base/src/styles/global.css` (matches host). Only the
9
- * postProcess hooks (Cargo.toml / tauri.conf.json / .gitignore patches)
10
- * remain feature-scoped.
6
+ * #2052: the FindInPageInit island (Cmd/Ctrl+F find bar for the Tauri
7
+ * WebView, where the browser-native find UI is unavailable) is wired into
8
+ * `pages/lib/_body-end-islands.tsx` via the three injections below — import,
9
+ * displayName, and Island mount. zfb's island scanner only registers
10
+ * components reachable through static import chains (page → wrapper →
11
+ * component), so without this injection the feature-copied component files
12
+ * are orphaned dead code that never hydrates. The find-match highlight CSS
13
+ * is unconditional in `templates/base/src/styles/global.css` (matches host);
14
+ * the component runtime-gates itself (renders null unless
15
+ * `window.__TAURI_INTERNALS__` exists), so no settings field is needed.
11
16
  */
12
17
  export const tauriFeature = (choices) => ({
13
18
  name: "tauri",
14
- injections: [],
19
+ injections: [
20
+ // 1. Import the island entry. Inserted AFTER the
21
+ // `// @slot:body-end-islands:imports` anchor.
22
+ {
23
+ file: "pages/lib/_body-end-islands.tsx",
24
+ anchor: "// @slot:body-end-islands:imports",
25
+ position: "after",
26
+ content: `import FindInPageInit from "@/components/find-in-page-init";`,
27
+ },
28
+ // 2. Stable island marker name (same belt-and-braces guard as the
29
+ // sibling islands in the file). Inserted AFTER the
30
+ // `// @slot:body-end-islands:display-names` anchor.
31
+ {
32
+ file: "pages/lib/_body-end-islands.tsx",
33
+ anchor: "// @slot:body-end-islands:display-names",
34
+ position: "after",
35
+ content: `(FindInPageInit as { displayName?: string }).displayName = "FindInPageInit";`,
36
+ },
37
+ // 3. Island mount. Inserted AFTER the
38
+ // `{/* @slot:body-end-islands:extra-islands */}` anchor.
39
+ // when="load" (not "idle"): the island's job is to intercept
40
+ // Cmd/Ctrl+F via a keydown listener, so it must hydrate as soon as
41
+ // the islands runtime mounts — same rationale as the
42
+ // clientRouterBootstrap click intercept above it. Deferring to idle
43
+ // would leave a post-load window where Cmd+F does nothing, which is
44
+ // the very bug this injection fixes.
45
+ {
46
+ file: "pages/lib/_body-end-islands.tsx",
47
+ anchor: "{/* @slot:body-end-islands:extra-islands */}",
48
+ position: "after",
49
+ content: ` {/* Tauri-only find-in-page (Cmd/Ctrl+F) bar. Renders null outside
50
+ a Tauri WebView, so the island is inert in plain browser builds
51
+ of the same scaffold. */}
52
+ {Island({
53
+ when: "load",
54
+ children: <FindInPageInit />,
55
+ }) as unknown as VNode}`,
56
+ },
57
+ ],
15
58
  postProcess: async (targetDir) => {
16
59
  // Patch Cargo.toml package name
17
60
  const cargoPath = path.join(targetDir, "src-tauri/Cargo.toml");
package/dist/scaffold.js CHANGED
@@ -269,16 +269,24 @@ function generatePackageJson(choices) {
269
269
  // cross-file anchor validation. BREAKING upstream: the no-op
270
270
  // `linkValidation.allowExternal` knob was removed — neither the host nor
271
271
  // the generated config ever emitted it, so no migration is needed here.
272
- "@takazudo/zfb": "0.1.0-next.38",
273
- "@takazudo/zfb-runtime": "0.1.0-next.38",
272
+ // next.39 is features + fixes, no breaking changes:
273
+ // npm-dist `"use client"` island scanning, link-resolution fixes for
274
+ // directory-style hrefs, and island-registry hardening (warns on island
275
+ // marker-name collisions).
276
+ // next.40 (current pin) flips `zfb dev` to lazy rendering by default —
277
+ // pages render on first request instead of on every file-change tick
278
+ // (Takazudo/zudo-front-builder#1029); `ZFB_DEV_EAGER=1` restores eager
279
+ // mode. Dev-server-only change, no build/config migration needed.
280
+ "@takazudo/zfb": "0.1.0-next.40",
281
+ "@takazudo/zfb-runtime": "0.1.0-next.40",
274
282
  // zfb-adapter-cloudflare — required for any route with `prerender = false`.
275
283
  // Pinned in lockstep with @takazudo/zfb.
276
- "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.38",
284
+ "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.40",
277
285
  // @takazudo/zudo-doc — published from this monorepo via
278
286
  // .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
279
287
  // lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
280
288
  // version moves, so a fresh scaffold pulls the version we just published.
281
- "@takazudo/zudo-doc": "^0.2.1",
289
+ "@takazudo/zudo-doc": "^0.2.3",
282
290
  // zod — used by the generated zfb.config.ts. zfb-config-gen emits
283
291
  // `import { z } from "zod"` for the content-collection schema +
284
292
  // `z.toJSONSchema(...)` conversion. Without this dep, the consumer
@@ -332,7 +340,7 @@ function generatePackageJson(choices) {
332
340
  // @takazudo/zudo-doc/integrations/doc-history which in turn imports
333
341
  // @takazudo/zudo-doc-history-server/git-history. Without this dep the
334
342
  // plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
335
- deps["@takazudo/zudo-doc-history-server"] = "^0.2.1";
343
+ deps["@takazudo/zudo-doc-history-server"] = "^0.2.3";
336
344
  // W7A (#1736): doc-history-plugin.mjs spawns `tsx -e <inline-script>` to
337
345
  // run the v2 runtime in a TS-aware Node subprocess; without tsx the
338
346
  // plugin's preBuild step exits with ENOENT before zfb finishes config
@@ -69,7 +69,10 @@ export function generateSettingsFile(choices) {
69
69
  lines.push(` } satisfies Record<string, LocaleConfig>,`);
70
70
  }
71
71
  else {
72
- lines.push(` locales: {} satisfies Record<string, LocaleConfig>,`);
72
+ // `as`, not `satisfies`: satisfies keeps the inferred type at literal {},
73
+ // so Object.entries(settings.locales) in the generated zfb.config.ts
74
+ // yields unknown values and `zfb check` fails with TS18046 (#2053).
75
+ lines.push(` locales: {} as Record<string, LocaleConfig>,`);
73
76
  }
74
77
  // mermaid is controlled by the markdown.features block in zfb.config.ts
75
78
  // (zfb next.12+). This field is retained for compatibility with framework
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -36,6 +36,11 @@ export default function NotFoundPage(): JSX.Element {
36
36
  noindex={true}
37
37
  hideSidebar={true}
38
38
  hideToc={true}
39
+ // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
40
+ // Sidebar island — its marker never hydrates for published-package
41
+ // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
42
+ // hidden on this page anyway (zudolab/zudo-doc#2057).
43
+ sidebarOverride={<></>}
39
44
  headerOverride={<HeaderWithDefaults lang={locale} />}
40
45
  footerOverride={<FooterWithDefaults lang={locale} />}
41
46
  bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
@@ -65,6 +65,11 @@ export default function IndexPage(): JSX.Element {
65
65
  noindex={settings.noindex}
66
66
  hideSidebar={true}
67
67
  hideToc={true}
68
+ // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
69
+ // Sidebar island — its marker never hydrates for published-package
70
+ // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
71
+ // hidden on this page anyway (zudolab/zudo-doc#2057).
72
+ sidebarOverride={<></>}
68
73
  headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase("/")} />}
69
74
  footerOverride={<FooterWithDefaults lang={locale} />}
70
75
  bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
@@ -37,6 +37,7 @@ import ClientRouterBootstrap from "@/components/client-router-bootstrap";
37
37
  import DesignTokenPanelBootstrap from "@/components/design-token-panel-bootstrap";
38
38
  import ImageEnlarge, { ImageEnlargeSsrFallback } from "@/components/image-enlarge";
39
39
  import { PageLoadingOverlay } from "@takazudo/zudo-doc/page-loading";
40
+ // @slot:body-end-islands:imports
40
41
 
41
42
  // Set explicit `displayName` on each default-exported island so zfb's
42
43
  // `captureComponentName` produces a stable marker even after the SSR
@@ -51,6 +52,7 @@ import { PageLoadingOverlay } from "@takazudo/zudo-doc/page-loading";
51
52
  (DesignTokenPanelBootstrap as { displayName?: string }).displayName =
52
53
  "DesignTokenPanelBootstrap";
53
54
  (ImageEnlarge as { displayName?: string }).displayName = "ImageEnlarge";
55
+ // @slot:body-end-islands:display-names
54
56
 
55
57
  /**
56
58
  * Default sr-only label rendered as the AiChatModal SSR fallback. This
@@ -107,10 +109,14 @@ export interface BodyEndIslandsProps {
107
109
  }
108
110
 
109
111
  /**
110
- * The three default body-end islands every doc page mounts: the
111
- * design-token tweak panel (overlay, fixed-position), the AI chat
112
- * modal (`<dialog>` overlay), and the image-enlarge dialog (mounted
113
- * lazily based on viewport scan).
112
+ * The default body-end islands a doc page may mount: the design-token
113
+ * tweak panel (overlay, fixed-position), the AI chat modal (`<dialog>`
114
+ * overlay), and the image-enlarge dialog (mounted lazily based on
115
+ * viewport scan). Each is feature-gated — the design-token panel on
116
+ * `settings.designTokenPanel`, the AI chat modal (and its sr-only
117
+ * landmark heading) on `settings.aiAssistant`, and image-enlarge on
118
+ * `settings.imageEnlarge` — so a feature-off consumer ships neither the
119
+ * island marker nor a misleading landmark (zudolab/zudo-doc#2058).
114
120
  *
115
121
  * Each island is wrapped in `<Island ssrFallback>` so the heavy
116
122
  * component is NOT evaluated server-side — they depend on
@@ -118,9 +124,10 @@ export interface BodyEndIslandsProps {
118
124
  * fetch, etc. The hydration runtime swaps each placeholder on the
119
125
  * client.
120
126
  *
121
- * The `<h2 class="sr-only">AI Assistant</h2>` heading is emitted in
122
- * the SSG output so screen readers and crawlers can discover the chat
123
- * section landmark before JS hydration.
127
+ * When `settings.aiAssistant` is enabled, the
128
+ * `<h2 class="sr-only">AI Assistant</h2>` heading is emitted in the SSG
129
+ * output so screen readers and crawlers can discover the chat section
130
+ * landmark before JS hydration.
124
131
  */
125
132
  export function BodyEndIslands({
126
133
  basePath,
@@ -161,26 +168,52 @@ export function BodyEndIslands({
161
168
  )
162
169
  : null;
163
170
 
164
- // Use a visually-hidden paragraph as the AiChatModal SSR fallback so
165
- // the body label is present in static HTML for screen readers before
166
- // JS hydration. sr-only keeps it invisible to sighted users.
167
- const aiChat = Island({
168
- ssrFallback: <p class="sr-only">{aiChatBodyLabel}</p>,
169
- children: <AiChatModal basePath={basePath} />,
170
- }) as unknown as VNode;
171
+ // Gated on `settings.aiAssistant` (zudolab/zudo-doc#2058): when the AI
172
+ // assistant feature is off, neither the AiChatModal island marker nor the
173
+ // sr-only "AI Assistant" landmark heading should reach the SSG output —
174
+ // otherwise feature-off consumers ship a dead island marker plus a
175
+ // misleading screen-reader landmark for a section that never hydrates.
176
+ // Mirrors the `designTokenPanel` gating above.
177
+ //
178
+ // KNOWN CAVEAT: zfb's island scanner walks the static `"use client"`
179
+ // import chain, so gating this JSX removes the SSR marker and heading but
180
+ // may NOT strip the AiChatModal bundle from the build output. Marker
181
+ // removal is the agreed first fix (#2058); bundle stripping is out of scope.
182
+ //
183
+ // The sr-only <p> fallback keeps the body label in static HTML for screen
184
+ // readers before JS hydration; sr-only keeps it invisible to sighted users.
185
+ const aiAssistant = settings.aiAssistant ? (
186
+ <>
187
+ {/* Emits the "AI Assistant" heading in the SSG output so screen
188
+ readers can discover the chat section landmark before JS
189
+ hydration. */}
190
+ <h2 class="sr-only">AI Assistant</h2>
191
+ {
192
+ Island({
193
+ ssrFallback: <p class="sr-only">{aiChatBodyLabel}</p>,
194
+ children: <AiChatModal basePath={basePath} />,
195
+ }) as unknown as VNode
196
+ }
197
+ </>
198
+ ) : null;
171
199
 
172
- // Wave 11 (zudolab/zudo-doc#1355): the SSR fallback is the empty,
173
- // closed `<dialog class="zd-enlarge-dialog ...">` shell so the dist
174
- // HTML carries one dialog from the start. Without this the smoke
175
- // "exactly one zd-enlarge-dialog element" assertion sees zero
176
- // (skip-ssr placeholders are empty divs) and the no-JS path has no
177
- // dialog at all. Hydration replaces this shell with the real
178
- // ImageEnlarge component when the page goes idle.
179
- const imageEnlarge = Island({
180
- when: "idle",
181
- ssrFallback: <ImageEnlargeSsrFallback />,
182
- children: <ImageEnlarge />,
183
- }) as unknown as VNode;
200
+ // Gated on `settings.imageEnlarge` (zudolab/zudo-doc#2058). Same caveat as
201
+ // the AI assistant gating: removing this JSX drops the SSR dialog shell and
202
+ // island marker, but the bundle may persist via the static import scan.
203
+ //
204
+ // Wave 11 (zudolab/zudo-doc#1355): the SSR fallback is the empty, closed
205
+ // `<dialog class="zd-enlarge-dialog ...">` shell so the dist HTML carries
206
+ // one dialog from the start. Without this the smoke "exactly one
207
+ // zd-enlarge-dialog element" assertion sees zero (skip-ssr placeholders are
208
+ // empty divs) and the no-JS path has no dialog at all. Hydration replaces
209
+ // this shell with the real ImageEnlarge component when the page goes idle.
210
+ const imageEnlarge = settings.imageEnlarge
211
+ ? (Island({
212
+ when: "idle",
213
+ ssrFallback: <ImageEnlargeSsrFallback />,
214
+ children: <ImageEnlarge />,
215
+ }) as unknown as VNode)
216
+ : null;
184
217
 
185
218
  return (
186
219
  <>
@@ -190,12 +223,9 @@ export function BodyEndIslands({
190
223
  <PageLoadingOverlay />
191
224
  {clientRouterBootstrap}
192
225
  {designTokenPanelBootstrap}
193
- {/* Emits the "AI Assistant" heading in the SSG output so screen
194
- readers can discover the chat section landmark before JS
195
- hydration. */}
196
- <h2 class="sr-only">AI Assistant</h2>
197
- {aiChat}
226
+ {aiAssistant}
198
227
  {imageEnlarge}
228
+ {/* @slot:body-end-islands:extra-islands */}
199
229
  </>
200
230
  );
201
231
  }
@@ -17,9 +17,12 @@
17
17
  // keeping the create-zudo-doc template copies byte-identical to the host.
18
18
 
19
19
  import type { ComponentChildren, JSX, VNode } from "preact";
20
+ import { Island } from "@takazudo/zfb";
20
21
  import { settings } from "@/config/settings";
21
22
  import type { NavNode } from "@/utils/docs";
22
23
  import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
24
+ import { Toc, MobileToc } from "@takazudo/zudo-doc/toc";
25
+ import { getTocTitle } from "./_toc-title";
23
26
  import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
24
27
  import { NavCardGrid } from "@takazudo/zudo-doc/nav-indexing";
25
28
  import { HeadWithDefaults } from "./_head-with-defaults";
@@ -145,6 +148,31 @@ export function DocPageShell(props: DocPageShellProps): JSX.Element {
145
148
  docHistorySlot,
146
149
  } = props;
147
150
 
151
+ // TOC overrides: mount the package Toc/MobileToc with the host-resolved
152
+ // locale-aware `tocTitle`. The gating mirrors the package's
153
+ // `shouldRenderDefaultToc` exactly (`!hideToc && headings.length > 0`) so an
154
+ // undefined override never silently falls back to the package default with a
155
+ // different title. Each is wrapped in `<Island when="load">` here (the call
156
+ // site), matching how the package wraps its own default. Hydrating these
157
+ // npm-dist "use client" components requires zfb >= 0.1.0-next.39, whose
158
+ // scanner registers node_modules islands (zfb#999/#1001) — the former
159
+ // scanner-visible local shims (#2057) are gone; re-adding them would
160
+ // recreate island marker-name collisions.
161
+ const tocTitle = getTocTitle(locale);
162
+ const shouldRenderToc = !hideToc && headings.length > 0;
163
+ const tocOverride = shouldRenderToc
164
+ ? (Island({
165
+ when: "load",
166
+ children: <Toc headings={headings} title={tocTitle} />,
167
+ }) as unknown as VNode)
168
+ : undefined;
169
+ const mobileTocOverride = shouldRenderToc
170
+ ? (Island({
171
+ when: "load",
172
+ children: <MobileToc headings={headings} title={tocTitle} />,
173
+ }) as unknown as VNode)
174
+ : undefined;
175
+
148
176
  return (
149
177
  <DocLayoutWithDefaults
150
178
  title={composeMetaTitle(title)}
@@ -183,6 +211,8 @@ export function DocPageShell(props: DocPageShellProps): JSX.Element {
183
211
  currentPath={currentPath}
184
212
  />
185
213
  }
214
+ tocOverride={tocOverride}
215
+ mobileTocOverride={mobileTocOverride}
186
216
  afterSidebar={<SidebarPrepaint />}
187
217
  footerOverride={<FooterWithDefaults lang={locale} />}
188
218
  bodyEndComponents={<DocBodyEnd />}
@@ -45,10 +45,11 @@ import {
45
45
  VersionSwitcher,
46
46
  type VersionSwitcherLabels,
47
47
  } from "@takazudo/zudo-doc/i18n-version";
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.
48
+ // The bare (non-island-wrapped) package ThemeToggle, imported straight from
49
+ // the npm subpath: zfb >= 0.1.0-next.39 scans "use client" modules under
50
+ // node_modules (zfb#999/#1001), so the marker registers without the former
51
+ // project-source shim (#2048/#2057). This header composes its own Island
52
+ // below, avoiding nesting an island inside an island.
52
53
  import { ThemeToggle } from "@takazudo/zudo-doc/theme-toggle";
53
54
  import SidebarToggle from "@/components/sidebar-toggle";
54
55
  import { settings } from "@/config/settings";
@@ -249,6 +249,9 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
249
249
  self._entries = Array.isArray(data) ? data : (data.entries || []);
250
250
  prepareLc(self._entries);
251
251
  self._loading = false;
252
+ // Clear the unavailable flag BEFORE re-running search so a successful
253
+ // retry (e.g. via the openDialog() reload path) fully recovers (#2062).
254
+ self._indexUnavailable = false;
252
255
  // If user already typed, search now
253
256
  if (self._input && self._input.value.trim()) {
254
257
  self.search();
@@ -277,6 +280,20 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
277
280
  }
278
281
 
279
282
  if (!this._entries) {
283
+ // Index failed to load: show the terminal "Search unavailable" state and
284
+ // stop — do NOT show "Loading search index…" or refetch on every
285
+ // keystroke (#2062). The openDialog() reload path is the intended retry
286
+ // trigger. Clear any stale result state/count/sentinel first.
287
+ if (this._indexUnavailable) {
288
+ this.teardownSentinel();
289
+ this._allResults = [];
290
+ this._shownCount = 0;
291
+ if (this._results) {
292
+ this._results.innerHTML = "<p class=\\"text-small text-muted\\">Search unavailable</p>";
293
+ }
294
+ this.updateCount();
295
+ return;
296
+ }
280
297
  if (this._results) {
281
298
  this._results.innerHTML = "<p class=\\"text-small text-muted\\">Loading search index\\u2026</p>";
282
299
  }
@@ -0,0 +1,22 @@
1
+ // TOC section-label resolver for the Toc/MobileToc overrides mounted by
2
+ // `_doc-page-shell.tsx`. Hand-mirrors `getTocTitle`, which the zudo-doc
3
+ // package exports from "@takazudo/zudo-doc/toc" in versions > 0.2.2. The
4
+ // published version this scaffold installs (<= 0.2.2) does not yet export it,
5
+ // so the tiny lang→title map is duplicated here. After bumping the
6
+ // @takazudo/zudo-doc dependency past 0.2.2 you can replace this whole file
7
+ // with: export { getTocTitle } from "@takazudo/zudo-doc/toc";
8
+ const TOC_TITLES: Record<string, string> = {
9
+ en: "On this page",
10
+ ja: "目次",
11
+ de: "Auf dieser Seite",
12
+ };
13
+
14
+ const DEFAULT_TOC_TITLE = "On this page";
15
+
16
+ /** Return the TOC section label for the given BCP-47 language tag. */
17
+ export function getTocTitle(lang?: string): string {
18
+ if (!lang) return DEFAULT_TOC_TITLE;
19
+ // "en-US" → try "en" first, then "en-US", then the English default.
20
+ const primary = lang.split("-")[0]!;
21
+ return TOC_TITLES[primary] ?? TOC_TITLES[lang] ?? DEFAULT_TOC_TITLE;
22
+ }
@@ -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
@@ -131,8 +131,15 @@ export default function SidebarToggle({
131
131
  onClick={() => setOpen(false)}
132
132
  />
133
133
 
134
- {/* Sidebar panel - mobile only (desktop sidebar is in doc-layout) */}
134
+ {/* Sidebar panel - mobile only (desktop sidebar is in doc-layout).
135
+ `inert` when closed: the panel is only visually hidden via
136
+ `-translate-x-full`, so without `inert` its links/filter/buttons
137
+ stay in the tab order and accessibility tree while off-screen
138
+ (zudolab/zudo-doc#2059). `inert={false}` serialises to no attribute,
139
+ so the SSR (open=false → `inert`) and initial client render stay
140
+ byte-stable for hydration. */}
135
141
  <aside
142
+ inert={!open}
136
143
  className={`
137
144
  fixed top-[3.5rem] left-0 z-40 h-[calc(100vh-3.5rem)] w-[16rem] flex flex-col
138
145
  border-r border-muted bg-bg transition-transform duration-200
@@ -288,7 +288,7 @@ export default function SidebarTree({ nodes, currentSlug, rootMenuItems, backToM
288
288
  type="text"
289
289
  placeholder={filterPlaceholder}
290
290
  value={query}
291
- onChange={(e) => setQuery(e.target.value)}
291
+ onChange={(e) => setQuery(e.currentTarget.value)}
292
292
  className="bg-transparent text-small outline-none w-full text-fg placeholder:text-muted"
293
293
  />
294
294
  </div>
@@ -0,0 +1,167 @@
1
+ // Local type shim for the bare `zfb/config` specifier.
2
+ //
3
+ // `@takazudo/zfb` is consumed as a published npm package (version pinned
4
+ // in the root `package.json`). The package exposes its real config types
5
+ // under the *scoped* subpath `@takazudo/zfb/config` → `dist/config.d.ts`.
6
+ // But `zfb.config.ts` imports from the *bare* specifier `zfb/config`,
7
+ // which zfb's build tool aliases to a runtime-only stub at parse time
8
+ // (`zfb-config-stub.mjs` — `defineConfig` is identity, carrying no types).
9
+ // No real file backs `zfb/config` in `node_modules`, so this ambient
10
+ // declaration is what supplies the `ZfbConfig` type to `zfb.config.ts`.
11
+ //
12
+ // IMPORTANT — this block is the source of truth for the type `zfb check`
13
+ // (plain `tsc --noEmit`) binds against the config. An ambient `declare
14
+ // module` wins over node resolution AND over tsconfig `paths`, so it must
15
+ // be kept in sync BY HAND with the published `@takazudo/zfb/config`
16
+ // (`dist/config.d.ts`). When it lags the engine, valid config fields fail
17
+ // `pnpm check` with TS2353 (see Takazudo/zudo-front-builder#678 +
18
+ // zudolab/zudo-doc#1834 — `bundle` was missing here, blocking next.22's
19
+ // `bundle.exclude`).
20
+
21
+ declare module "zfb/config" {
22
+ /** JSX framework runtime. */
23
+ export type Framework = "preact" | "react";
24
+
25
+ /** A content collection registered with the zfb engine. */
26
+ export interface CollectionDef {
27
+ /** Identifier used at the call site (e.g. `"docs"`). */
28
+ name: string;
29
+ /** Directory (relative to the project root) holding the entries. */
30
+ path: string;
31
+ /**
32
+ * Optional schema. Reserved for v1.1 — accepted but not enforced
33
+ * today. Authored as zod and converted to JSON Schema via
34
+ * `z.toJSONSchema()` at the boundary.
35
+ */
36
+ schema?: Record<string, unknown>;
37
+ }
38
+
39
+ /** Tailwind options; absent = defaults. */
40
+ export interface TailwindConfig {
41
+ enabled?: boolean;
42
+ }
43
+
44
+ /** User-supplied plugin configuration entry. */
45
+ export interface PluginConfig {
46
+ name: string;
47
+ options?: Record<string, unknown>;
48
+ }
49
+
50
+ /**
51
+ * Bundler options. Mirrors `BundleConfig` in crates/zfb/src/config.rs
52
+ * and the published `@takazudo/zfb/config` (`dist/config.d.ts`). Added
53
+ * in next.22 (`bundle.exclude`, #664) and extended in next.23
54
+ * (`mainFields` / `external`, #676).
55
+ */
56
+ export interface BundleConfig {
57
+ /**
58
+ * Project-relative, gitignore-style globs for source files the bundler
59
+ * must NOT pull into the esbuild graph (e.g. test fixtures or
60
+ * `*.stories.tsx`). Matched files are skipped from the shadow-tree walk
61
+ * and dropped from any eager `import.meta.glob(...)` expansion.
62
+ */
63
+ exclude?: string[];
64
+ /**
65
+ * Explicit esbuild `main-fields` for the `--platform=neutral` page/SSR
66
+ * pass (empty by default under `neutral`), letting CJS-`main`-only deps
67
+ * resolve. Mirrors `BundleConfig::main_fields`.
68
+ */
69
+ mainFields?: string[];
70
+ /**
71
+ * Bare specifiers to mark external in the `--platform=neutral` pass so
72
+ * esbuild leaves them unbundled. Mirrors `BundleConfig::external`.
73
+ */
74
+ external?: string[];
75
+ }
76
+
77
+ /** Mirrors the Rust `Config` struct one-for-one. */
78
+ export interface ZfbConfig {
79
+ outDir?: string;
80
+ publicDir?: string;
81
+ host?: string;
82
+ port?: number;
83
+ framework?: Framework;
84
+ collections?: CollectionDef[];
85
+ tailwind?: TailwindConfig;
86
+ /**
87
+ * Bundler options. `bundle.exclude` keeps project-relative globs out of
88
+ * the esbuild graph — used here to skip `packages/md-plugins/__fixtures__/**`
89
+ * so the MDX link resolver no longer walks the test fixtures (silences
90
+ * ~15 pre-existing broken-link warnings). Mirrors `Config::bundle`.
91
+ */
92
+ bundle?: BundleConfig;
93
+ plugins?: PluginConfig[];
94
+ adapter?: string;
95
+ /**
96
+ * Strip `.md` / `.mdx` from in-page `<a href>` paths and append a
97
+ * trailing `/` so author-written `[label](other.mdx)` references
98
+ * resolve to the rendered route URL. Mirrors Config::strip_md_ext
99
+ * in crates/zfb/src/config.rs (zudolab/zfb#131).
100
+ */
101
+ stripMdExt?: boolean;
102
+ /**
103
+ * Site base path. Prefixed onto stable HTML asset URLs (CSS / JS
104
+ * `<link>` and `<script>` tags). Normalised to start AND end with
105
+ * `/`; `undefined` / `""` / `"/"` all behave identically (no
106
+ * prefix). Mirrors Config::base in crates/zfb/src/config.rs
107
+ * (Takazudo/zudo-front-builder#154).
108
+ */
109
+ base?: string;
110
+ /**
111
+ * Configures the syntect-based syntax highlighter shipped with zfb.
112
+ * Mirrors `code_highlight` in crates/zfb/src/config.rs (Takazudo/zudo-front-builder#188 / sub #194; landed in commit 339e30f).
113
+ * When omitted, the engine falls back to the hardcoded default theme `base16-ocean.dark`.
114
+ */
115
+ codeHighlight?: {
116
+ theme?: string;
117
+ themesDir?: string;
118
+ };
119
+ /**
120
+ * Markdown link resolver (port of `remarkResolveMarkdownLinks`).
121
+ * Mirrors `Config::resolve_markdown_links` in crates/zfb/src/config.rs
122
+ * (Takazudo/zudo-front-builder PR #234 / zudolab/zudo-doc#1577).
123
+ * When `enabled: true`, the build appends `ResolveLinksPlugin` to the
124
+ * mdast pipeline so author-written `[label](./other.mdx)` links are
125
+ * rewritten to the corresponding rendered route URL — bypassing the
126
+ * file→directory transformation that breaks relative paths in dist
127
+ * HTML when `foo.mdx` becomes `foo/index.html`.
128
+ */
129
+ resolveMarkdownLinks?: {
130
+ enabled?: boolean;
131
+ docsDir?: string;
132
+ dirs?: Array<{ dir: string; routePrefix: string }>;
133
+ onBrokenLinks?: "warn" | "error" | "ignore";
134
+ };
135
+ /**
136
+ * Whether the basePath rewriter should append a trailing `/` to
137
+ * extensionless absolute hrefs. Mirrors `Config::trailing_slash` in
138
+ * crates/zfb/src/config.rs (Takazudo/zudo-front-builder PR #234 /
139
+ * zudolab/zudo-doc#1579). Off by default — preserves byte-for-byte
140
+ * parity with the pre-`trailingSlash` build for projects that
141
+ * haven't opted in.
142
+ */
143
+ trailingSlash?: boolean;
144
+ /**
145
+ * Markdown / MDX pipeline options. Mirrors `Config::markdown` →
146
+ * `MarkdownConfig` in crates/zfb/src/config.rs. zfb next.12 moved the
147
+ * former-Core features under `markdown.features` and next.13 ships the
148
+ * rest as opt-in; zudo-doc uses `markdown.features` to opt back into the
149
+ * former-Core four plus the additional opt-in features (#1804). Each
150
+ * `features` value is per-feature: `true` for boolean-shorthand features,
151
+ * or an options object for object-typed features.
152
+ */
153
+ markdown?: {
154
+ gfm?: boolean | Record<string, boolean>;
155
+ toc?: Record<string, unknown>;
156
+ externalLinks?: Record<string, unknown>;
157
+ cjkFriendly?: boolean;
158
+ features?: Record<string, boolean | Record<string, unknown>>;
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Identity helper: returns the supplied config as-is, but typed
164
+ * against `ZfbConfig`. Use as the default export of `zfb.config.ts`.
165
+ */
166
+ export function defineConfig(config: ZfbConfig): ZfbConfig;
167
+ }
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { useState, useEffect, useCallback, useMemo, useRef } from "preact/compat";
2
4
  import type { Change } from "diff";
3
5
  import type { DocHistoryData, DocHistoryEntry } from "@/types/doc-history";
@@ -131,6 +131,11 @@ export function TagDetailPageView({
131
131
  noindex={settings.noindex}
132
132
  hideSidebar={true}
133
133
  hideToc={true}
134
+ // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
135
+ // Sidebar island — its marker never hydrates for published-package
136
+ // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
137
+ // hidden on this page anyway (zudolab/zudo-doc#2057).
138
+ sidebarOverride={<></>}
134
139
  // Tag segment URL-encoded — emitted href/path sites only; route params
135
140
  // stay raw (e.g. "type:guide" → "type%3Aguide").
136
141
  headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`${prefix}/docs/tags/${encodeURIComponent(tag)}`)} />}
@@ -184,6 +189,11 @@ export function TagsIndexPageView({ locale }: { locale: string }): JSX.Element {
184
189
  noindex={settings.noindex}
185
190
  hideSidebar={true}
186
191
  hideToc={true}
192
+ // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
193
+ // Sidebar island — its marker never hydrates for published-package
194
+ // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
195
+ // hidden on this page anyway (zudolab/zudo-doc#2057).
196
+ sidebarOverride={<></>}
187
197
  headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`${prefix}/docs/tags`)} />}
188
198
  breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
189
199
  footerOverride={<FooterWithDefaults lang={locale} />}
@@ -103,6 +103,11 @@ export default function LocaleIndexPage({ params }: PageArgs): JSX.Element {
103
103
  noindex={settings.noindex}
104
104
  hideSidebar={true}
105
105
  hideToc={true}
106
+ // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
107
+ // Sidebar island — its marker never hydrates for published-package
108
+ // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
109
+ // hidden on this page anyway (zudolab/zudo-doc#2057).
110
+ sidebarOverride={<></>}
106
111
  headerOverride={<HeaderWithDefaults lang={locale as Locale} currentPath={withBase(`/${locale}/`)} />}
107
112
  footerOverride={<FooterWithDefaults lang={locale} />}
108
113
  bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { useState, useEffect, useRef } from "preact/compat";
2
4
  import type { JSX } from "preact";
3
5
 
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { useState, useEffect, useRef } from "preact/compat";
2
4
  import { FindBar } from "./find-bar";
3
5
  import { createFindInPage } from "@/utils/find-in-page";
@@ -30,14 +32,18 @@ export default function FindInPageInit() {
30
32
  return () => document.removeEventListener("keydown", handler);
31
33
  }, [isTauri]);
32
34
 
33
- // Clear search on zfb page navigation
35
+ // Clear search on zfb page navigation. zfb navigates via SPA body swap and
36
+ // fires "zfb:before-preparation" on document before nav — it never fires the
37
+ // native "pagehide" (full-unload) event. The literal is inlined because
38
+ // downstream scaffolds do not depend on @takazudo/zudo-doc as a runtime dep
39
+ // (same reason as the designTokenPanel bootstrap).
34
40
  useEffect(() => {
35
41
  const handler = () => {
36
42
  findInPageRef.current.stop();
37
43
  setVisible(false);
38
44
  };
39
- document.addEventListener("pagehide", handler);
40
- return () => document.removeEventListener("pagehide", handler);
45
+ document.addEventListener("zfb:before-preparation", handler);
46
+ return () => document.removeEventListener("zfb:before-preparation", handler);
41
47
  }, []);
42
48
 
43
49
  if (!isTauri) return null;
@@ -65,6 +65,11 @@ export function VersionsPageView({ locale }: { locale: string }): JSX.Element {
65
65
  noindex={settings.noindex}
66
66
  hideSidebar={true}
67
67
  hideToc={true}
68
+ // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
69
+ // Sidebar island — its marker never hydrates for published-package
70
+ // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
71
+ // hidden on this page anyway (zudolab/zudo-doc#2057).
72
+ sidebarOverride={<></>}
68
73
  headerOverride={<HeaderWithDefaults lang={locale as Locale} currentPath={withBase(`${prefix}/docs/versions`)} />}
69
74
  footerOverride={<FooterWithDefaults lang={locale} />}
70
75
  bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}