create-zudo-doc 0.2.2 → 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/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.2",
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.2";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "0.2.2",
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 ?? "/"} />}
@@ -109,10 +109,14 @@ export interface BodyEndIslandsProps {
109
109
  }
110
110
 
111
111
  /**
112
- * The three default body-end islands every doc page mounts: the
113
- * design-token tweak panel (overlay, fixed-position), the AI chat
114
- * modal (`<dialog>` overlay), and the image-enlarge dialog (mounted
115
- * 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).
116
120
  *
117
121
  * Each island is wrapped in `<Island ssrFallback>` so the heavy
118
122
  * component is NOT evaluated server-side — they depend on
@@ -120,9 +124,10 @@ export interface BodyEndIslandsProps {
120
124
  * fetch, etc. The hydration runtime swaps each placeholder on the
121
125
  * client.
122
126
  *
123
- * The `<h2 class="sr-only">AI Assistant</h2>` heading is emitted in
124
- * the SSG output so screen readers and crawlers can discover the chat
125
- * 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.
126
131
  */
127
132
  export function BodyEndIslands({
128
133
  basePath,
@@ -163,26 +168,52 @@ export function BodyEndIslands({
163
168
  )
164
169
  : null;
165
170
 
166
- // Use a visually-hidden paragraph as the AiChatModal SSR fallback so
167
- // the body label is present in static HTML for screen readers before
168
- // JS hydration. sr-only keeps it invisible to sighted users.
169
- const aiChat = Island({
170
- ssrFallback: <p class="sr-only">{aiChatBodyLabel}</p>,
171
- children: <AiChatModal basePath={basePath} />,
172
- }) 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;
173
199
 
174
- // Wave 11 (zudolab/zudo-doc#1355): the SSR fallback is the empty,
175
- // closed `<dialog class="zd-enlarge-dialog ...">` shell so the dist
176
- // HTML carries one dialog from the start. Without this the smoke
177
- // "exactly one zd-enlarge-dialog element" assertion sees zero
178
- // (skip-ssr placeholders are empty divs) and the no-JS path has no
179
- // dialog at all. Hydration replaces this shell with the real
180
- // ImageEnlarge component when the page goes idle.
181
- const imageEnlarge = Island({
182
- when: "idle",
183
- ssrFallback: <ImageEnlargeSsrFallback />,
184
- children: <ImageEnlarge />,
185
- }) 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;
186
217
 
187
218
  return (
188
219
  <>
@@ -192,11 +223,7 @@ export function BodyEndIslands({
192
223
  <PageLoadingOverlay />
193
224
  {clientRouterBootstrap}
194
225
  {designTokenPanelBootstrap}
195
- {/* Emits the "AI Assistant" heading in the SSG output so screen
196
- readers can discover the chat section landmark before JS
197
- hydration. */}
198
- <h2 class="sr-only">AI Assistant</h2>
199
- {aiChat}
226
+ {aiAssistant}
200
227
  {imageEnlarge}
201
228
  {/* @slot:body-end-islands:extra-islands */}
202
229
  </>
@@ -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,14 +45,12 @@ import {
45
45
  VersionSwitcher,
46
46
  type VersionSwitcherLabels,
47
47
  } from "@takazudo/zudo-doc/i18n-version";
48
- // ThemeToggle via the project-source shim (`@/components/theme-toggle`)
49
- // rather than the package subpath directly. zfb's island scanner does not
50
- // register "use client" modules under node_modules (zfb#999), so importing
51
- // the package component here would emit an island marker with no registry
52
- // entry and the toggle would never hydrate (zudolab/zudo-doc#2048). The shim
53
- // re-wraps the bare (non-island-wrapped) package ThemeToggle; this wrapper
54
- // composes its own Island below, avoiding nesting an island inside an island.
55
- import { ThemeToggle } from "@/components/theme-toggle";
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.
53
+ import { ThemeToggle } from "@takazudo/zudo-doc/theme-toggle";
56
54
  import SidebarToggle from "@/components/sidebar-toggle";
57
55
  import { settings } from "@/config/settings";
58
56
  import { defaultLocale, locales, t, type Locale } from "@/config/i18n";
@@ -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
+ }
@@ -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
@@ -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 ?? "/"} />}
@@ -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 ?? "/"} />}
@@ -1,23 +0,0 @@
1
- "use client";
2
-
3
- // Scanner-visible ThemeToggle shim. zfb's island scanner does not register
4
- // "use client" modules located under node_modules (Takazudo/zudo-front-builder#999),
5
- // so importing the package ThemeToggle straight into the server-rendered header
6
- // emits an island marker with no matching registry entry — the toggle renders
7
- // but never hydrates, on every page (zudolab/zudo-doc#2048). This thin
8
- // project-source wrapper gives the scanner a local binding to register; it
9
- // re-wraps the bare (non-island-wrapped) package component unchanged.
10
- import type { JSX } from "preact";
11
- import {
12
- ThemeToggle as ZudoDocThemeToggle,
13
- type ThemeToggleProps,
14
- } from "@takazudo/zudo-doc/theme-toggle";
15
-
16
- export function ThemeToggle(props: ThemeToggleProps): JSX.Element {
17
- return <ZudoDocThemeToggle {...props} />;
18
- }
19
- ThemeToggle.displayName = "ThemeToggle";
20
-
21
- export type { ThemeToggleProps };
22
-
23
- export default ThemeToggle;