create-zudo-doc 0.2.21 → 0.2.22

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 (71) hide show
  1. package/dist/features/image-enlarge.d.ts +10 -2
  2. package/dist/features/image-enlarge.js +10 -2
  3. package/dist/scaffold.js +20 -22
  4. package/dist/zfb-config-gen.d.ts +16 -10
  5. package/dist/zfb-config-gen.js +34 -239
  6. package/package.json +1 -1
  7. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  8. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  9. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  10. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  11. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  12. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  13. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  15. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  16. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  17. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  18. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  19. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  20. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  21. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  22. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  23. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  24. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  25. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  26. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  27. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  28. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  29. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  30. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  31. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  33. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  34. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  35. package/templates/base/pages/lib/locale-merge.ts +32 -145
  36. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  37. package/templates/base/pages/robots.txt.tsx +2 -26
  38. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  39. package/templates/base/src/components/doc-history.tsx +9 -8
  40. package/templates/base/src/components/image-enlarge.tsx +11 -8
  41. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  42. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  43. package/templates/base/src/components/site-tree-nav.tsx +6 -220
  44. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  45. package/templates/base/src/config/i18n.ts +9 -0
  46. package/templates/base/src/config/z-index-tokens.ts +5 -4
  47. package/templates/base/src/styles/global.css +5 -579
  48. package/templates/base/src/utils/base.ts +1 -1
  49. package/templates/base/src/utils/docs.ts +47 -16
  50. package/templates/base/src/utils/github.ts +12 -9
  51. package/templates/base/src/utils/nav-scope.ts +13 -42
  52. package/templates/base/src/utils/sidebar.ts +18 -86
  53. package/templates/base/src/utils/slug.ts +10 -53
  54. package/templates/base/src/utils/smart-break.tsx +12 -120
  55. package/templates/base/src/utils/tags.ts +25 -68
  56. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  57. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  58. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  59. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  60. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  61. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  62. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  63. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  64. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  65. package/templates/base/plugins/connect-adapter.mjs +0 -169
  66. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  67. package/templates/base/scripts/gen-z-index.mjs +0 -157
  68. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  69. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  70. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  71. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,292 +1,58 @@
1
- // Pure URL-enumeration helpers shared by both page paths() functions and the
2
- // sitemap. Extracting these prevents the sitemap from drifting out of sync
3
- // with the actual routes the page modules produce.
4
- //
5
- // Each enumerator returns absolute paths (with settings.base prefix and
6
- // trailing slash applied) as expected by the sitemap and page modules.
7
- // `enumerateAllRoutes()` composes the others and returns a deduped
8
- // Map<url, lastmod> that the sitemap renderer wraps directly.
9
- //
10
- // Design principles:
11
- // - Draft pages are always excluded (never built).
12
- // - Unlisted pages ARE included — they have real HTML files and should
13
- // appear in the sitemap even though they're hidden from nav.
14
- // - toRouteSlug() is applied to all entry ids so category index pages
15
- // (e.g. "getting-started/index" → "getting-started") get correct URLs.
16
- // - Auto-generated category index pages (categories without index.mdx) are
17
- // emitted by building the nav tree and calling collectAutoIndexNodes.
18
-
19
- import { loadDocs } from "../_data";
20
- import { mergeLocaleDocs } from "./locale-merge";
21
- import { resolveNavSource, resolveVersionedLocaleSource } from "./_nav-source-docs";
1
+ // Thin stub — route-enumerators moved to the package (epic #2344, S6).
2
+ // Calls `createRouteEnumerators(ctx)` from @takazudo/zudo-doc/route-enumerators
3
+ // with the host singletons injected, then re-exports the resulting enumeration
4
+ // functions so all existing call sites continue to work unchanged.
5
+
6
+ import { createRouteEnumerators } from "@takazudo/zudo-doc/route-enumerators";
7
+ import type {
8
+ DocsEntryForTags,
9
+ TagInfoForEnum,
10
+ DocPageEntry,
11
+ DocNavNode,
12
+ } from "@takazudo/zudo-doc/route-enumerators";
22
13
  import { settings } from "@/config/settings";
