@takazudo/zudo-doc 1.0.2 → 1.2.0

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.
@@ -0,0 +1,32 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { JSX } from "preact";
4
+ /** The `settings` subset this factory reads — the three package-island flags.
5
+ * A structural subset so the host can pass its full `Settings` object. */
6
+ export interface BodyEndIslandsSettings {
7
+ aiAssistant: boolean;
8
+ imageEnlarge: boolean;
9
+ mermaid: boolean;
10
+ }
11
+ /** Dependencies injected by `_chrome.tsx` (carries the virtual-module settings). */
12
+ export interface BodyEndIslandsDeps {
13
+ settings: BodyEndIslandsSettings;
14
+ }
15
+ /** Props for the produced `BodyEndIslands` component. */
16
+ export interface BodyEndIslandsProps {
17
+ /** Base path the AI chat modal uses to construct API URLs. */
18
+ basePath: string;
19
+ /**
20
+ * Sr-only label rendered as the AiChatModal SSR fallback. Defaults to the
21
+ * English string; pass a locale-translated string for non-default locales so
22
+ * screen readers announce the chat entrypoint correctly before hydration.
23
+ */
24
+ aiChatBodyLabel?: string;
25
+ }
26
+ /**
27
+ * Build the package-default `BodyEndIslands` component bound to the host's
28
+ * serializable `settings` flags. The produced component matches the
29
+ * `createDocBodyEnd` `BodyEndIslands` slot contract
30
+ * (`(props: { basePath: string }) => JSX.Element`).
31
+ */
32
+ export declare function createBodyEndIslands(deps: BodyEndIslandsDeps): (props: BodyEndIslandsProps) => JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
2
+ import { Island } from "@takazudo/zfb";
3
+ import { AiChatModal } from "../ai-chat-modal/index.js";
4
+ import { ImageEnlarge, ImageEnlargeSsrFallback } from "../image-enlarge/index.js";
5
+ import { MermaidEnlarge, MermaidEnlargeSsrFallback } from "../mermaid-enlarge/index.js";
6
+ const DEFAULT_AI_CHAT_BODY_LABEL = "Ask a question about the documentation.";
7
+ function createBodyEndIslands(deps) {
8
+ const { settings } = deps;
9
+ function BodyEndIslands({
10
+ basePath,
11
+ aiChatBodyLabel = DEFAULT_AI_CHAT_BODY_LABEL
12
+ }) {
13
+ const aiAssistant = settings.aiAssistant ? /* @__PURE__ */ jsxs(Fragment, { children: [
14
+ /* @__PURE__ */ jsx("h2", { class: "sr-only", children: "AI Assistant" }),
15
+ Island({
16
+ ssrFallback: /* @__PURE__ */ jsx("p", { class: "sr-only", children: aiChatBodyLabel }),
17
+ children: /* @__PURE__ */ jsx(AiChatModal, { basePath })
18
+ })
19
+ ] }) : null;
20
+ const imageEnlarge = settings.imageEnlarge ? Island({
21
+ when: "idle",
22
+ ssrFallback: /* @__PURE__ */ jsx(ImageEnlargeSsrFallback, {}),
23
+ children: /* @__PURE__ */ jsx(ImageEnlarge, {})
24
+ }) : null;
25
+ const mermaidEnlarge = settings.mermaid ? Island({
26
+ when: "idle",
27
+ ssrFallback: /* @__PURE__ */ jsx(MermaidEnlargeSsrFallback, {}),
28
+ children: /* @__PURE__ */ jsx(MermaidEnlarge, {})
29
+ }) : null;
30
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
31
+ aiAssistant,
32
+ imageEnlarge,
33
+ mermaidEnlarge
34
+ ] });
35
+ }
36
+ return BodyEndIslands;
37
+ }
38
+ export {
39
+ createBodyEndIslands
40
+ };
@@ -34,7 +34,10 @@ function createRenderDocPage(deps) {
34
34
  const currentPath = version ? versionedDocsUrl(slug, version.slug, locale) : docsUrl(slug, locale);
35
35
  const canonical = absoluteUrl(currentPath);
36
36
  const navSection = getNavSectionForSlug(slug);
37
- const hideSidebar = props.kind === "entry" ? props.entry.data.hide_sidebar : void 0;
37
+ const entryData = props.kind === "entry" ? props.entry.data : void 0;
38
+ const standalone = entryData?.standalone;
39
+ const hideSidebar = entryData ? entryData.hide_sidebar || standalone : void 0;
40
+ const hideToc = entryData ? entryData.hide_toc || standalone : void 0;
38
41
  const sidebarPersistKey = hideSidebar ? void 0 : `sidebar-${locale}-${navSection ?? "default"}`;
39
42
  const ContentComponent = props.kind === "entry" ? props.entry.Content : null;
40
43
  return /* @__PURE__ */ jsx(
@@ -53,7 +56,7 @@ function createRenderDocPage(deps) {
53
56
  navSection,
54
57
  sidebarPersistKey,
55
58
  hideSidebar,
56
- hideToc: props.kind === "entry" ? props.entry.data.hide_toc : void 0,
59
+ hideToc,
57
60
  currentPath,
58
61
  currentVersion: version?.slug,
59
62
  versionSwitcher: buildInlineVersionSwitcher(slug, locale, version?.slug),
@@ -49,6 +49,30 @@ const EJECTABLE = {
49
49
  details: {
50
50
  packageSubpath: "@takazudo/zudo-doc/details",
51
51
  localDir: "src/components/zudo-doc/details"
52
+ },
53
+ "sidebar-tree-island": {
54
+ packageSubpath: "@takazudo/zudo-doc/sidebar-tree-island",
55
+ localDir: "src/components/zudo-doc/sidebar-tree-island"
56
+ },
57
+ "sidebar-toggle-island": {
58
+ packageSubpath: "@takazudo/zudo-doc/sidebar-toggle-island",
59
+ localDir: "src/components/zudo-doc/sidebar-toggle-island"
60
+ },
61
+ "desktop-sidebar-toggle-island": {
62
+ packageSubpath: "@takazudo/zudo-doc/desktop-sidebar-toggle-island",
63
+ localDir: "src/components/zudo-doc/desktop-sidebar-toggle-island"
64
+ },
65
+ "image-enlarge": {
66
+ packageSubpath: "@takazudo/zudo-doc/image-enlarge",
67
+ localDir: "src/components/zudo-doc/image-enlarge"
68
+ },
69
+ "doc-history": {
70
+ packageSubpath: "@takazudo/zudo-doc/doc-history",
71
+ localDir: "src/components/zudo-doc/doc-history"
72
+ },
73
+ "site-tree-nav-island": {
74
+ packageSubpath: "@takazudo/zudo-doc/site-tree-nav-island",
75
+ localDir: "src/components/zudo-doc/site-tree-nav-island"
52
76
  }
53
77
  };
54
78
  async function rewireImports(dir) {
@@ -29,7 +29,7 @@ function DocCardGrid(props) {
29
29
  /* @__PURE__ */ jsx(ArrowIcon, {}),
30
30
  /* @__PURE__ */ jsx("span", { class: "font-medium text-accent group-hover:underline", children: item.title })
31
31
  ] }),
32
- item.description && /* @__PURE__ */ jsx("span", { class: "mt-vsp-2xs block text-small text-muted", children: item.description })
32
+ item.description && /* @__PURE__ */ jsx("span", { class: "mt-vsp-2xs block text-small text-muted group-hover:text-accent group-focus-visible:text-accent", children: item.description })
33
33
  ]
34
34
  },
