create-zudo-doc 0.2.20 → 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 (74) 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 +22 -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 -47
  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/tauri/files/src-tauri/capabilities/default.json +1 -4
  65. package/templates/features/tauri/files/src-tauri/tauri.conf.json +1 -1
  66. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +1 -1
  67. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  68. package/templates/base/plugins/connect-adapter.mjs +0 -169
  69. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  70. package/templates/base/scripts/gen-z-index.mjs +0 -157
  71. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  72. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  73. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  74. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,147 +1,22 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // og:title / og:description / color-scheme head injection for the zfb doc pages.
4
- //
5
- // Why this wrapper exists: the v2 `<DocLayout>` shell exposes a `head` slot
6
- // but intentionally does NOT emit og:* meta or mount `<ColorSchemeProvider>` —
7
- // that is the host's responsibility.
8
- //
9
- // Without OgTags the SSG output is missing og:title / og:description,
10
- // which crawlers and link-preview tools rely on. Without ColorSchemeProvider
11
- // the runtime `:root { --zd-* }` palette is missing, so every component that
12
- // resolves a color via `--zd-*` (search match-keyword highlight, image-overlay,
13
- // etc.) falls back to UA defaults — and the smoke-search "matched
14
- // keywords" regression guard at e2e/smoke-search.spec.ts:167 fires
15
- // because `getComputedStyle(root).getPropertyValue("--zd-matched-keyword-bg")`
16
- // returns "" instead of the resolved palette token.
17
- //
18
- // (#1355 wave 13 — ColorSchemeProvider mount restored after the retirement of
19
- // the zfb cutover's initial setup.)
20
-
21
- import type { JSX } from "preact";
22
- import { OgTags, TwitterCard } from "@takazudo/zudo-doc/head";
23
- import { SIDEBAR_RESIZER_RESTORE_SCRIPT } from "@takazudo/zudo-doc/sidebar-resizer";
24
- // Import ColorSchemeProvider from the dedicated
25
- // `./theme/color-scheme-provider` subpath rather than the
26
- // "@takazudo/zudo-doc/theme" barrel — the barrel also re-exports the
27
- // ColorTweakExportModal island and the design-token SerDe/iframe-bridge
28
- // modules, which this SSR-only head emission does not need in its zfb
29
- // esbuild graph.
30
- import ColorSchemeProvider from "@takazudo/zudo-doc/theme/color-scheme-provider";
31
- import { composeMetaTitle } from "./_compose-meta-title";
32
- import { withBase, absoluteUrl } from "@/utils/base";
3
+ // Host thin-stub see @takazudo/zudo-doc/head-with-defaults (epic #2344, S5).
33
4
  import { settings } from "@/config/settings";
34
- // W3B (#1730): cssText + colorMode are precomputed here — the v2
35
- // ColorSchemeProvider no longer reaches into the host config tree.
5
+ import { withBase, absoluteUrl } from "@/utils/base";
36
6
  import {
37
7
  generateCssCustomProperties,
38
8
  generateLightDarkCssProperties,
39
9
  } from "@/config/color-scheme-utils";
10
+ import { composeMetaTitle } from "./_compose-meta-title";
11
+ import { createHeadWithDefaults } from "@takazudo/zudo-doc/head-with-defaults";
40
12
 