23
14
  import { defaultLocale, type Locale } from "@/config/i18n";
24
- import type { VersionConfig } from "@/config/settings";
25
- import type { DocsEntry } from "@/types/docs-entry";
26
- import { docsUrl, versionedDocsUrl, withBase } from "@/utils/base";
15
+ import { docsUrl, versionedDocsUrl, withBase, isDefaultLocaleOnlyPath } from "@/utils/base";
16
+ import { buildNavTree, collectAutoIndexNodes, type CategoryMeta } from "@/utils/docs";
27
17
  import { collectTags } from "@/utils/tags";
28
18
  import { toRouteSlug } from "@/utils/slug";
29
- import {
30
- buildNavTree,
31
- collectAutoIndexNodes,
32
- } from "@/utils/docs";
33
-
34
- // ---------------------------------------------------------------------------
35
- // enumerateDocsRoutes
36
- // ---------------------------------------------------------------------------
37
-
38
- /**
39
- * Enumerate all doc page URLs for a locale.
40
- *
41
- * For the default locale: loads the "docs" collection directly.
42
- * For non-default locales: inlines a locale-first merge — locale docs take
43
- * priority; base EN docs fill in slugs not covered by the locale collection,
44
- * with default-locale-only paths excluded. A nav-tree pass then adds
45
- * auto-generated category index pages.
46
- *
47
- * Applies toRouteSlug so "category/index" entries become "category/" URLs.
48
- * Returns deduplicated URL strings with base prefix and trailing slash.
49
- */
50
- export function enumerateDocsRoutes(locale: string): string[] {
51
- const urls: string[] = [];
52
-
53
- // Identity-stable nav source — same instances the doc routes use, so the
54
- // nav-tree fast-path applies here too (#1902).
55
- const { docs: allDocs, navDocs, categoryMeta } = resolveNavSource(
56
- locale as Locale,
57
- undefined,
58
- { applyDefaultLocaleOnlyFilter: true, keepUnlisted: true },
59
- );
60
- const tree = buildNavTree(navDocs, locale as Locale, categoryMeta);
61
-
62
- for (const doc of allDocs) {
63
- // A `category_no_page` index.mdx is metadata-only — no route, so no sitemap
64
- // URL. Same exclusion the doc-route paths() apply (zfb retains every .mdx
65
- // as a collection entry, so the skip must be explicit).
66
- if (doc.data.category_no_page === true) continue;
67
- // Canonical route slug via the one shared rule (@/utils/slug). `doc.id` is
68
- // already `toRouteSlug(doc.slug)` (bridged through stripIndexSuffix in
69
- // pages/_data.ts), so a bare root index.mdx is "" here → `/docs/` — the
70
- // canonical root URL (#1891). The `toRouteSlug` fallback is a redundant
71
- // safety re-application of the same rule (idempotent on the already-
72
- // stripped id); kept so this enumerator's slug derivation reads as a
73
- // single explicit call to the canonical helper.
74
- urls.push(docsUrl(doc.data.slug ?? toRouteSlug(doc.id), locale as Locale));
75
- }
76
- for (const node of collectAutoIndexNodes(tree)) {
77
- urls.push(docsUrl(node.slug, locale as Locale));
78
- }
79
-
80
- return [...new Set(urls)];
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // enumerateTagsRoutes
85
- // ---------------------------------------------------------------------------
86
-
87
- /**
88
- * Enumerate tag-index and per-tag URLs for a locale.
89
- *
90
- * Uses the same tag map as the tag pages (unlisted + draft excluded) so the
91
- * sitemap lists exactly the same tag pages that get built.
92
- *
93
- * Returns:
94
- * - /docs/tags/ (or /{locale}/docs/tags/)
95
- * - /docs/tags/{tag}/ (or /{locale}/docs/tags/{tag}/) for each unique tag
96
- */
97
- export function enumerateTagsRoutes(locale: string): string[] {
98
- if (!settings.docTags) return [];
99
-
100
- const urls: string[] = [];
101
-
102
- const tagsBase =
103
- locale === defaultLocale ? "/docs/tags" : `/${locale}/docs/tags`;
104
- urls.push(withBase(tagsBase));
105
-
106
- // Collect tags from the same merged doc set the tag pages use.
107
- // Filter unlisted + draft + category_no_page — mirrors the tag [tag].tsx
108
- // pages so the sitemap lists exactly the tag pages that get built (a
109
- // category_no_page index has no route, so a tag it carries must not coin a
110
- // tag page that links back to it). The category_no_page drop happens AFTER
111
- // the locale merge so a locale override carrying the flag first wins the
112
- // merge — pre-merge filtering would let the unflagged base doc resurface.
113
- let docs: DocsEntry[];
114
- if (locale === defaultLocale) {
115
- docs = loadDocs("docs").filter(
116
- (d) => !d.data.unlisted && !d.data.draft && !d.data.category_no_page,
117
- );
118
- } else {
119
- const result = mergeLocaleDocs({
120
- baseDocs: loadDocs("docs").filter((d) => !d.data.draft),
121
- localeDocs: loadDocs(`docs-${locale}`).filter((d) => !d.data.draft),
122
- applyDefaultLocaleOnlyFilter: true,
123
- });
124
- docs = result.docs.filter((d) => !d.data.category_no_page);
125
- }
126
-
127
- const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
128
-
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);
134
- const tagPath =
135
- locale === defaultLocale
136
- ? `/docs/tags/${encoded}`
137
- : `/${locale}/docs/tags/${encoded}`;
138
- urls.push(withBase(tagPath));
139
- }
140
-
141
- return urls;
142
- }
143
-
144
- // ---------------------------------------------------------------------------
145
- // enumerateVersionedRoutes
146
- // ---------------------------------------------------------------------------
147
-
148
- /**
149
- * Enumerate doc URLs for a single (version, locale) combination.
150
- *
151
- * For the default locale: loads `docs-v-${version.slug}`.
152
- * For non-default locales: locale-first merge — locale-specific collection
153
- * takes priority; base EN collection fills in pages not yet translated.
154
- * If the locale collection doesn't exist for this version, all pages fall
155
- * back to the EN base (matching the page module's behaviour).
156
- *
157
- * Returns versioned URLs like /v/{version}/docs/{slug}/ or
158
- * /v/{version}/{locale}/docs/{slug}/.
159
- */
160
- export function enumerateVersionedRoutes(
161
- version: VersionConfig,
162
- locale: string,
163
- ): string[] {
164
- const urls: string[] = [];
165
-
166
- if (locale === defaultLocale) {
167
- // Versioned EN base — identity-stable source (#1902).
168
- const { docs: allDocs, navDocs, categoryMeta } = resolveNavSource(
169
- "en",
170
- version.slug,
171
- );
172
- const tree = buildNavTree(navDocs, "en", categoryMeta);
173
-
174
- for (const doc of allDocs) {
175
- // category_no_page index.mdx → no route, no sitemap URL (see paths()).
176
- if (doc.data.category_no_page === true) continue;
177
- const slug = doc.data.slug ?? toRouteSlug(doc.id);
178
- urls.push(versionedDocsUrl(slug, version.slug));
179
- }
180
- for (const node of collectAutoIndexNodes(tree)) {
181
- urls.push(versionedDocsUrl(node.slug, version.slug));
182
- }
183
- } else {
184
- const localeDir = version.locales?.[locale]?.dir;
19
+ import type { DocsEntry } from "@/types/docs-entry";
20
+ import { loadDocs } from "../_data";
21
+ import { resolveNavSource, resolveVersionedLocaleSource } from "./_nav-source-docs";
22
+ import { mergeLocaleDocs } from "./locale-merge";
185
23
 
186
- // Versioned locale source — locale-first merge over the version's EN base
187
- // (identity-stable; #1902).
188
- const { docs: allDocs, navDocs, categoryMeta } = resolveVersionedLocaleSource(
189
- version.slug,
190
- version.docsDir,
24
+ export const {
25
+ enumerateDocsRoutes,
26
+ enumerateTagsRoutes,
27
+ enumerateVersionedRoutes,
28
+ enumerateAllRoutes,
29
+ } = createRouteEnumerators({
30
+ defaultLocale,
31
+ getLocaleKeys: () => Object.keys(settings.locales),
32
+ getVersions: () => settings.versions,
33
+ getDocTags: () => settings.docTags,
34
+ docsUrl,
35
+ versionedDocsUrl,
36
+ withBase,
37
+ loadDocs,
38
+ isDefaultLocaleOnlyPath,
39
+ // The factory describes collectTags / buildNavTree with the package's minimal
40
+ // structural slots (DocsEntryForTags / DocPageEntry / Map<string, unknown> /
41
+ // TagInfoForEnum). The host helpers are typed against the concrete project
42
+ // types (DocsEntry / NavNode / the Locale union / TagInfo) but are runtime-
43
+ // identical, so the stub adapts them with thin casting wrappers at the
44
+ // injection boundary where the host owns the type knowledge.
45
+ collectTags: (docs: DocsEntryForTags[], slugFn): Map<string, TagInfoForEnum> =>
46
+ collectTags(docs as unknown as DocsEntry[], slugFn),
47
+ toRouteSlug,
48
+ buildNavTree: (docs: DocPageEntry[], locale: string, categoryMeta: Map<string, unknown>) =>
49
+ buildNavTree(
50
+ docs as unknown as DocsEntry[],
191
51
  locale as Locale,
192
- localeDir,
193
- { applyDefaultLocaleOnlyFilter: true, keepUnlisted: true },
194
- );
195
- const tree = buildNavTree(navDocs, locale as Locale, categoryMeta);
196
-
197
- for (const doc of allDocs) {
198
- // category_no_page index.mdx → no route, no sitemap URL (see paths()).
199
- if (doc.data.category_no_page === true) continue;
200
- const slug = doc.data.slug ?? toRouteSlug(doc.id);
201
- urls.push(versionedDocsUrl(slug, version.slug, locale as Locale));
202
- }
203
- for (const node of collectAutoIndexNodes(tree)) {
204
- urls.push(versionedDocsUrl(node.slug, version.slug, locale as Locale));
205
- }
206
- }
207
-
208
- return [...new Set(urls)];
209
- }
210
-
211
- // ---------------------------------------------------------------------------
212
- // enumerateAllRoutes
213
- // ---------------------------------------------------------------------------
214
-
215
- /**
216
- * Compose all route enumerators into a deduped Map<url, lastmod>.
217
- *
218
- * Covers:
219
- * - Site root
220
- * - Default-locale docs + tags
221
- * - Per-locale homepages, docs, and tags
222
- * - Versioned EN docs (for each version in settings.versions)
223
- * - Versioned locale docs (for each locale in settings.locales)
224
- *
225
- * The map keys are absolute paths (with settings.base prefix + trailing
226
- * slash). The sitemap renderer prefixes each with settings.siteUrl.
227
- */
228
- export function enumerateAllRoutes(): Map<string, string> {
229
- const today = new Date().toISOString().split("T")[0] ?? "";
230
- const routes = new Map<string, string>();
231
-
232
- function add(url: string): void {
233
- if (!routes.has(url)) {
234
- routes.set(url, today);
235
- }
236
- }
237
-
238
- // Site root
239
- add(withBase("/"));
240
-
241
- // Default locale docs
242
- for (const url of enumerateDocsRoutes(defaultLocale)) {
243
- add(url);
244
- }
245
-
246
- // Default locale tags
247
- for (const url of enumerateTagsRoutes(defaultLocale)) {
248
- add(url);
249
- }
250
-
251
- // Non-default locales
252
- for (const locale of Object.keys(settings.locales)) {
253
- add(withBase(`/${locale}`));
254
-
255
- for (const url of enumerateDocsRoutes(locale)) {
256
- add(url);
257
- }
258
-
259
- for (const url of enumerateTagsRoutes(locale)) {
260
- add(url);
261
- }
262
- }
263
-
264
- // Versions listing pages — /docs/versions/ and /{locale}/docs/versions/.
265
- // These static utility pages are built by pages/docs/versions.tsx and
266
- // pages/[locale]/docs/versions.tsx whenever versioning is configured.
267
- // They are not part of any content collection so they are added explicitly.
268
- if (settings.versions) {
269
- add(withBase("/docs/versions"));
270
- for (const locale of Object.keys(settings.locales)) {
271
- add(withBase(`/${locale}/docs/versions`));
272
- }
273
- }
274
-
275
- // Versioned docs
276
- if (settings.versions) {
277
- for (const version of settings.versions as VersionConfig[]) {
278
- for (const url of enumerateVersionedRoutes(version, defaultLocale)) {
279
- add(url);
280
- }
281
- // Non-default locales always have versioned pages (they fall back to EN
282
- // when a locale-specific collection is not configured).
283
- for (const locale of Object.keys(settings.locales)) {
284
- for (const url of enumerateVersionedRoutes(version, locale)) {
285
- add(url);
286
- }
287
- }
288
- }
289
- }
290
-
291
- return routes;
292
- }
52
+ categoryMeta as Map<string, CategoryMeta>,
53
+ ) as DocNavNode[],
54
+ collectAutoIndexNodes,
55
+ resolveNavSource,
56
+ resolveVersionedLocaleSource,
57
+ mergeLocaleDocs,
58
+ });
@@ -1,29 +1,5 @@
1
- // Prerendered robots.txt page route, gated by settings.noindex.
2
- //
3
- // Filename → output extension mapping (zfb convention): the
4
- // second-to-last `.`-separated segment of the stem becomes the output
5
- // extension, so `robots.txt.tsx` builds `dist/robots.txt`. The
6
- // explicit `contentType` export pins the dev-server Content-Type
7
- // header to `text/plain` regardless of the filename hint.
8
-
1
+ import { renderRobots } from "@takazudo/zudo-doc/robots";
9
2
  import { settings } from "@/config/settings";