35
35
  `doc-card-${i}`
@@ -32,7 +32,7 @@ function NavCardGrid(props) {
32
32
  /* @__PURE__ */ jsx(ArrowIcon, {}),
33
33
  /* @__PURE__ */ jsx("span", { class: "font-medium text-accent group-hover:underline", children: child.label })
34
34
  ] }),
35
- child.description && /* @__PURE__ */ jsx("span", { class: "mt-vsp-2xs block text-small text-muted group-hover:underline decoration-muted", children: child.description })
35
+ child.description && /* @__PURE__ */ jsx("span", { class: "mt-vsp-2xs block text-small text-muted group-hover:text-accent group-hover:underline group-focus-visible:text-accent decoration-muted", children: child.description })
36
36
  ]
37
37
  },
38
38
  `nav-card-${i}`
@@ -48,12 +48,14 @@ const plugin = definePlugin({
48
48
  const settings = options.settings ?? {};
49
49
  const translations = options.translations ?? {};
50
50
  const tagVocabulary = options.tagVocabulary ?? [];
51
+ const colorSchemes = options.colorSchemes ?? null;
51
52
  ctx.addVirtualModule(
52
53
  "virtual:zudo-doc-route-context",
53
54
  () => `export const routeContext = ${JSON.stringify({
54
55
  settings,
55
56
  translations,
56
- tagVocabulary
57
+ tagVocabulary,
58
+ colorSchemes
57
59
  })};
58
60
  `
59
61
  );
package/dist/preset.d.ts CHANGED
@@ -33,6 +33,7 @@
33
33
  * bundles cleanly under `--platform=neutral` (verified: zero `node:*`).
34
34
  */
35
35
  import { z } from "zod";
36
+ import type { ColorScheme } from "./color-scheme-utils.js";
36
37
  /** A single locale's content directory (`settings.locales[code]`). */
37
38
  export interface PresetLocaleConfig {
38
39
  dir: string;
@@ -72,18 +73,23 @@ export interface PresetSettings {
72
73
  claudeResources?: PresetClaudeResourcesConfig | false;
73
74
  /** "owner/repo" — when set, enables `#123` / SHA autolinks in markdown. Omit to disable entirely. */
74
75
  githubAutolinksRepo?: string;
75
- /** When `true`, the preset adds the package-owned route-injection plugin
76
- * (`@takazudo/zudo-doc/plugins/routes`). On by default since the fast-follow
77
- * (#2372): the showcase and create-zudo-doc both EMIT `packageOwnedRoutes: true`
78
- * into their generated `settings.ts`. `buildPlugins` reads this value truthily
79
- * and does NO central defaulting a hand-written settings object that OMITS the
80
- * field is treated as off; set it explicitly to control injection.
81
- * See `docs/adr/route-injection-seam.md`. */
76
+ /**
77
+ * When `true` (the **default** when omitted — #2404), the preset adds the
78
+ * package-owned route-injection plugin (`@takazudo/zudo-doc/plugins/routes`).
79
+ * Set explicitly to `false` to opt out — only needed for projects shipping
80
+ * their own `pages/*.tsx` stubs for every doc route. When `false` AND doc
81
+ * content is configured (`docsDir` non-empty and/or locales/versions set),
82
+ * a single `console.warn` is emitted per build as a heads-up (the build
83
+ * succeeds). See `docs/adr/route-injection-seam.md`. (#2404)
84
+ */
82
85
  packageOwnedRoutes?: boolean;
83
86
  /** Gate for the `/docs/tags` + `/docs/tags/[tag]` injected routes. */
84
87
  docTags?: boolean;
85
88
  /** Gate for the SSR `/api/ai-chat` injected route (`prerender: false`). */
86
89
  aiAssistant?: boolean;
90
+ /** When `false`, disables zfb's CJK-friendly line-break behaviour.
91
+ * Absent means "use the engine default" (currently `true`). */
92
+ cjkFriendly?: boolean;
87
93
  }
88
94
  /**
89
95
  * The directives recipe map (`markdown.features.directives`): directive name →
@@ -143,6 +149,19 @@ export interface ZudoDocPresetArgs {
143
149
  * `settings.packageOwnedRoutes` is true. Optional — defaults to `[]`.
144
150
  */
145
151
  tagVocabulary?: readonly PresetTagVocabularyEntry[];
152
+ /**
153
+ * The host's color-scheme palette map (`src/config/color-schemes.ts`
154
+ * `colorSchemes`). Only consumed when `settings.packageOwnedRoutes` is true
155
+ * — rides into the route-context virtual module so the package-owned routes
156
+ * (incl. `/404`) can emit the correct `--zd-*` CSS custom properties.
157
+ *
158
+ * Serializable JSON (ColorScheme has no function-valued fields), so it
159
+ * round-trips through `JSON.stringify` losslessly.
160
+ *
161
+ * Optional — when absent, package-owned routes fall back to `DEFAULT_SCHEME`
162
+ * (a neutral grey ramp). See `routes/_chrome.tsx`.
163
+ */
164
+ colorSchemes?: Record<string, ColorScheme>;
146
165
  }
147
166
  export interface PresetCollection {
148
167
  name: string;
@@ -163,6 +182,7 @@ export interface PresetResolveMarkdownLinks {
163
182
  }
164
183
  export interface PresetMarkdown {
165
184
  features: Record<string, boolean | Record<string, unknown>>;
185
+ cjkFriendly?: boolean;
166
186
  }
167
187
  export interface PresetCodeHighlight {
168
188
  themeLight: string;
@@ -195,4 +215,4 @@ export interface ZudoDocPresetResult {
195
215
  * });
196
216
  * ```
197
217
  */
198
- export declare function zudoDocPreset({ settings, buildDocsSchema, directiveVocabulary, translations, tagVocabulary, }: ZudoDocPresetArgs): ZudoDocPresetResult;
218
+ export declare function zudoDocPreset({ settings, buildDocsSchema, directiveVocabulary, translations, tagVocabulary, colorSchemes, }: ZudoDocPresetArgs): ZudoDocPresetResult;
package/dist/preset.js CHANGED
@@ -4,13 +4,17 @@ function zudoDocPreset({
4
4
  buildDocsSchema,
5
5
  directiveVocabulary,
6
6
  translations,
7
- tagVocabulary
7
+ tagVocabulary,
8
+ colorSchemes
8
9
  }) {
9
10
  const docsSchemaJson = z.toJSONSchema(buildDocsSchema());
10
11
  return {
11
12
  collections: buildCollections(settings, docsSchemaJson),
12
- plugins: buildPlugins(settings, { translations, tagVocabulary }),
13
- markdown: { features: buildMarkdownFeatures(settings, directiveVocabulary) },
13
+ plugins: buildPlugins(settings, { translations, tagVocabulary, colorSchemes }),
14
+ markdown: {
15
+ features: buildMarkdownFeatures(settings, directiveVocabulary),
16
+ ...settings.cjkFriendly !== void 0 ? { cjkFriendly: settings.cjkFriendly } : {}
17
+ },
14
18
  // Dual-theme syntect (zfb >= 0.1.0-next.45). Theme names are SYNTECT
15
19
  // built-ins, NOT Shiki names. Tokens emit `--shiki-light`/`--shiki-dark`
16
20
  // CSS custom properties resolved by the host CSS via `light-dark()`, so
@@ -113,18 +117,41 @@ function buildPlugins(settings, routeContext) {
113
117
  const localeRecord = Object.fromEntries(
114
118
  Object.entries(settings.locales).map(([code, locale]) => [code, { dir: locale.dir }])
115
119
  );
120
+ const effectivePackageOwnedRoutes = settings.packageOwnedRoutes ?? true;
121
+ if (effectivePackageOwnedRoutes) {
122
+ const contentConfiguredForWarn = typeof settings.docsDir === "string" && settings.docsDir.length > 0 || Object.keys(settings.locales).length > 0 || Array.isArray(settings.versions) && settings.versions.length > 0;
123
+ const translationsMissing = !routeContext.translations || Object.keys(routeContext.translations).length === 0;
124
+ const colorSchemesMissing = !routeContext.colorSchemes;
125
+ if (contentConfiguredForWarn && (translationsMissing || colorSchemesMissing)) {
126
+ const missing = [
127
+ ...translationsMissing ? ["translations"] : [],
128
+ ...colorSchemesMissing ? ["colorSchemes"] : []
129
+ ].join(" and/or ");
130
+ console.warn(
131
+ `zudo-doc: packageOwnedRoutes is on but ${missing} were not passed to zudoDocPreset \u2014 package-owned routes (incl. /404) will render with fallback i18n/theme. Pass them to inherit host bindings.`
132
+ );
133
+ }
134
+ }
135
+ if (!effectivePackageOwnedRoutes) {
136
+ const contentConfigured = typeof settings.docsDir === "string" && settings.docsDir.length > 0 || Object.keys(settings.locales).length > 0 || Array.isArray(settings.versions) && settings.versions.length > 0;
137
+ if (contentConfigured) {
138
+ console.warn(
139
+ "zudo-doc: packageOwnedRoutes is off but doc content is configured \u2014 no doc routes will be injected; the build will produce only host pages/. Set packageOwnedRoutes: true in settings."
140
+ );
141
+ }
142
+ }
116
143
  return [
117
- // Package-owned route injection — dormant by default (Decision 4). The
118
- // descriptor is a BARE SPECIFIER, never an imported plugin function: the
119
- // preset's node-free eval-graph guard (preset.test.ts) bundles this module
120
- // under --platform=neutral, and importing the plugin would drag its
144
+ // Package-owned route injection — default-on (#2404). The descriptor is a
145
+ // BARE SPECIFIER, never an imported plugin function: the preset's node-free
146
+ // eval-graph guard (preset.test.ts) bundles this module under
147
+ // --platform=neutral, and importing the plugin would drag its
121
148
  // `injectRoute`/`node:*` graph into the config eval. The plugin's `setup`
122
149
  // hook reads `options` to (a) emit the route-context virtual module
123
150
  // (serializable settings/translations/tagVocabulary only) and (b) derive
124
151
  // the route catalog from `settings.locales` / `settings.versions`. Listed
125
152
  // FIRST so an injected route is registered before the other plugins'
126
153
  // preBuild work runs (ordering is cosmetic — injection happens in `setup`).
127
- ...settings.packageOwnedRoutes ? [
154
+ ...effectivePackageOwnedRoutes ? [
128
155
  {
129
156
  name: "@takazudo/zudo-doc/plugins/routes",
130
157
  options: {
@@ -133,7 +160,8 @@ function buildPlugins(settings, routeContext) {
133
160
  // src/config/settings.ts), so it round-trips losslessly.
134
161
  settings,
135
162
  translations: routeContext.translations ?? {},
136
- tagVocabulary: routeContext.tagVocabulary ?? []
163
+ tagVocabulary: routeContext.tagVocabulary ?? [],
164
+ colorSchemes: routeContext.colorSchemes ?? null
137
165
  }
138
166
  }
139
167
  ] : [],
@@ -5,7 +5,7 @@ import {
5
5
  HeadWithDefaults,
6
6
  HeaderWithDefaults,
7
7
  FooterWithDefaults,
8
- BodyEndIslandsStub,
8
+ BodyEndIslands,
9
9
  composeMetaTitle
10
10
  } from "./_chrome.js";
11
11
  const frontmatter = { title: "404" };
@@ -24,7 +24,7 @@ function NotFoundPage() {
24
24
  sidebarOverride: /* @__PURE__ */ jsx(Fragment, {}),
25
25
  headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale }),
26
26
  footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }),
27
- bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslandsStub, { basePath: settings.base ?? "/" }),
27
+ bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
28
28
  enableClientRouter: settings.dynamicPageTransition,
29
29
  children: /* @__PURE__ */ jsxs("div", { class: "min-h-[60vh] flex flex-col items-center justify-center px-hsp-2xl py-vsp-xl", children: [
30
30
  /* @__PURE__ */ jsx("h1", { class: "text-display font-bold mb-vsp-md", children: "404" }),
@@ -1,13 +1,13 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- import type { JSX, VNode } from "preact";
3
+ import type { JSX, VNode, ComponentChildren } from "preact";
4
4
  import type { DocNavNode } from "./_docs-helpers.js";
5
- /** Package no-op body-end islands. The host's `BodyEndIslands` wires project
6
- * island bootstraps (client-router / design-token-panel) that cannot live in
7
- * the package; the package default renders nothing. */
8
- declare function BodyEndIslandsStub(_props: {
9
- basePath: string;
10
- }): JSX.Element;
5
+ /** Package-default body-end islands the package-island subset of the host's
6
+ * `BodyEndIslands` (AiChatModal / ImageEnlarge / MermaidEnlarge), reconstructed
7
+ * from the serializable virtual-module `settings` flags (#2406 / #2401(c)).
8
+ * The host-owned bootstraps (client-router / design-token-panel) cannot live
9
+ * in the package, so they are NOT included here — A2/S4 re-homes those. */
10
+ declare const BodyEndIslands: (props: import("../doc-body-end-islands/index.js").BodyEndIslandsProps) => JSX.Element;
11
11
  export declare const composeMetaTitle: (title: string) => string;
12
12
  export declare const HeadWithDefaults: (props: import("../head-with-defaults/index.js").HeadWithDefaultsProps) => JSX.Element;
13
13
  export declare const HeaderWithDefaults: (props: import("../header-with-defaults/index.js").HeaderWithDefaultsProps) => JSX.Element;
@@ -25,8 +25,8 @@ export declare const collectTagMapForLocale: (locale: string) => Map<string, imp
25
25
  tagInfo: import("../tag-pages/index.js").TagInfo;
26
26
  }) => JSX.Element, TagsIndexPageView: (props: {
27
27
  locale: string;
28
- children?: import("preact").ComponentChildren;
28
+ children?: ComponentChildren;
29
29
  }) => JSX.Element;
30
- export { SiteTreeNavWrapper, BodyEndIslandsStub };
30
+ export { SiteTreeNavWrapper, BodyEndIslands };
31
31
  export { buildDocRouteEntries, } from "./_context.js";
32
32
  export type { DocNavNode };
@@ -22,7 +22,8 @@ import {
22
22
  findNode,
23
23
  firstRoutedHref,
24
24
  collectTags,
25
- stableDocs
25
+ stableDocs,
26
+ colorSchemes
26
27
  } from "./_context.js";
27
28
  import { createComposeMetaTitle } from "../compose-meta-title/index.js";
28
29
  import {
@@ -35,6 +36,7 @@ import { createFooterWithDefaults } from "../footer-with-defaults/index.js";
35
36
  import { createSidebarWithDefaults } from "../sidebar-with-defaults/index.js";
36
37
  import { createSidebarPrepaint } from "../sidebar-prepaint/index.js";
37
38
  import { createDocBodyEnd } from "../doc-body-end/index.js";
39
+ import { createBodyEndIslands } from "../doc-body-end-islands/index.js";
38
40
  import { createDocPager } from "../doc-pager/index.js";
39
41
  import { createDocPageShell } from "../doc-page-shell/index.js";
40
42
  import { createDocContentHeader } from "../doc-content-header/index.js";
@@ -60,6 +62,10 @@ import {
60
62
  getThemeDefaultMode as getThemeDefaultModeBase
61
63
  } from "../nav-data-prep/index.js";
62
64
  import { buildSidebarForSection } from "../sidebar-utils/index.js";
65
+ import { Details } from "../details/index.js";
66
+ import {
67
+ HtmlPreviewWrapper
68
+ } from "../html-preview-wrapper/index.js";
63
69
  const GREY_RAMP = [
64
70
  "#000000",
65
71
  "#1a1a1a",
@@ -88,9 +94,7 @@ const DEFAULT_SCHEME = {
88
94
  };
89
95
  const docHistoryMeta = {};
90
96
  const sidebarsConfig = {};
91
- function BodyEndIslandsStub(_props) {
92
- return /* @__PURE__ */ jsx(Fragment, {});
93
- }
97
+ const BodyEndIslands = createBodyEndIslands({ settings });
94
98
  function DocHistoryStub(_props) {
95
99
  return /* @__PURE__ */ jsx(Fragment, {});
96
100
  }
@@ -98,13 +102,28 @@ function SearchWidgetBound(props) {
98
102
  return SearchWidget({ ...props, base: withBase("/") });
99
103
  }
100
104
  const composeMetaTitle = createComposeMetaTitle(settings.siteName);
105
+ function resolveHostScheme(key) {
106
+ if (!colorSchemes) return DEFAULT_SCHEME;
107
+ return colorSchemes[key] ?? DEFAULT_SCHEME;
108
+ }
101
109
  const HeadWithDefaults = createHeadWithDefaults({
102
110
  settings,
103
111
  composeMetaTitle,
104
112
  withBase,
105
113
  absoluteUrl,
106
- generateCssCustomProperties: () => generateCssCustomProperties(DEFAULT_SCHEME),
107
- generateLightDarkCssProperties: () => generateLightDarkCssProperties(DEFAULT_SCHEME, DEFAULT_SCHEME)
114
+ // Called only in single-scheme mode (colorMode false): resolve via settings.colorScheme.
115
+ generateCssCustomProperties: () => generateCssCustomProperties(resolveHostScheme(settings.colorScheme)),
116
+ // Called only in light/dark mode (colorMode truthy): resolve the pair.
117
+ generateLightDarkCssProperties: () => {
118
+ const cm = settings.colorMode;
119
+ if (cm) {
120
+ return generateLightDarkCssProperties(
121
+ resolveHostScheme(cm.lightScheme),
122
+ resolveHostScheme(cm.darkScheme)
123
+ );
124
+ }
125
+ return generateLightDarkCssProperties(DEFAULT_SCHEME, DEFAULT_SCHEME);
126
+ }
108
127
  });
109
128
  function buildRootMenuItems(lang, currentVersion) {
110
129
  return buildRootMenuItemsBase(
@@ -189,7 +208,7 @@ const SidebarWithDefaults = createSidebarWithDefaults({
189
208
  t
190
209
  });
191
210
  const SidebarPrepaint = createSidebarPrepaint({ sidebarToggle: settings.sidebarToggle });
192
- const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands: BodyEndIslandsStub });
211
+ const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands });
193
212
  const DocPager = createDocPager({ t });
194
213
  const DocMetainfoArea = createDocMetainfoArea({
195
214
  settings,
@@ -270,6 +289,15 @@ const SiteTreeNavWrapper = createSiteTreeNavWrapper({
270
289
  groupSatelliteNodes,
271
290
  getCategoryOrder
272
291
  });
292
+ function HtmlPreviewBound(props) {
293
+ return HtmlPreviewWrapper({
294
+ globalConfig: settings.htmlPreview ?? null,
295
+ ...props
296
+ });
297
+ }
298
+ function IslandPassthrough(props) {
299
+ return props.children ?? null;
300
+ }
273
301
  function createMdxComponentsBound(lang = defaultLocale) {
274
302
  return createMdxComponents({
275
303
  settings,
@@ -279,9 +307,22 @@ function createMdxComponentsBound(lang = defaultLocale) {
279
307
  CategoryTreeNav: CategoryTreeNavWrapper,
280
308
  SiteTreeNav: SiteTreeNavWrapper
281
309
  },
282
- // PresetGenerator and other showcase-only slots resolve to a package stub
283
- // (render nothing) they are project-bound and not in the package.
310
+ // Package-owned content components wired here so an INJECTED docs route
311
+ // (packageOwnedRoutes, no host `pages/` stub) renders MDX using these tags
312
+ // without the "MDX requires '<X>' to be passed via the 'components' prop"
313
+ // error (sub-issue #2390 / supersedes #2377).
314
+ // - Details → fully functional (pure <details>, no client JS).
315
+ // - HtmlPreview → SSR-renders; see HtmlPreviewBound for the hydration
316
+ // caveat tied to the node_modules islands-scanner gap.
317
+ // - Island → SSR pass-through; see IslandPassthrough.
318
+ // PresetGenerator stays a package stub (render nothing): it is the
319
+ // showcase's project-bound interactive island and downstream projects stub
320
+ // it. The showcase keeps its host-owned docs catch-all routes (allowlisted)
321
+ // so its real PresetGenerator still renders.
284
322
  extras: {
323
+ Details,
324
+ HtmlPreview: HtmlPreviewBound,
325
+ Island: IslandPassthrough,
285
326
  PresetGenerator: (_props) => null
286
327
  }
287
328
  });
@@ -322,7 +363,7 @@ const VersionsPageView = createVersionsPageView({
322
363
  HeadWithDefaults,
323
364
  HeaderWithDefaults,
324
365
  FooterWithDefaults,
325
- BodyEndIslands: BodyEndIslandsStub
366
+ BodyEndIslands
326
367
  }
327
368
  });
328
369
  const tagPages = createTagPages({
@@ -339,7 +380,7 @@ const tagPages = createTagPages({
339
380
  HeadWithDefaults,
340
381
  HeaderWithDefaults,
341
382
  FooterWithDefaults,
342
- BodyEndIslands: BodyEndIslandsStub,
383
+ BodyEndIslands,
343
384
  DocHistoryArea
344
385
  }
345
386
  });
@@ -348,7 +389,7 @@ import {
348
389
  buildDocRouteEntries
349
390
  } from "./_context.js";
350
391
  export {
351
- BodyEndIslandsStub,
392
+ BodyEndIslands,
352
393
  FooterWithDefaults,
353
394
  HeadWithDefaults,
354
395
  HeaderWithDefaults,
@@ -1,4 +1,5 @@
1
1
  import type { Settings } from "../settings.js";
2
+ import type { ColorScheme } from "../color-scheme-utils.js";
2
3
  import type { FactoryI18n } from "../factory-context/index.js";
3
4
  import { type UrlHelpers } from "../url-helpers/index.js";
4
5
  import { toRouteSlug, toSlugParams } from "../slug/index.js";
@@ -10,10 +11,14 @@ export interface RouteContextPayload {
10
11
  settings: Settings;
11
12
  translations: Record<string, Record<string, string>>;
12
13
  tagVocabulary: readonly TagVocabularyEntry[];
14
+ colorSchemes: Record<string, ColorScheme> | null;
13
15
  }
14
16
  /** The serializable route-context (from the virtual module). */
15
17
  export declare const ctx: RouteContextPayload;
16
18
  export declare const settings: Settings;
19
+ /** Host color-scheme palette map. `null` when not supplied — `_chrome.tsx`
20
+ * falls back to `DEFAULT_SCHEME` in that case. */
21
+ export declare const colorSchemes: Record<string, ColorScheme> | null;
17
22
  export declare const defaultLocale: string;
18
23
  export declare const locales: readonly string[];
19
24
  export declare function getLocaleConfig(locale: string): {
@@ -35,6 +35,7 @@ const ctx = routeContext;
35
35
  const settings = ctx.settings;
36
36
  const translations = ctx.translations;
37
37
  const tagVocabulary = ctx.tagVocabulary;
38
+ const colorSchemes = ctx.colorSchemes;
38
39
  const defaultLocale = settings.defaultLocale;
39
40
  const locales = [
40
41
  defaultLocale,
@@ -183,6 +184,7 @@ export {
183
184
  buildNavTree,
184
185
  collectAutoIndexNodes,
185
186
  collectTags,
187
+ colorSchemes,
186
188
  ctx,
187
189
  defaultLocale,
188
190
  docsUrl,
@@ -19,7 +19,7 @@ import {
19
19
  HeadWithDefaults,
20
20
  HeaderWithDefaults,
21
21
  FooterWithDefaults,
22
- BodyEndIslandsStub,
22
+ BodyEndIslands,
23
23
  composeMetaTitle
24
24
  } from "./_chrome.js";
25
25
  const frontmatter = { title: "Home" };
@@ -53,7 +53,7 @@ function IndexPage() {
53
53
  sidebarOverride: /* @__PURE__ */ jsx(Fragment, {}),
54
54
  headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: withBase("/") }),
55
55
  footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }),
56
- bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslandsStub, { basePath: settings.base ?? "/" }),
56
+ bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
57
57
  enableClientRouter: settings.dynamicPageTransition,
58
58
  children: [
59
59
  /* @__PURE__ */ jsx("div", { class: "flex justify-center mb-vsp-xl", children: /* @__PURE__ */ jsxs("div", { class: "flex flex-col items-center text-center gap-hsp-md lg:flex-row lg:text-left lg:gap-hsp-xl", children: [
@@ -20,7 +20,7 @@ import {
20
20
  HeadWithDefaults,
21
21
  HeaderWithDefaults,
22
22
  FooterWithDefaults,
23
- BodyEndIslandsStub,
23
+ BodyEndIslands,
24
24
  composeMetaTitle
25
25
  } from "./_chrome.js";
26
26
  const frontmatter = { title: "Home" };
@@ -63,7 +63,7 @@ function LocaleIndexPage({ params }) {
63
63
  sidebarOverride: /* @__PURE__ */ jsx(Fragment, {}),
64
64
  headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: withBase(`/${locale}/`) }),
65
65
  footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }),
66
- bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslandsStub, { basePath: settings.base ?? "/" }),
66
+ bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
67
67
  enableClientRouter: settings.dynamicPageTransition,
68
68
  children: [
69
69
  /* @__PURE__ */ jsx("div", { class: "flex justify-center mb-vsp-xl", children: /* @__PURE__ */ jsxs("div", { class: "flex flex-col items-center text-center gap-hsp-md lg:flex-row lg:text-left lg:gap-hsp-xl", children: [