41
- export interface HeadWithDefaultsProps {
42
- /** Page title forwarded to og:title. Required. */
43
- title: string;
44
- /** Optional page description forwarded to og:description. */
45
- description?: string;
46
- /**
47
- * Absolute canonical URL for this page. When supplied, emits
48
- * <link rel="canonical" href="...">. Compute via `absoluteUrl(pageUrl)`
49
- * (@/utils/base) in each host page; it returns undefined when
50
- * settings.siteUrl is empty so the link is simply omitted.
51
- */
52
- canonical?: string;
53
- }
54
-
55
- /**
56
- * Default-bearing host wrapper that injects og:title / og:description,
57
- * the ColorSchemeProvider (`:root { --zd-* }` palette + theme bootstrap),
58
- * the favicon link, and an optional canonical link into the v2 layout's
59
- * `head` slot.
60
- *
61
- * og:title is run through composeMetaTitle so it matches the
62
- * "<title> | <siteName>" shape emitted by the host's <title> element
63
- * (the legacy Astro layout produced both shapes; the zfb host has to
64
- * compose them itself).
65
- *
66
- * og:title is always emitted — it is the unconditional DocHead contract
67
- * (OgTags always emits og:title regardless of settings). All other tags
68
- * are gated by settings.metaTags.
69
- *
70
- * Pure SSR — no state, no client-only imports. Intended for use as:
71
- * head={<HeadWithDefaults title={title} description={description} canonical={canonical} />}
72
- * on every DocLayoutWithDefaults call site in the host pages.
73
- */
74
- export function HeadWithDefaults({
75
- title,
76
- description,
77
- canonical,
78
- }: HeadWithDefaultsProps): JSX.Element {
79
- const { metaTags } = settings;
80
-
81
- // og:image / twitter:image must be absolute URLs — crawlers silently drop
82
- // relative og:image values. absoluteUrl joins siteUrl (no trailing slash) +
83
- // the base-prefixed asset path, and returns undefined when siteUrl is empty
84
- // (e.g. a freshly scaffolded create-zudo-doc project that hasn't configured
85
- // siteUrl yet) so we never ship a useless relative og:image. OgTags /
86
- // TwitterCard already gate their image emission on the prop being defined;
87
- // the og:image:* companion tags below are gated explicitly because they
88
- // would dangle without the parent og:image.
89
- const ogImageUrl =
90
- metaTags.ogImage !== false
91
- ? absoluteUrl(withBase(metaTags.ogImage))
92
- : undefined;
93
-
94
- // Resolve the palette CSS body once per page render (the v2 component
95
- // is pure SSR — no caching needed).
96
- const colorMode = settings.colorMode ? settings.colorMode : null;
97
- const cssText = colorMode
98
- ? generateLightDarkCssProperties()
99
- : generateCssCustomProperties();
13
+ export type { HeadWithDefaultsProps } from "@takazudo/zudo-doc/head-with-defaults";
100
14
 
101
- return (
102
- <>
103
- <OgTags
104
- title={composeMetaTitle(title)}
105
- description={metaTags.description ? description : undefined}
106
- ogType="website"
107
- ogUrl={canonical}
108
- ogImage={ogImageUrl}
109
- ogSiteName={metaTags.ogSiteName ? settings.siteName : undefined}
110
- />
111
- {metaTags.keywords !== false && metaTags.keywords.length > 0 && (
112
- <meta name="keywords" content={metaTags.keywords} />
113
- )}
114
- {/* og:image:width / og:image:height / og:image:alt — not in OgTags API;
115
- emitted here directly to avoid expanding the shared HeadProps surface.
116
- Standard 1200×630 social preview dimensions. Gated on ogImageUrl so
117
- the companion tags don't dangle when og:image itself was suppressed. */}
118
- {ogImageUrl !== undefined && (
119
- <>
120
- <meta property="og:image:width" content="1200" />
121
- <meta property="og:image:height" content="630" />
122
- <meta property="og:image:alt" content={composeMetaTitle(title)} />
123
- </>
124
- )}
125
- {metaTags.twitterCard !== false && (
126
- <TwitterCard
127
- card={metaTags.twitterCard}
128
- image={ogImageUrl}
129
- site={metaTags.twitterSite}
130
- creator={metaTags.twitterCreator}
131
- />
132
- )}
133
- <ColorSchemeProvider cssText={cssText} colorMode={colorMode} />
134
- {/* Pre-paint inline script: restore persisted sidebar width to
135
- --zd-sidebar-w on :root before first paint, so a reload after
136
- drag-resizing the sidebar doesn't snap back to the CSS default
137
- clamp() width. Mirrors the sibling sidebar-toggle restore
138
- script emitted from the page's afterSidebar slot. */}
139
- {settings.sidebarResizer && <script dangerouslySetInnerHTML={{ __html: SIDEBAR_RESIZER_RESTORE_SCRIPT }} />}
140
- {/* favicon set — withBase() handles the configured base path prefix */}
141
- <link rel="icon" href={withBase("/favicon.ico")} sizes="any" />
142
- <link rel="icon" type="image/png" sizes="32x32" href={withBase("/favicon-32x32.png")} />
143
- <link rel="icon" type="image/png" sizes="16x16" href={withBase("/favicon-16x16.png")} />
144
- {canonical !== undefined && <link rel="canonical" href={canonical} />}
145
- </>
146
- );
147
- }
15
+ export const HeadWithDefaults = createHeadWithDefaults({
16
+ settings,
17
+ composeMetaTitle,
18
+ withBase,
19
+ absoluteUrl,
20
+ generateCssCustomProperties,
21
+ generateLightDarkCssProperties,
22
+ });
@@ -1,57 +1,6 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // Locale-/version-aware Header wrapper for the zfb doc pages.
4
- //
5
- // Header data-prep utilities — builds the resolved header config from user
6
- // settings + page context: header nav, active-path state, mobile SidebarToggle
7
- // island (hamburger + slide-in panel) with the full sidebar tree for doc routes,
8
- // and feeds everything into the v2 <Header> shell.
9
- //
10
- // Why this wrapper exists: the v2 Header shell is intentionally
11
- // framework-agnostic — it accepts slot props (sidebarToggle, themeToggle,
12
- // etc.) but does not import host helpers (@/config/*, @/utils/*) so the
13
- // package can be published independently. The data prep stays on the host
14
- // side. Without this wrapper the zfb doc pages fall through to the
15
- // DocLayoutWithDefaults minimal default (a bare <header> with only
16
- // <ThemeToggle>) — the full logo + nav + mobile-sidebar markup is absent
17
- // from the SSG output, breaking crawlers, screen readers, and no-JS users.
18
- //
19
- // Mobile sidebar strategy:
20
- // - The v2 <Header> accepts a `sidebarToggle` slot that holds the
21
- // complete mobile sidebar widget: hamburger button + backdrop overlay +
22
- // slide-in <aside> panel (all rendered by <SidebarToggle>).
23
- // - This wrapper ALWAYS builds the sidebarToggle (refs #1453:
24
- // SidebarToggle is rendered unconditionally on every page; the host CSS
25
- // hides it on pages with hide_sidebar). When `navSection` is defined the
26
- // panel gets the full section tree; when undefined (home, 404, tags,
27
- // versions) nodes=[] so the panel shows only rootMenuItems.
28
- // - The bare package ThemeToggle (wrapped in Island below) is always passed
29
- // to Header.themeToggle so the ThemeToggle island marker appears in the
30
- // header on every page — matching the documented header contract.
31
- //
32
- // Locale switcher strategy (refs #1453):
33
- // - This wrapper always renders <LanguageSwitcher /> in the right-items row
34
- // when multiple locales are configured. Builds locale links from
35
- // buildLocaleLinks() and passes a <LanguageSwitcher> as the
36
- // languageSwitcher slot prop.
37
- // - Header only renders the slot when settings.locales has > 1 entry, so
38
- // single-locale projects are unaffected.
39
-
40
- import type { VNode, JSX } from "preact";
41
- import { Island } from "@takazudo/zfb";
42
- import { Header } from "@takazudo/zudo-doc/header";
43
- import {
44
- LanguageSwitcher,
45
- VersionSwitcher,
46
- type VersionSwitcherLabels,
47
- } from "@takazudo/zudo-doc/i18n-version";
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";
54
- import SidebarToggle from "@/components/sidebar-toggle";
3
+ // Host thin-stub see @takazudo/zudo-doc/header-with-defaults (epic #2344, S5).
55
4
  import { settings } from "@/config/settings";