10
3
 
11
- export const frontmatter = { title: "Robots" };
12
4
  export const contentType = "text/plain";
13
-
14
- export default function Robots(): string {
15
- if (settings.noindex) {
16
- // noindex: disallow all crawlers. Omit Sitemap: — advertising a sitemap
17
- // while disallowing all crawlers is contradictory.
18
- return `User-agent: *\nDisallow: /\n`;
19
- }
20
-
21
- const siteUrlBase = (settings.siteUrl ?? "").replace(/\/$/, "");
22
- const hasSitemapLine = siteUrlBase !== "" && settings.sitemap;
23
-
24
- const sitemapLine = hasSitemapLine
25
- ? `Sitemap: ${siteUrlBase}/sitemap.xml\n`
26
- : "";
27
-
28
- return `User-agent: *\nAllow: /\n${sitemapLine}`;
29
- }
5
+ export default () => renderRobots(settings);
@@ -1,12 +1,11 @@
1
- "use client";
2
-
3
- // W6A stub — no-op default export.
1
+ // W6A stub — no-op default + AiChatModal named exports.
4
2
  //
5
- // The host (zudo-doc showcase) ships a full AI-chat modal island here. In
6
- // generated downstream projects the AI assistant is gated behind a future
7
- // settings flag; for now the file exists so unconditional page imports
8
- // (`pages/lib/_body-end-islands.tsx`) resolve, and the component renders
9
- // nothing. Wire a real implementation by replacing this file.
3
+ // The AiChatModal island now ships from the package
4
+ // (`@takazudo/zudo-doc/ai-chat-modal`); the unconditional
5
+ // `pages/lib/_body-end-islands.tsx` imports it directly from there. This
6
+ // stub exists only so any project-local code that references the
7
+ // `@/components/ai-chat-modal` path still resolves it renders nothing.
8
+ // Replace this file to wire a project-specific modal.
10
9
  import type { JSX } from "preact";
11
10
 
12
11
  function AiChatModal(): JSX.Element | null {
@@ -15,3 +14,5 @@ function AiChatModal(): JSX.Element | null {
15
14
  AiChatModal.displayName = "AiChatModal";
16
15
 
17
16
  export default AiChatModal;
17
+
18
+ export { AiChatModal };
@@ -1,13 +1,14 @@
1
- "use client";
2
-
3
1
  // W6A stub — no-op default + DocHistory named exports.
4
2
  //
5
- // When the docHistory feature is enabled, the feature template
6
- // overwrites this file with the real island. Generated projects
7
- // without the feature ship the no-op so the unconditional
8
- // `pages/lib/_doc-history-area` import resolves. The host module also
9
- // exposes `DocHistory` as a named export, so the stub mirrors both
10
- // shapes to keep the import surface stable.
3
+ // The real DocHistory island now ships from the package
4
+ // (`@takazudo/zudo-doc/doc-history`); the unconditional
5
+ // `pages/lib/_doc-history-area.tsx` imports it directly from there. When
6
+ // the docHistory feature is enabled the feature template overwrites this
7
+ // file with a re-export shim pointing at the package island. Generated
8
+ // projects without the feature ship this no-op so any project-local code
9
+ // that references the `@/components/doc-history` path still resolves. The
10
+ // host module exposes `DocHistory` as a named export, so the stub mirrors
11
+ // both the default and named shapes to keep the import surface stable.
11
12
  import type { JSX } from "preact";
12
13
 
13
14
  function DocHistoryComponent(): JSX.Element | null {
@@ -1,12 +1,13 @@
1
- "use client";
2
-
3
- // W6A stub — no-op default + ImageEnlargeSsrFallback named exports.
1
+ // W6A stub — no-op default + ImageEnlarge / ImageEnlargeSsrFallback exports.
4
2
  //
5
- // When the imageEnlarge feature is enabled, the feature template
6
- // overwrites this file with the real island and SSR fallback. Generated
7
- // projects without the feature ship the no-op so the unconditional
8
- // `pages/lib/_body-end-islands` import resolves (the body-end islands
9
- // renderer references both the default and the SSR fallback).
3
+ // The real ImageEnlarge island and its SSR fallback now ship from the
4
+ // package (`@takazudo/zudo-doc/image-enlarge`); the unconditional
5
+ // `pages/lib/_body-end-islands.tsx` imports them directly from there. When
6
+ // the imageEnlarge feature is enabled the feature template overwrites this
7
+ // file with a re-export shim pointing at the package island. Generated
8
+ // projects without the feature ship this no-op so any project-local code
9
+ // that references the `@/components/image-enlarge` path still resolves (the
10
+ // body-end renderer references both the default and the SSR fallback).
10
11
  import type { JSX } from "preact";
11
12
 
12
13
  function ImageEnlarge(): JSX.Element | null {
@@ -16,6 +17,8 @@ ImageEnlarge.displayName = "ImageEnlarge";
16
17
 
17
18
  export default ImageEnlarge;
18
19
 
20
+ export { ImageEnlarge };
21
+
19
22
  export function ImageEnlargeSsrFallback(): JSX.Element | null {
20
23
  return null;
21
24
  }
@@ -1,170 +1,6 @@
1
- "use client";
2
-
3
- // Use preact hook entrypoints directly the "react" → "preact/compat" alias
4
- // lets us consume React-typed components in this Preact app (configured
5
- // project-wide). Same pattern as src/components/sidebar-tree.tsx and
6
- // packages/zudo-doc/src/theme-toggle/index.tsx. Type references via
7
- // the global React namespace still resolve via @types/react.
8
- import { useState, useEffect } from "preact/hooks";
9
- // After zudolab/zudo-doc#1335 (E2 task 2 half B) the host components
10
- // pull lifecycle event names from the v2 transitions module rather
11
- // than hard-coding `astro:*` literals.
12
- import { AFTER_NAVIGATE_EVENT } from "@takazudo/zudo-doc/transitions";
13
- import SidebarTree from "@/components/sidebar-tree";
14
- import type { NavNode } from "@/utils/docs";
15
- import type { LocaleLink } from "@/types/locale";
16
- // Types-only subpath (`./sidebar/types`) sidesteps the JSX type-graph
17
- // pulled in by `./sidebar`'s runtime barrel.
18
- import type { SidebarRootMenuItem } from "@takazudo/zudo-doc/sidebar/types";
19
-
20
- const cx = (...classes: Array<string | false | null | undefined>) =>
21
- classes.filter(Boolean).join(" ");
22
-
23
- // Mobile drawer hosts the SidebarTree directly (rather than receiving it as
24
- // JSX children) so the tree's data props ride across the SSR → hydrate
25
- // boundary inside the Island marker's `data-props` attribute. zfb's
26
- // `Island()` only serialises a child component's *own* props (excluding
27
- // `children`); when SidebarTree was passed as `children`, its data was
28
- // dropped during hydration and Preact wiped the SSR-rendered tree DOM.
29
- // Mirroring the desktop `<Sidebar treeComponent={SidebarTree} ...>` shape
30
- // keeps the data attached to the wrapping island. zudolab/zudo-doc#1355
31
- // wave 13.5.
32
-
33
- interface SidebarToggleProps {
34
- nodes: NavNode[];
35
- currentSlug?: string;
36
- rootMenuItems?: SidebarRootMenuItem[];
37
- backToMenuLabel?: string;
38
- localeLinks?: LocaleLink[];
39
- themeDefaultMode?: "light" | "dark";
40
- }
41
-
42
- export default function SidebarToggle({
43
- nodes,
44
- currentSlug,
45
- rootMenuItems,
46
- backToMenuLabel,
47
- localeLinks,
48
- themeDefaultMode,
49
- }: SidebarToggleProps) {
50
- // Initial state must match SSR (`open=false`) so the hydration DOM
51
- // matches the SSG output byte-for-byte. The backdrop and toggle-icon
52
- // are rendered unconditionally below so the hydration tree has the
53
- // same shape regardless of `open`, preventing Preact from re-mounting
54
- // the subtree (which can drop click handlers on the hamburger button).
55
- const [open, setOpen] = useState(false);
56
-
57
- useEffect(() => {
58
- if (open) {
59
- document.body.style.overflow = "hidden";
60
- } else {
61
- document.body.style.overflow = "";
62
- }
63
- return () => {
64
- document.body.style.overflow = "";
65
- };
66
- }, [open]);
67
-
68
- // Close mobile sidebar on View Transition navigation
69
- useEffect(() => {
70
- function handleSwap() {
71
- setOpen(false);
72
- }
73
- document.addEventListener(AFTER_NAVIGATE_EVENT, handleSwap);
74
- return () => document.removeEventListener(AFTER_NAVIGATE_EVENT, handleSwap);
75
- }, []);
76
-
77
- return (
78
- <>
79
- {/* Hamburger button - visible only on mobile.
80
- Both icons are always rendered so the SSR output has the same
81
- DOM shape as the post-hydration tree. The closed-state icon is
82
- hidden via `hidden` when open=true, and vice versa, so Preact's
83
- hydration walk sees byte-stable markup and keeps the click
84
- handler attached. */}
85
- <button
86
- type="button"
87
- onClick={() => setOpen(!open)}
88
- className="lg:hidden px-hsp-sm py-vsp-xs -ml-hsp-sm mr-hsp-sm text-muted hover:text-fg"
89
- aria-label={open ? "Close sidebar" : "Open sidebar"}
90
- aria-expanded={open}
91
- >
92
- {/* X icon — visible only when open */}
93
- <svg
94
- xmlns="http://www.w3.org/2000/svg"
95
- className={cx("h-icon-lg w-icon-lg", !open && "hidden")}
96
- aria-hidden="true"
97
- fill="none"
98
- viewBox="0 0 24 24"
99
- stroke="currentColor"
100
- strokeWidth={2}
101
- >
102
- <path
103
- strokeLinecap="round"
104
- strokeLinejoin="round"
105
- d="M6 18L18 6M6 6l12 12"
106
- />
107
- </svg>
108
- {/* Hamburger icon — visible only when closed */}
109
- <svg
110
- xmlns="http://www.w3.org/2000/svg"
111
- className={cx("h-icon-lg w-icon-lg", open && "hidden")}
112
- aria-hidden="true"
113
- fill="none"
114
- viewBox="0 0 24 24"
115
- stroke="currentColor"
116
- strokeWidth={2}
117
- >
118
- <path
119
- strokeLinecap="round"
120
- strokeLinejoin="round"
121
- d="M4 6h16M4 12h16M4 18h16"
122
- />
123
- </svg>
124
- </button>
125
-
126
- {/* Backdrop overlay - mobile only.
127
- Rendered unconditionally; CSS `hidden` toggles visibility so
128
- the SSR DOM tree matches the hydrated tree (no subtree
129
- mount/unmount across the hydration boundary).
130
- `z-modal-backdrop` (50) intentionally sits ABOVE the header
131
- (`z-toolbar`, 20): the open mobile drawer is a modal surface that
132
- dims the whole viewport, header included. Closing is via tapping the
133
- backdrop (onClick below), so the header hamburger being dimmed under
134
- it is fine. */}
135
- <div
136
- className={cx("fixed inset-0 z-modal-backdrop bg-overlay/30 lg:hidden", !open && "hidden")}
137
- aria-hidden={!open}
138
- onClick={() => setOpen(false)}
139
- />
140
-
141
- {/* Sidebar panel - mobile only (desktop sidebar is in doc-layout).
142
- `inert` when closed: the panel is only visually hidden via
143
- `-translate-x-full`, so without `inert` its links/filter/buttons
144
- stay in the tab order and accessibility tree while off-screen
145
- (zudolab/zudo-doc#2059). `inert={false}` serialises to no attribute,
146
- so the SSR (open=false → `inert`) and initial client render stay
147
- byte-stable for hydration. */}
148
- <aside
149
- inert={!open}
150
- className={`
151
- fixed top-[3.5rem] left-0 z-modal h-[calc(100vh-3.5rem)] w-[16rem] flex flex-col
152
- border-r border-muted bg-bg transition-transform duration-200
153
- lg:hidden
154
- ${open ? "translate-x-0" : "-translate-x-full"}
155
- `}
156
- >
157
- <div className="flex-1 overflow-y-auto">
158
- <SidebarTree
159
- nodes={nodes}
160
- currentSlug={currentSlug}
161
- rootMenuItems={rootMenuItems}
162
- backToMenuLabel={backToMenuLabel}
163
- localeLinks={localeLinks}
164
- themeDefaultMode={themeDefaultMode}
165
- />
166
- </div>
167
- </aside>
168
- </>
169
- );
170
- }
1
+ // Thin re-export shim. SidebarToggle moved into the package
2
+ // (`@takazudo/zudo-doc/sidebar-toggle-island`) as part of the package-first
3
+ // migration (#2344, S2). Host code using `@/components/sidebar-toggle`
4
+ // still resolves correctly; the implementation lives in the package.
5
+ export { SidebarToggle } from "@takazudo/zudo-doc/sidebar-toggle-island";
6
+ export type { SidebarToggleProps } from "@takazudo/zudo-doc/sidebar-toggle-island";