56
5
  import { defaultLocale, locales, t, type Locale } from "@/config/i18n";
57
6
  import { buildGitHubRepoUrl } from "@/utils/github";
@@ -62,7 +11,7 @@ import {
62
11
  versionedDocsUrl,
63
12
  withBase,
64
13
  } from "@/utils/base";
65
- import { filterHeaderRightItems } from "@takazudo/zudo-doc/header";
14
+ import { createHeaderWithDefaults } from "@takazudo/zudo-doc/header-with-defaults";
66
15
  import { SearchWidget } from "./_search-widget";
67
16
  import {
68
17
  buildRootMenuItems,
@@ -71,275 +20,26 @@ import {
71
20
  getThemeDefaultMode,
72
21
  } from "./_nav-data-prep";
73
22
 
74
- // ---------------------------------------------------------------------------
75
- // Component
76
- // ---------------------------------------------------------------------------
77
-
78
- export interface HeaderWithDefaultsProps {
79
- /** Active locale; defaults to the configured defaultLocale. */
80
- lang?: Locale | string;
81
- /**
82
- * Current page URL path (as the layout passes from Astro.url.pathname or
83
- * the zfb equivalent). Used by the Header to compute the active nav item
84
- * and by the mobile sidebar footer locale-switcher links.
85
- */
86
- currentPath?: string;
87
- /** Active version slug, when rendering inside /v/{version}/... routes. */
88
- currentVersion?: string;
89
- /**
90
- * Slug of the active doc page — forwarded to SidebarTree so it can
91
- * highlight the current entry. Required when navSection is set.
92
- */
93
- currentSlug?: string;
94
- /**
95
- * Header-nav category matcher used to scope the sidebar tree (e.g.
96
- * "guides"). When provided the mobile sidebar toggle is wired with the
97
- * full sidebar tree for this section. When omitted (404, index, tags,
98
- * versions pages) no mobile sidebar toggle is included in the header.
99
- */
100
- navSection?: string;
101
- }
102
-
103
- /**
104
- * Default-bearing host wrapper around v2's <Header> shell.
105
- *
106
- * Handles:
107
- * 1. Logo, main nav with active-path highlight — delegated to <Header>.
108
- * 2. ThemeToggle island in the right items row — passed as themeToggle slot.
109
- * 3. Mobile SidebarToggle island (hamburger + slide-in aside + tree) —
110
- * built from the same nav data as SidebarWithDefaults and passed as
111
- * the sidebarToggle slot when navSection is defined.
112
- */
113
- export function HeaderWithDefaults(
114
- props: HeaderWithDefaultsProps,
115
- ): JSX.Element {
116
- const {
117
- lang: langProp = defaultLocale,
118
- currentPath = "",
119
- currentVersion,
120
- currentSlug,
121
- navSection,
122
- } = props;
123
- // Route params arrive as `string`; cast to Locale since keys of settings.locales
124
- // are always valid locale codes. The prop accepts `Locale | string` so callers
125
- // without a Locale variable don't need to cast (e.g. _tag-pages.tsx).
126
- const lang = langProp as Locale;
127
-
128
- // Root-menu items, locale links, sidebar nodes, and theme mode — all
129
- // delegated to the shared _nav-data-prep helpers so header and sidebar
130
- // wrappers stay in sync without duplicating the logic.
131
- const rootMenuItems = buildRootMenuItems(lang, currentVersion);
132
-
133
- // Build the mobile sidebar toggle unconditionally — SidebarToggle is rendered
134
- // on every page (refs #1453); the host CSS hides it where unneeded. When navSection is
135
- // defined the panel gets the full section tree; when undefined (home, 404,
136
- // tags, versions) nodes=[] so the panel shows only rootMenuItems + locale
137
- // links (the basic nav menu without a doc tree).
138
- const backToMenuLabel = t("nav.backToMenu", lang);
139
-
140
- // Locale-switcher links in the mobile sidebar footer — only when
141
- // multiple locales are configured (mirrors _sidebar-with-defaults.tsx).
142
- const localeLinks = buildLocaleLinksForNav(currentPath, lang, locales.length);
143
-
144
- const themeDefaultMode = getThemeDefaultMode();
145
-
146
- const sidebarNodes = buildSidebarNodes(lang, navSection, currentVersion);
147
-
148
- // Wrap SidebarToggle (hamburger button + slide-in aside + SidebarTree) in
149
- // Island so the SSG output carries the full tree HTML AND the
150
- // data-zfb-island="SidebarToggle" marker for client-side hydration.
151
- // Island.captureComponentName reads SidebarToggle.name → "SidebarToggle".
152
- //
153
- // SidebarTree's data props (nodes, currentSlug, rootMenuItems, …) are
154
- // passed to SidebarToggle directly rather than as JSX children so they
155
- // ride across the SSR → hydrate boundary in the Island marker's
156
- // `data-props` attribute. Island only serialises a wrapped child
157
- // component's *own* props (excluding `children`); when SidebarTree was
158
- // nested as a JSX child its data was dropped during hydration and
159
- // SidebarToggle re-rendered with `children=undefined`, wiping the SSR
160
- // tree DOM. zudolab/zudo-doc#1355 wave 13.5.
161
- //
162
- // C4 — media-gated hydration. zfb only supports load|idle|visible
163
- // strategies (no "media" strategy; matchMedia inside the component is too
164
- // late — props are already emitted, bundle already downloaded).
165
- // Upstream feature request: Takazudo/zudo-front-builder#969.
166
- //
167
- // Best achievable downstream: when="visible" + all SidebarToggle children
168
- // are lg:hidden, so on desktop the Island wrapper div has zero rendered
169
- // dimensions. IntersectionObserver fires isIntersecting=false on desktop
170
- // (zero-size element) → Preact hydrate() is never called. On mobile (and
171
- // on desktop→mobile resize) the children become visible, the element gains
172
- // size, IO fires isIntersecting=true, and hydration completes normally.
173
- //
174
- // Residual: data-props JSON (~2.4 KB) is still emitted in the SSR HTML on
175
- // every page regardless of viewport, because it is serialised at build time
176
- // and not gated by media. Eliminating it requires a zfb "media" hydration
177
- // strategy — tracked in Takazudo/zudo-front-builder#969.
178
- const sidebarToggle = Island({
179
- when: "visible",
180
- children: (
181
- <SidebarToggle
182
- nodes={sidebarNodes}
183
- currentSlug={currentSlug}
184
- rootMenuItems={rootMenuItems}
185
- backToMenuLabel={backToMenuLabel}
186
- localeLinks={localeLinks}
187
- themeDefaultMode={themeDefaultMode}
188
- />
189
- ),
190
- }) as unknown as VNode;
191
-
192
- // Wrap the bare ThemeToggle in Island({when:"load"}) so the SSG
193
- // output emits a data-zfb-island="ThemeToggle" marker the hydration
194
- // runtime can find — matching the documented header contract.
195
- const themeToggle = Island({
196
- when: "load",
197
- children: <ThemeToggle defaultMode={themeDefaultMode} />,
198
- }) as unknown as VNode;
199
-
200
- // Locale-aware search widget. Renders the full dialog markup in SSR
201
- // so the placeholder text ("Type to search..." / 「検索したい単語を入力」)
202
- // and keyboard-shortcut hint appear in the static HTML on every page.
203
- // Strings are derived from the host's t() helper so locale switching works.
204
- const searchWidget = (
205
- <SearchWidget
206
- placeholderText={t("search.placeholder", lang)}
207
- shortcutHint={t("search.shortcutHint", lang)}
208
- resultCountTemplate={t("search.resultCount", lang)}
209
- searchLabel={t("search.label", lang)}
210
- />
211
- );
23
+ export type { HeaderWithDefaultsProps } from "@takazudo/zudo-doc/header-with-defaults";
212
24
 
213
- // Build the version-switcher component when versioning is configured.
214
- // The VersionSwitcher is a pure SSR component — it emits the full dropdown
215
- // markup (including the "All versions" footer link) directly in the SSG
216
- // HTML so crawlers and JS-off users see the version list. The interactive
217
- // toggle behavior is wired by VERSION_SWITCHER_INIT_SCRIPT included in
218
- // the layout's body-end scripts.
219
- //
220
- // Gate: only render when settings.versions is a non-empty array. When
221
- // versioning is disabled (settings.versions === false) the slot is
222
- // undefined and the Header renders nothing for the version-switcher item.
223
- let versionSwitcher: VNode | undefined;
224
-
225
- if (settings.versions && settings.versions.length > 0) {
226
- const isNonDefaultLocale = lang !== defaultLocale;
227
- // "All versions" page URL — locale-prefixed when not on the default locale.
228
- const versionsPageUrl = withBase(
229
- isNonDefaultLocale ? `/${lang}/docs/versions` : "/docs/versions",
230
- );
231
- // "Latest" entry links to the current page in the latest (unversioned)
232
- // docs when a slug is available, or falls back to the versions index page.
233
- // Null check, not truthiness: "" is the canonical root-index slug (#1891)
234
- // and must produce real per-version root URLs.
235
- const latestUrl = currentSlug != null
236
- ? docsUrl(currentSlug, lang)
237
- : versionsPageUrl;
238
-
239
- // Per-version URLs for the current page. When there is no slug in scope
240
- // (e.g. on the versions page itself) all entries point to the versions
241
- // index — matching the documented version-switcher contract.
242
- const versionUrls: Record<string, string> = {};
243
- for (const v of settings.versions) {
244
- versionUrls[v.slug] = currentSlug != null
245
- ? versionedDocsUrl(currentSlug, v.slug, lang)
246
- : versionsPageUrl;
247
- }
248
-
249
- const labels: VersionSwitcherLabels = {
250
- latest: t("version.latest", lang),
251
- switcher: t("version.switcher.label", lang),
252
- unavailable: t("version.switcher.unavailable", lang),
253
- allVersions: t("version.switcher.allVersions", lang),
254
- };
255
-
256
- versionSwitcher = (
257
- <VersionSwitcher
258
- versions={settings.versions.map((v) => ({
259
- slug: v.slug,
260
- label: v.label ?? v.slug,
261
- }))}
262
- currentVersion={currentVersion}
263
- latestUrl={latestUrl}
264
- versionsPageUrl={versionsPageUrl}
265
- versionUrls={versionUrls}
266
- labels={labels}
267
- idSuffix="header"
268
- />
269
- ) as unknown as VNode;
270
- }
271
-
272
- // Build locale-switcher for the header right-items row (refs #1453).
273
- // Renders <LanguageSwitcher /> when multiple locales are configured.
274
- // Reuses the same localeLinks array built above for the mobile sidebar footer
275
- // (buildLocaleLinks is pure, but one call is cleaner).
276
- const languageSwitcher =
277
- localeLinks != null ? (
278
- <LanguageSwitcher
279
- links={localeLinks}
280
- />
281
- ) as unknown as VNode : undefined;
282
-
283
- // Locale-keyed persist key: same-locale swaps preserve the header's
284
- // DOM-node identity; cross-locale swaps use a different key and the
285
- // router replaces the header entirely (re-rendering locale-specific
286
- // SSR content such as the LanguageSwitcher anchors). See #1546 + #1549.
287
- const persistKey = `header-${lang}`;
288
-
289
- // Compute the right-items flags from the host's settings. The v2
290
- // `<Header>` no longer consults `@/config/settings` directly — see
291
- // sub-issue #1729 — so the wrapper is responsible for translating
292
- // host state into the prop bag the renderer expects. Boolean
293
- // coercion mirrors the original filter predicates verbatim.
294
- const headerRightItems = filterHeaderRightItems(
295
- settings.headerRightItems ?? [],
296
- {
297
- designTokenPanel: Boolean(settings.designTokenPanel),
298
- aiAssistant: Boolean(settings.aiAssistant),
299
- colorMode: Boolean(settings.colorMode),
300
- hasLocales: Object.keys(settings.locales).length > 0,
301
- hasVersions: Boolean(settings.versions),
302
- hasGithubUrl: Boolean(settings.githubUrl),
303
- },
304
- );
305
-
306
- const githubRepoUrl = buildGitHubRepoUrl();
307
- const githubLabel = t("header.github", lang);
308
-
309
- return (
310
- <Header
311
- lang={lang}
312
- currentPath={currentPath}
313
- currentVersion={currentVersion}
314
- activeCategory={navSection}
315
- sidebarToggle={sidebarToggle}
316
- themeToggle={themeToggle}
317
- search={searchWidget}
318
- versionSwitcher={versionSwitcher}
319
- languageSwitcher={languageSwitcher}
320
- persistKey={persistKey}
321
- siteName={settings.siteName}
322
- headerNav={settings.headerNav}
323
- headerRightItems={headerRightItems}
324
- colorModeEnabled={Boolean(settings.colorMode)}
325
- hasLocales={locales.length > 1}
326
- hasVersions={Boolean(settings.versions)}
327
- githubRepoUrl={githubRepoUrl}
328
- githubLabel={githubLabel}
329
- urlHelpers={{
330
- withBase,
331
- stripBase,
332
- // `navHref` from `@/utils/base` types the lang param as the
333
- // host's literal-locale union; v2's `Locale` is `string`. Wrap
334
- // so strictFunctionTypes accepts the assignment without losing
335
- // the runtime call shape (sub-issue #1729 boundary widening).
336
- navHref: (path, l, v) => navHref(path, l as Locale | undefined, v),
337
- }}
338
- i18n={{
339
- defaultLocale,
340
- locales,
341
- t,
342
- }}
343
- />
344
- );
345
- }
25
+ export const HeaderWithDefaults = createHeaderWithDefaults({
26
+ settings,
27
+ defaultLocale,
28
+ locales,
29
+ t: (key, lang) => t(key as Parameters<typeof t>[0], lang as Locale),
30
+ withBase,
31
+ stripBase,
32
+ docsUrl: (slug, lang) => docsUrl(slug, lang as Locale),
33
+ navHref: (path, lang, version) => navHref(path, lang as Locale | undefined, version),
34
+ versionedDocsUrl: (slug, versionSlug, lang) =>
35
+ versionedDocsUrl(slug, versionSlug, lang as Locale),
36
+ buildLocaleLinksForNav: (currentPath, lang, localeCount) =>
37
+ buildLocaleLinksForNav(currentPath, lang as Locale, localeCount),
38
+ buildRootMenuItems: (lang, currentVersion) =>
39
+ buildRootMenuItems(lang as Locale, currentVersion),
40
+ buildSidebarNodes: (lang, navSection, currentVersion) =>
41
+ buildSidebarNodes(lang as Locale, navSection, currentVersion),
42
+ getThemeDefaultMode,
43
+ buildGitHubRepoUrl,
44
+ SearchWidget,
45
+ });
@@ -1,85 +1,23 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // Inline VersionSwitcher for the breadcrumb rightSlot on doc pages.
4
- //
5
- // The reference site (takazudomodular.com) renders the version-switcher
6
- // pill inline at the right of the breadcrumb row on every doc page:
7
- //
8
- // <div class="mb-vsp-sm flex ... sm:justify-between [&_nav]:mb-0">
9
- // <breadcrumb nav>
10
- // <VersionSwitcher ... />
11
- // </div>
12
- //
13
- // (Wave 2 parity sweep, zudolab/zudo-doc#1478; placement fix #1534.)
14
- // The header already shows a VersionSwitcher via _header-with-defaults.tsx;
15
- // this helper provides the pill for the breadcrumb's `rightSlot` prop so
16
- // both the DOM position AND the flex-row layout match the reference.
17
- //
18
- // Gate: only returns a non-null element when settings.versions is a
19
- // non-empty array. When versioning is disabled the caller gets undefined
20
- // and <Breadcrumb> renders without a rightSlot (no wrapper div emitted).
3
+ // Thin stub inline-version-switcher moved to the package (epic #2344, S8).
4
+ // Calls `createInlineVersionSwitcher(deps)` from
5
+ // @takazudo/zudo-doc/inline-version-switcher with host singletons injected,
6
+ // then re-exports the resulting builder so all existing call sites continue
7
+ // to work unchanged.
21
8
 
22
- import type { JSX } from "preact";
23
- import {
24
- VersionSwitcher,
25
- type VersionSwitcherLabels,
26
- } from "@takazudo/zudo-doc/i18n-version";
9
+ import { createInlineVersionSwitcher } from "@takazudo/zudo-doc/inline-version-switcher";
27
10
  import { settings } from "@/config/settings";
28
- import { defaultLocale, t, type Locale } from "@/config/i18n";
11
+ import { defaultLocale, t } from "@/config/i18n";
29
12
  import { docsUrl, versionedDocsUrl, withBase } from "@/utils/base";
30
13
 
31
- /**
32
- * Build an inline VersionSwitcher element for the `<Breadcrumb rightSlot>` prop.
33
- *
34
- * Returns `undefined` when versioning is not configured (callers can pass
35
- * the return value directly as `rightSlot` — `<Breadcrumb>` renders the
36
- * plain nav (no flex wrapper, no version pill) when `rightSlot` is
37
- * `undefined`).
38
- *
39
- * @param slug - Slug of the current doc page (used to build per-version URLs).
40
- * @param locale - Active locale string.
41
- * @param currentVersion - Active version slug, or `undefined` on the "latest" route.
42
- */
43
- export function buildInlineVersionSwitcher(
44
- slug: string,
45
- locale: Locale,
46
- currentVersion?: string,
47
- ): JSX.Element | undefined {
48
- if (!settings.versions || settings.versions.length === 0) return undefined;
14
+ export { type InlineVersionSwitcherVersionEntry } from "@takazudo/zudo-doc/inline-version-switcher";
49
15
 
50
- const isNonDefaultLocale = locale !== defaultLocale;
51
- const versionsPageUrl = withBase(
52
- isNonDefaultLocale ? `/${locale}/docs/versions` : "/docs/versions",
53
- );
54
- // The docs root index has the canonical empty slug "" (#1891), and
55
- // docsUrl("")/versionedDocsUrl("") resolve to the per-version docs roots —
56
- // so an empty slug must NOT fall back to the versions page.
57
- const latestUrl = docsUrl(slug, locale);
58
-
59
- const versionUrls: Record<string, string> = {};
60
- for (const v of settings.versions) {
61
- versionUrls[v.slug] = versionedDocsUrl(slug, v.slug, locale);
62
- }
63
-
64
- const labels: VersionSwitcherLabels = {
65
- latest: t("version.latest", locale),
66
- switcher: t("version.switcher.label", locale),
67
- unavailable: t("version.switcher.unavailable", locale),
68
- allVersions: t("version.switcher.allVersions", locale),
69
- };
70
-
71
- return (
72
- <VersionSwitcher
73
- versions={settings.versions.map((v) => ({
74
- slug: v.slug,
75
- label: v.label ?? v.slug,
76
- }))}
77
- currentVersion={currentVersion}
78
- latestUrl={latestUrl}
79
- versionsPageUrl={versionsPageUrl}
80
- versionUrls={versionUrls}
81
- labels={labels}
82
- idSuffix="inline"
83
- />
84
- );
85
- }
16
+ export const buildInlineVersionSwitcher = createInlineVersionSwitcher({
17
+ settings,
18
+ defaultLocale,
19
+ t,
20
+ docsUrl,
21
+ versionedDocsUrl,
22
+ withBase,
23
+ });