@takazudo/zudo-doc 1.1.0 → 1.3.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.
package/dist/content.css CHANGED
@@ -271,19 +271,22 @@
271
271
  * rules must live here explicitly. Closes zudolab/zudo-doc#1357 / #1444 / #1456.
272
272
  *
273
273
  * Spacing: the box carries NO margin — the separating gap to the next block is
274
- * supplied by that block's flow-space `margin-top` (the owl rule above). The
275
- * snug bottom padding keeps the body tight inside the box without inflating
276
- * that gap. (A missing bottom margin on the box is intentional in the flow
277
- * model do NOT add one; see zudolab/zudo-doc#2188 for why the per-element
278
- * margin model that needed it was retired.)
274
+ * supplied by that block's flow-space `margin-top` (the page owl above). Blocks
275
+ * INSIDE `.admonition-body` get their own shorter flow gap (vsp-sm) via the
276
+ * nested owl below, since the page owl only reaches direct children of
277
+ * `.zd-content`; the box's vsp-sm bottom padding then gives the last block room
278
+ * so it isn't cramped against the edge. (A missing bottom margin on the box is
279
+ * intentional in the flow model — do NOT add one; see zudolab/zudo-doc#2188 for
280
+ * why the per-element margin model that needed it was retired.)
279
281
  * ======================================== */
280
282
 
281
283
  [data-admonition] {
282
284
  border-left: 4px solid var(--color-muted);
283
- /* Asymmetric padding mirrors the Astro reference (`pt-vsp-md pb-vsp-2xs`)
284
- * generous space at the top, snug at the bottom — so the title row sits
285
- * under generous breathing room without inflating the bottom gap. */
286
- padding: var(--spacing-vsp-md) var(--spacing-hsp-lg) var(--spacing-vsp-2xs);
285
+ /* Generous top padding gives the title row breathing room; the bottom uses
286
+ * the same vsp-sm as the body inner flow (below) so the last block isn't
287
+ * cramped against the box edge. (Was pb-vsp-2xs too tight once the body
288
+ * has multiple flowed blocks; zudolab/zudo-doc#2417.) */
289
+ padding: var(--spacing-vsp-md) var(--spacing-hsp-lg) var(--spacing-vsp-sm);
287
290
  background-color: color-mix(in srgb, var(--color-muted) 12%, var(--color-bg));
288
291
  border-radius: 0 var(--radius-DEFAULT) var(--radius-DEFAULT) 0;
289
292
  }
@@ -301,6 +304,19 @@
301
304
  margin-right: var(--spacing-hsp-2xs);
302
305
  }
303
306
 
307
+ /* Flow spacing between blocks INSIDE an admonition body. The page-level
308
+ * `.zd-content > * + *` owl only reaches direct children of `.zd-content`, so
309
+ * nested blocks here had no inter-element gap (zudolab/zudo-doc#2417). The
310
+ * admonition is a narrower scope than the page flow, so a shorter gap
311
+ * (vsp-sm < the page's vsp-md) reads as natural. Layered in `zd-flow` for the
312
+ * same reason as the page owl: it must beat preflight's `* { margin: 0 }` but
313
+ * lose to an unlayered `mt-*` utility on a nested block. */
314
+ @layer zd-flow {
315
+ .admonition-body > :where(* + *) {
316
+ margin-top: var(--spacing-vsp-sm);
317
+ }
318
+ }
319
+
304
320
  .admonition-body > :first-child {
305
321
  margin-top: 0;
306
322
  }
@@ -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),
@@ -36,12 +36,11 @@ generated: true
36
36
  `;
37
37
  fs.writeFileSync(path.join(outputDir, "index.mdx"), mdx);
38
38
  }
39
- function writeUnlistedSubPage(outputPath, title, slug, body) {
39
+ function writeUnlistedSubPage(outputPath, title, body) {
40
40
  fs.writeFileSync(
41
41
  outputPath,
42
42
  `---
43
43
  title: "${escapeTitle(title)}"
44
- slug: "${slug}"
45
44
  unlisted: true
46
45
  generated: true
47
46
  ---
@@ -55,6 +54,46 @@ function assertNotIndexReserved(nameOrSlug, errorMessage) {
55
54
  throw new Error(errorMessage);
56
55
  }
57
56
  }
57
+ function isRepoRelativeLink(url) {
58
+ const trimmed = url.trim();
59
+ if (trimmed === "") return false;
60
+ if (trimmed.startsWith("#")) return false;
61
+ if (trimmed.startsWith("/")) return false;
62
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return false;
63
+ return true;
64
+ }
65
+ function downgradeRepoRelativeLinks(content) {
66
+ const blockPlaceholder = "\0CRLINK_BLOCK_";
67
+ const inlinePlaceholder = "\0CRLINK_INLINE_";
68
+ const codeBlocks = [];
69
+ const withBlocks = content.replace(/(`{3,}|~{3,})[^\n]*\n[\s\S]*?\1/g, (match) => {
70
+ codeBlocks.push(match);
71
+ return `${blockPlaceholder}${codeBlocks.length - 1}\0`;
72
+ });
73
+ const transformed = withBlocks.split(new RegExp(`(${blockPlaceholder}\\d+\0)`, "g")).map((part) => {
74
+ if (new RegExp(`^${blockPlaceholder}\\d+\0$`).test(part)) return part;
75
+ const inlineCodes = [];
76
+ const withInline = part.replace(
77
+ /(`{1,3})(?!`)([\s\S]*?[^`])\1(?!`)/g,
78
+ (match) => {
79
+ inlineCodes.push(match);
80
+ return `${inlinePlaceholder}${inlineCodes.length - 1}\0`;
81
+ }
82
+ );
83
+ const rewritten = withInline.replace(
84
+ /!?\[([^\]]*)\]\(([^)]+)\)/g,
85
+ (match, text, url) => isRepoRelativeLink(url) ? `\`${text}\`` : match
86
+ );
87
+ return rewritten.replace(
88
+ new RegExp(`${inlinePlaceholder}(\\d+)\0`, "g"),
89
+ (_, idx) => inlineCodes[Number(idx)] ?? ""
90
+ );
91
+ }).join("");
92
+ return transformed.replace(
93
+ new RegExp(`${blockPlaceholder}(\\d+)\0`, "g"),
94
+ (_, idx) => codeBlocks[Number(idx)] ?? ""
95
+ );
96
+ }
58
97
  function findClaudeMdFiles(dir, excludeDirs) {
59
98
  const results = [];
60
99
  if (!fs.existsSync(dir)) return results;
@@ -133,7 +172,7 @@ generated: true
133
172
 
134
173
  **Path:** \`${item.relPath}\`
135
174
 
136
- ${escapeForMdx(content.trim())}
175
+ ${escapeForMdx(downgradeRepoRelativeLinks(content.trim()))}
137
176
  `;
138
177
  fs.writeFileSync(path.join(outputDir, `${item.slug}.mdx`), mdx);
139
178
  });
@@ -297,13 +336,13 @@ generated: true
297
336
  ---
298
337
 
299
338
  ${body}`;
300
- fs.writeFileSync(path.join(outputDir, `${dir}.mdx`), mdx);
301
- const skillSlugBase = `claude-skills/${dir}`;
339
+ const skillDirOut = path.join(outputDir, dir);
340
+ ensureDir(skillDirOut);
341
+ fs.writeFileSync(path.join(skillDirOut, "index.mdx"), mdx);
302
342
  for (const ref of references) {
303
343
  writeUnlistedSubPage(
304
- path.join(outputDir, `${dir}--ref-${ref.name}.mdx`),
344
+ path.join(skillDirOut, `ref-${ref.name}.mdx`),
305
345
  ref.title,
306
- `${skillSlugBase}/ref-${ref.name}`,
307
346
  escapeForMdx(ref.content.trim())
308
347
  );
309
348
  }
@@ -316,9 +355,8 @@ ${body}`;
316
355
  const h1Match = raw.match(/^#\s+(.+)$/m);
317
356
  const title = h1Match?.[1] ?? slug;
318
357
  writeUnlistedSubPage(
319
- path.join(outputDir, `${dir}--script-${slug}.mdx`),
358
+ path.join(skillDirOut, `script-${slug}.mdx`),
320
359
  title,
321
- `${skillSlugBase}/script-${slug}`,
322
360
  escapeForMdx(raw.trim())
323
361
  );
324
362
  }
@@ -331,9 +369,8 @@ ${body}`;
331
369
  const h1Match = raw.match(/^#\s+(.+)$/m);
332
370
  const title = h1Match?.[1] ?? slug;
333
371
  writeUnlistedSubPage(
334
- path.join(outputDir, `${dir}--asset-${slug}.mdx`),
372
+ path.join(skillDirOut, `asset-${slug}.mdx`),
335
373
  title,
336
- `${skillSlugBase}/asset-${slug}`,
337
374
  escapeForMdx(raw.trim())
338
375
  );
339
376
  }
@@ -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,13 +73,15 @@ 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;
@@ -146,6 +149,19 @@ export interface ZudoDocPresetArgs {
146
149
  * `settings.packageOwnedRoutes` is true. Optional — defaults to `[]`.
147
150
  */
148
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>;
149
165
  }
150
166
  export interface PresetCollection {
151
167
  name: string;
@@ -199,4 +215,4 @@ export interface ZudoDocPresetResult {
199
215
  * });
200
216
  * ```
201
217
  */
202
- 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,12 +4,13 @@ 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
+ plugins: buildPlugins(settings, { translations, tagVocabulary, colorSchemes }),
13
14
  markdown: {
14
15
  features: buildMarkdownFeatures(settings, directiveVocabulary),
15
16
  ...settings.cjkFriendly !== void 0 ? { cjkFriendly: settings.cjkFriendly } : {}
@@ -116,18 +117,41 @@ function buildPlugins(settings, routeContext) {
116
117
  const localeRecord = Object.fromEntries(
117
118
  Object.entries(settings.locales).map(([code, locale]) => [code, { dir: locale.dir }])
118
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
+ }
119
143
  return [
120
- // Package-owned route injection — dormant by default (Decision 4). The
121
- // descriptor is a BARE SPECIFIER, never an imported plugin function: the
122
- // preset's node-free eval-graph guard (preset.test.ts) bundles this module
123
- // 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
124
148
  // `injectRoute`/`node:*` graph into the config eval. The plugin's `setup`
125
149
  // hook reads `options` to (a) emit the route-context virtual module
126
150
  // (serializable settings/translations/tagVocabulary only) and (b) derive
127
151
  // the route catalog from `settings.locales` / `settings.versions`. Listed
128
152
  // FIRST so an injected route is registered before the other plugins'
129
153
  // preBuild work runs (ordering is cosmetic — injection happens in `setup`).
130
- ...settings.packageOwnedRoutes ? [
154
+ ...effectivePackageOwnedRoutes ? [
131
155
  {
132
156
  name: "@takazudo/zudo-doc/plugins/routes",
133
157
  options: {
@@ -136,7 +160,8 @@ function buildPlugins(settings, routeContext) {
136
160
  // src/config/settings.ts), so it round-trips losslessly.
137
161
  settings,
138
162
  translations: routeContext.translations ?? {},
139
- tagVocabulary: routeContext.tagVocabulary ?? []
163
+ tagVocabulary: routeContext.tagVocabulary ?? [],
164
+ colorSchemes: routeContext.colorSchemes ?? null
140
165
  }
141
166
  }
142
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" }),
@@ -2,12 +2,12 @@
2
2
  /** @jsxImportSource preact */
3
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;
@@ -27,6 +27,6 @@ export declare const collectTagMapForLocale: (locale: string) => Map<string, imp
27
27
  locale: string;
28
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";
@@ -92,9 +94,7 @@ const DEFAULT_SCHEME = {
92
94
  };
93
95
  const docHistoryMeta = {};
94
96
  const sidebarsConfig = {};
95
- function BodyEndIslandsStub(_props) {
96
- return /* @__PURE__ */ jsx(Fragment, {});
97
- }
97
+ const BodyEndIslands = createBodyEndIslands({ settings });
98
98
  function DocHistoryStub(_props) {
99
99
  return /* @__PURE__ */ jsx(Fragment, {});
100
100
  }
@@ -102,13 +102,28 @@ function SearchWidgetBound(props) {
102
102
  return SearchWidget({ ...props, base: withBase("/") });
103
103
  }
104
104
  const composeMetaTitle = createComposeMetaTitle(settings.siteName);
105
+ function resolveHostScheme(key) {
106
+ if (!colorSchemes) return DEFAULT_SCHEME;
107
+ return colorSchemes[key] ?? DEFAULT_SCHEME;
108
+ }
105
109
  const HeadWithDefaults = createHeadWithDefaults({
106
110
  settings,
107
111
  composeMetaTitle,
108
112
  withBase,
109
113
  absoluteUrl,
110
- generateCssCustomProperties: () => generateCssCustomProperties(DEFAULT_SCHEME),
111
- 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
+ }
112
127
  });
113
128
  function buildRootMenuItems(lang, currentVersion) {
114
129
  return buildRootMenuItemsBase(
@@ -193,7 +208,7 @@ const SidebarWithDefaults = createSidebarWithDefaults({
193
208
  t
194
209
  });
195
210
  const SidebarPrepaint = createSidebarPrepaint({ sidebarToggle: settings.sidebarToggle });
196
- const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands: BodyEndIslandsStub });
211
+ const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands });
197
212
  const DocPager = createDocPager({ t });
198
213
  const DocMetainfoArea = createDocMetainfoArea({
199
214
  settings,
@@ -348,7 +363,7 @@ const VersionsPageView = createVersionsPageView({
348
363
  HeadWithDefaults,
349
364
  HeaderWithDefaults,
350
365
  FooterWithDefaults,
351
- BodyEndIslands: BodyEndIslandsStub
366
+ BodyEndIslands
352
367
  }
353
368
  });
354
369
  const tagPages = createTagPages({
@@ -365,7 +380,7 @@ const tagPages = createTagPages({
365
380
  HeadWithDefaults,
366
381
  HeaderWithDefaults,
367
382
  FooterWithDefaults,
368
- BodyEndIslands: BodyEndIslandsStub,
383
+ BodyEndIslands,
369
384
  DocHistoryArea
370
385
  }
371
386
  });
@@ -374,7 +389,7 @@ import {
374
389
  buildDocRouteEntries
375
390
  } from "./_context.js";
376
391
  export {
377
- BodyEndIslandsStub,
392
+ BodyEndIslands,
378
393
  FooterWithDefaults,
379
394
  HeadWithDefaults,
380
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: [
package/dist/safelist.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /* generated by gen-safelist.mjs — do not edit by hand */
2
- @source inline("-mb-px -ml-hsp-sm -translate-x-full 2xl:w-[24px] [&::-webkit-details-marker]:hidden [&_a]:text-accent [&_a]:underline [&_nav]:mb-0 [data-kbd-shortcut] [doc-history-meta] [doc-history] [doc-layout] [llms-txt] a a2 abbr about above absent absolute across activated active actual added admonition admonition- admonition-body admonition-title after after-breadcrumb after-content after-navigate after-sidebar after-title against agent agents ai-chat ai-chat-md ai-chat-trigger alert align-top all allow-same-origin allow-scripts alone already already-executed an anchor anchored and animate-spin announce antialiased any application/json application/xml applies apply-css-vars approach are area arg aria-atomic aria-busy aria-controls aria-current aria-disabled aria-expanded aria-haspopup aria-hidden aria-label aria-live aria-orientation aria-pressed aria-selected aria-valuemax aria-valuemin aria-valuenow arrive arrows article as asc aside aspect-[1200/630] aspect-square assets assigning assistant async at attach attribute attributes auto autogenerated available avoid await away b back backdrop:bg-bg/30 backdrop:bg-bg/80 backdrop:bg-overlay/60 backdrop:z-modal-backdrop background backtick backticks baked banner bare base base64 based batch be because before below best between bg bg-[#fff] bg-accent bg-bg bg-chat-assistant-bg bg-chat-user-bg bg-code-bg bg-fg bg-info/10 bg-info/5 bg-muted bg-overlay/30 bg-surface bg-transparent bg-warning/10 bg-warning/5 bi bigint bin blank blanks blob block blockquote blocks blur body body-end-components body-end-scripts boolean bootstrap border border-accent border-b border-b-2 border-b-[5px] border-bg/30 border-collapse border-danger border-dashed border-fg border-info/30 border-l border-l-0 border-l-[3px] border-muted border-none border-r border-solid border-t border-t-[2px] border-t-[3px] border-transparent border-warning/30 border-y both bottom-vsp-xl box-border br breadcrumb:end breadcrumb:start break-words browser browsers btn bug bundler but button buttons by bypassed cached call caller can cancellation cannot canonical caption carry cases cat-nav- catch category catppuccin-latte caught caution center center/contain change changed changes checkbox child ci circle cite class class-less claude claude-agents claude-commands claude-md claude-skills cleaned clear clear-css-vars clearing click client client-router client-side clip clobbering close closed closing code code-block-sr-announce code-group code-group-panel col col-resize colgroup collision color color-scheme color-scheme-changed color-scheme-provider color-tweak colorization colors command commands commit compare component component:github-link component:language-switcher component:search component:theme-toggle component:version-switcher compute computed concrete configuration configure configured connect const consumer consumes container containers containing content content-admonition content-type content-wrapper:end content-wrapper:start contents context controller controls converts copy correct correctly corrupt count covered covers cp crashes created cross-component crumb- cs css cur current cursor cursor-not-allowed cursor-pointer custom danger dark data data-active data-admonition data-base data-close-search data-group-id data-header data-header-logo data-header-nav data-header-right data-kbd-shortcut data-loading-index data-mermaid-enlarge-ready data-mermaid-rendered data-mermaid-src data-nav-item data-nav-item-dropdown data-nav-more data-nav-more-menu data-nav-more-toggle data-no-results data-open-search data-pan-active data-processed data-result-count-template data-search-count data-search-count-narrow data-search-dialog data-search-input data-search-placeholder data-search-results data-search-unavailable data-sidebar-hidden data-sidebar-resizer data-site-nav data-tab-btn data-tab-default data-tab-label data-tab-value data-tabs data-taglist-group data-testid data-theme data-theme/style data-variant data-version-banner data-version-menu data-version-switcher data-version-toggle data-zfb-transition-persist dd debounced decimal declare decoration-muted default defaults del delegated desc description design design-token-panel design-token-trigger desktop desktop-sidebar desktop-sidebar-toggle desktop-sidebar-toggle-island destructive detach detached details deterministic develop dfn diagram diagrams dialog dieser diff diff-line-added diff-line-content diff-line-empty diff-line-num diff-line-removed diff-row dir directly directories directory disabled disabled:opacity-50 disc display:none dist div dl do doc doc-card- doc-history doc-history-generate doc-history-panel doc-history-trigger doc-pager docs docs- docs-v- document document-level does double-registration drag draggable drop dropdown dropdown-child dropdown-parent dropdowns dt duration-150 duration-200 during dynamically e e2e each earlier ease-in-out edge eject ejected el element elements els else em emit emitting empty empty/undefined en enable end enlarged entire entities entries entry error escape escaped even eventually every exactly excerpt existing exists exit expected export extends failed fall fallback fallbacks falls false fast feature feed fg fields fieldset figcaption figure file fill fills finally find fire fires first fixed fixtures flag flash flat flex flex-1 flex-col flex-wrap flip flipping flips focus focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 focus-visible:underline focus:border-accent focus:outline-none focus:underline font font-bold font-medium font-mono font-semibold footer footer- for form found free fresh from frontmatter frontmatter-preview frozen fs-extra full fully function further g gap-[0.3em] gap-[clamp(1.5rem,3vw,4rem)] gap-hsp-2xs gap-hsp-md gap-hsp-sm gap-hsp-xl gap-hsp-xs gap-vsp-2xs gap-vsp-lg gap-vsp-md gap-vsp-xs gap-x-hsp-2xs gap-x-hsp-lg gap-x-hsp-md gap-x-hsp-sm gap-x-hsp-xs gap-y-vsp-2xs gap-y-vsp-lg gap-y-vsp-md gap-y-vsp-xs gaps gate generate generation genuine geometry get github github-link go got graph gray-matter grid grid-cols-1 grid-cols-2 group group-focus-visible:text-accent group-focus-visible:underline group-focus-within:block group-hover:bg-fg group-hover:block group-hover:text-accent group-hover:text-bg group-hover:underline group-open:rotate-90 guard h-[0.5rem] h-[0.625rem] h-[0.875rem] h-[1.125rem] h-[1.25rem] h-[1.575rem] h-[14px] h-[1lh] h-[2rem] h-[3.5rem] h-[3rem] h-[90vh] h-[calc(100%-3rem)] h-[calc(100vh-3.5rem)] h-dvh h-full h-icon-lg h-icon-md h-icon-sm h-icon-xs h1 h2 h3 h4 h5 h6 half hand handle handled handler handlers has hash-link have head head-links head-scripts header header- header-call:end header-call:start height here hex hidden highlight highlighter history hit horizontal host hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)] hover:bg-accent-hover hover:bg-accent/10 hover:bg-surface hover:border-accent hover:border-accent-hover hover:border-fg hover:text-accent hover:text-accent-hover hover:text-fg hover:underline hr href hrefs html i i2 i3 i4 icon identical idle idx if iframe image image-enlarge image/png img import important imports in inactive inbox includes index index-load info inherit initial initialised injected inline inline-block inline-flex input input-clear ins inset-0 inside install instance instanceof instead instructions intended intent into invalid inverse invoke is issues it italic item item- items items-baseline items-center items-start its itself javascript justify-between justify-center justify-end justify-start katex kbd keep keeps kept keyboard keyboard-shortcut keydown keystroke keywords khroma label landing language-switcher last:border-b-0 later launch leading-relaxed leading-snug leading-tight leaf- leak leaves leaving left left-0 left:calc legend legitimate lg lg:block lg:border lg:border-fg lg:border-solid lg:flex lg:flex-col lg:flex-row lg:gap-hsp-xl lg:grid-cols-[repeat(auto-fit,minmax(12rem,1fr))] lg:h-[90vh] lg:hidden lg:justify-start lg:m-auto lg:max-h-[90vh] lg:max-w-[52.5rem] lg:ml-[var(--zd-sidebar-w)] lg:pt-vsp-2xl lg:px-hsp-2xl lg:py-vsp-2xl lg:text-left lg:w-[90vw] lg:w-[clamp(16rem,25%,22rem)] li li2 light like likely line linger link link- links list-disc list-none listener literal literals lives llms llms-txt load local locale locales log longest-match look lostpointercapture lower lowercased luminance m m-0 m21 m6 main make malformed malicious maps mark marks matches matching math math-display math-inline max max-h-[80vh] max-h-[85vh] max-h-[90vh] max-h-full max-h-none max-w-[46rem] max-w-[80rem] max-w-[85%] max-w-[85vw] max-w-[90vw] max-w-[clamp(50rem,75vw,90rem)] max-w-full max-w-none may mb-0 mb-vsp-2xs mb-vsp-lg mb-vsp-md mb-vsp-sm mb-vsp-xl mb-vsp-xs measures measuring mechanism menu merged mermaid message messages meta metadata min-h-[60vh] min-h-[calc(100vh-3.5rem)] min-h-screen min-w-0 min-w-[10rem] min-w-[8rem] mirror mirrors missing ml-auto ml-hsp-2xl ml-hsp-lg ml-hsp-sm ml-hsp-xl mod mode mounted mouseenter mouseleave mr-hsp-sm mt-0 mt-vsp-2xl mt-vsp-2xs mt-vsp-3xs mt-vsp-lg mt-vsp-md mt-vsp-sm mt-vsp-xl must mutates mutation mutations mx-auto my-vsp-lg my-vsp-md name named native nav nav-card- navigating navigation navigations near needs new newly-swapped next no no-enlarge no-op no-repeat no-underline node node:buffer node:fs node:fs/promises node:module node:path nodes nofollow noindex non-empty non-persisted non-string none noopener noreferrer normal not not-object note now null number numeric object object-contain observe observer occurred of off og:description og:image og:image:alt og:image:height og:image:width og:title og:type og:url ol old older on once one only opacity-60 open open/close option or original other others out outline-none over overflow overflow-auto overflow-hidden overflow-x-auto overflow-y-auto overflow-y:auto overwrite own p p-0 p-hsp-lg p-hsp-md p-hsp-sm p-hsp-xl package packages padding page page-loading page-loading-overlay page-loading-spinner page-navigate-end pages paint pan panel panels parent parse parsed pass path paths pattern pb-[50vh] pb-vsp-md pb-vsp-xl pb-vsp-xs per per-call per-link permanently persisted pi pick picked picks picocolors pins pipelines pl-[1.25rem] pl-hsp-lg pl-hsp-sm pl-hsp-xl place placeholder placeholder:text-muted plain plural plus pnpm pointer-events-none pointercancel pointerdown pointermove pointerup polite polygon polyline populates port position position:fixed pr-[4px] pr-hsp-lg pr-hsp-md pr-hsp-sm pr-hsp-xl pre pre-lowercased preact preact/compat preact/hooks preact/jsx-runtime preload pres preserved produced produces production project propagating properties property props provided proxy pt-[0.15rem] pt-[2px] pt-vsp-3xs pt-vsp-md pt-vsp-sm pt-vsp-xl pt-vsp-xs ptag- public purely px px-hsp-2xl px-hsp-2xs px-hsp-lg px-hsp-md px-hsp-sm px-hsp-xl px-hsp-xs py-0 py-[2px] py-[calc(var(--spacing-vsp-xs)+0.15rem)] py-hsp-2xs py-hsp-sm py-hsp-xs py-vsp-2xs py-vsp-3xs py-vsp-lg py-vsp-md py-vsp-sm py-vsp-xl py-vsp-xs q query question r raw re-encode/decode re-lowercase re-querying re-render re-renders re-run re-running re-selects reach reached reaches read reading reads ready real real-value recorded recovers references refetch refreshes regenerate regenerates reinit reinits relative reload remains remove removed render rendered renders reorder repaint repeated repeating replaced replaces repopulate requires reserved resize resize-x resolve resolved resolves response restore restores result result-click results results-area retry return returns revision revisions rewrite right- right-0 ro robots role root rotate-180 rotate-90 round round-trip rounded rounded-[0.75rem] rounded-bl-[0.25rem] rounded-bl-[1rem] rounded-br-[0.25rem] rounded-br-[1rem] rounded-full rounded-lg rounded-t-[1rem] route router routes-src running runs runtime s safe safer same same-locale samp scanned schema-mismatch schema-missing scheme score scored script script-eval script-evaluation script-injection scripts scroll scrollbar scrolled scrollend search search-index searched section section- see sel-bg sel-fg select select-none self self-start sentinel separator server server-rendered set sets setup shadow-[0_1px_3px_color-mix(in_srgb,var(--color-fg)_8%,transparent)] shadow-lg shared shiki ship ships short shortcut should show shown shrink-0 sidebar sidebar- sidebar-toggle-island sidebar-tree-island signal similarity single singular site-search site-tree-nav-island sitemap- sites size skill skills skipping skips slash slug sm:border sm:border-muted sm:flex-row sm:grid-cols-2 sm:h-auto sm:items-center sm:justify-between sm:max-h-[80vh] sm:max-w-[52rem] sm:mx-auto sm:my-[10vh] sm:rounded-lg small smooth snapshot snapshots so soft soft-nav solid some somehow source sources space-y-vsp-2xs spacing span spans spec specifier specifiers sr-only src stale start state status stay sticky still stop stored stray string strings strip stroke-linecap stroke-linejoin stroke-width strong style style-attribute styles stylesheet sub subagents subsequent success successful summary sup support survives svg swap swapped swaps synchronous synchronously syntactically syntect t tab tab-item tab-panel tabindex table tablist tabpanel tabs tabs-container tabs-content tabs-nav tag tag- tag-item- tags tags:audit tbody td temp temp-element template temporary temporary-element terminal terms test-results text text-accent text-bg text-body text-caption text-center text-chat-assistant-text text-chat-user-text text-code-fg text-danger text-display text-fg text-heading text-info text-left text-micro text-muted text-muted/50 text-right text-small text-title text-warning text/plain textarea tfoot th that the thead their them theme theme-color theme-toggle theme/token then there these they this through throw time tip title to toggle toggle-ai-chat toggle-design-token-panel toggles token tokens tolerates too toolbar top-0 top-[3.5rem] top-full top-level total touches tp tr tracked tracking-wider trade-off transition transition-[background,color,border-color] transition-[left,color] transition-colors transition-transform translate-x-0 transparent treats tree tree-child- tree-item- tree-top- trick trigger trigger:ai-chat trigger:design-token-panel triggers true truncate truncated try turn twitter:card twitter:creator twitter:description twitter:image twitter:site twitter:title two type u ul unavailable unchanged undefined under underline understand unknown unmaintained unobserve unreadable unrelated unreleased unreliable unset unsupported up uppercase usage use used user uses utf-8 utf8 utilities v v2 val value value-reader values var variable version version- version-menu version-switcher vertical via video viewport virtual:zudo-doc-route-context visible vitesse-dark vocabulary w w-1/2 w-[0.5rem] w-[0.625rem] w-[0.875rem] w-[1.125rem] w-[1.575rem] w-[1.5rem] w-[1.75rem] w-[14px] w-[16px] w-[16rem] w-[18px] w-[280px] w-[2rem] w-[320px] w-[90vw] w-[var(--zd-sidebar-w)] w-dvw w-full w-icon-lg w-icon-md w-icon-sm w-icon-xs want warning was watching wbr wbr- we website went what when where whereas whether which while whitespace-nowrap whitespace-pre whole will with without word working worktrees would wrap wrapper wrappers written wrong wrote xl:flex xl:hidden y-scrollbar yet you your z-dropdown z-local-1 z-modal z-modal-backdrop z-sidebar z-toolbar zd-content zd-desktop-sidebar-toggle zd-doc-content-band zd-enlarge-btn zd-enlarge-dialog zd-enlarge-dialog-close zd-enlargeable zd-html-preview-code zd-mermaid-dialog zd-mermaid-enlargeable zd-mermaid-tool-btn zd-mermaid-toolbar zd-mermaid-transform zd-mermaid-viewport zd-sidebar-content-wrapper zd-sidebar-open zfb zfb:after-swap zfb:before-preparation zod zoom zudo-doc-design-tokens/v1 zudo-doc-sidebar-visible zudo-doc-sidebar-width zudo-doc-theme zudo-doc-theme-bridge");
2
+ @source inline("-mb-px -ml-hsp-sm -translate-x-full 2xl:w-[24px] [&::-webkit-details-marker]:hidden [&_a]:text-accent [&_a]:underline [&_nav]:mb-0 [data-kbd-shortcut] [doc-history-meta] [doc-history] [doc-layout] [llms-txt] a a2 abbr about above absent absolute across activated active actual added admonition admonition- admonition-body admonition-title after after-breadcrumb after-content after-navigate after-sidebar after-title against agent agents ai-chat ai-chat-md ai-chat-trigger alert align-top all allow-same-origin allow-scripts alone already already-executed an anchor anchored and and/or animate-spin announce antialiased any application/json application/xml applies apply-css-vars approach are area arg aria-atomic aria-busy aria-controls aria-current aria-disabled aria-expanded aria-haspopup aria-hidden aria-label aria-live aria-orientation aria-pressed aria-selected aria-valuemax aria-valuemin aria-valuenow arrive arrows article as asc aside aspect-[1200/630] aspect-square asset- assets assigning assistant async at attach attribute attributes auto autogenerated available avoid await away b back backdrop:bg-bg/30 backdrop:bg-bg/80 backdrop:bg-overlay/60 backdrop:z-modal-backdrop background backtick backticks baked banner bare base base64 based batch be because before below best between bg bg-[#fff] bg-accent bg-bg bg-chat-assistant-bg bg-chat-user-bg bg-code-bg bg-fg bg-info/10 bg-info/5 bg-muted bg-overlay/30 bg-surface bg-transparent bg-warning/10 bg-warning/5 bi bigint bin blank blanks blob block blockquote blocks blur body body-end-components body-end-scripts boolean bootstrap border border-accent border-b border-b-2 border-b-[5px] border-bg/30 border-collapse border-danger border-dashed border-fg border-info/30 border-l border-l-0 border-l-[3px] border-muted border-none border-r border-solid border-t border-t-[2px] border-t-[3px] border-transparent border-warning/30 border-y both bottom-vsp-xl box-border br breadcrumb:end breadcrumb:start break-words browser browsers btn bug build bundler but button buttons by bypassed cached call caller can cancellation cannot canonical caption carry cases cat-nav- catch category catppuccin-latte caught caution center center/contain change changed changes checkbox child ci circle cite class class-less claude claude-agents claude-commands claude-md claude-skills cleaned clear clear-css-vars clearing click client client-router client-side clip clobbering close closed closing code code-block-sr-announce code-group code-group-panel col col-resize colgroup collision color color-scheme color-scheme-changed color-scheme-provider color-tweak colorization colors command commands commit compare component component:github-link component:language-switcher component:search component:theme-toggle component:version-switcher compute computed concrete configuration configure configured connect const consumer consumes container containers containing content content-admonition content-type content-wrapper:end content-wrapper:start contents context controller controls converts copy correct correctly corrupt count covered covers cp crashes created cross-component crumb- cs css cur current cursor cursor-not-allowed cursor-pointer custom danger dark data data-active data-admonition data-base data-close-search data-group-id data-header data-header-logo data-header-nav data-header-right data-kbd-shortcut data-loading-index data-mermaid-enlarge-ready data-mermaid-rendered data-mermaid-src data-nav-item data-nav-item-dropdown data-nav-more data-nav-more-menu data-nav-more-toggle data-no-results data-open-search data-pan-active data-processed data-result-count-template data-search-count data-search-count-narrow data-search-dialog data-search-input data-search-placeholder data-search-results data-search-unavailable data-sidebar-hidden data-sidebar-resizer data-site-nav data-tab-btn data-tab-default data-tab-label data-tab-value data-tabs data-taglist-group data-testid data-theme data-theme/style data-variant data-version-banner data-version-menu data-version-switcher data-version-toggle data-zfb-transition-persist dd debounced decimal declare decoration-muted default defaults del delegated desc description design design-token-panel design-token-trigger desktop desktop-sidebar desktop-sidebar-toggle desktop-sidebar-toggle-island destructive detach detached details deterministic develop dfn diagram diagrams dialog dieser diff diff-line-added diff-line-content diff-line-empty diff-line-num diff-line-removed diff-row dir directly directories directory disabled disabled:opacity-50 disc display:none dist div dl do doc doc-card- doc-history doc-history-generate doc-history-panel doc-history-trigger doc-pager docs docs- docs-v- document document-level does double-registration drag draggable drop dropdown dropdown-child dropdown-parent dropdowns dt duration-150 duration-200 during dynamically e e2e each earlier ease-in-out edge eject ejected el element elements els else em emit emitting empty empty/undefined en enable end enlarged entire entities entries entry error escape escaped even eventually every exactly excerpt existing exists exit expected export extends failed fall fallback fallbacks falls false fast feature feed fg fields fieldset figcaption figure file fill fills finally find fire fires first fixed fixtures flag flash flat flex flex-1 flex-col flex-wrap flip flipping flips focus focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 focus-visible:underline focus:border-accent focus:outline-none focus:underline font font-bold font-medium font-mono font-semibold footer footer- for form found free fresh from frontmatter frontmatter-preview frozen fs-extra full fully function further g gap-[0.3em] gap-[clamp(1.5rem,3vw,4rem)] gap-hsp-2xs gap-hsp-md gap-hsp-sm gap-hsp-xl gap-hsp-xs gap-vsp-2xs gap-vsp-lg gap-vsp-md gap-vsp-xs gap-x-hsp-2xs gap-x-hsp-lg gap-x-hsp-md gap-x-hsp-sm gap-x-hsp-xs gap-y-vsp-2xs gap-y-vsp-lg gap-y-vsp-md gap-y-vsp-xs gaps gate generate generation genuine geometry get github github-link go got graph gray-matter grid grid-cols-1 grid-cols-2 group group-focus-visible:text-accent group-focus-visible:underline group-focus-within:block group-hover:bg-fg group-hover:block group-hover:text-accent group-hover:text-bg group-hover:underline group-open:rotate-90 guard h-[0.5rem] h-[0.625rem] h-[0.875rem] h-[1.125rem] h-[1.25rem] h-[1.575rem] h-[14px] h-[1lh] h-[2rem] h-[3.5rem] h-[3rem] h-[90vh] h-[calc(100%-3rem)] h-[calc(100vh-3.5rem)] h-dvh h-full h-icon-lg h-icon-md h-icon-sm h-icon-xs h1 h2 h3 h4 h5 h6 half hand handle handled handler handlers has hash-link have head head-links head-scripts header header- header-call:end header-call:start height here hex hidden highlight highlighter history hit horizontal host hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)] hover:bg-accent-hover hover:bg-accent/10 hover:bg-surface hover:border-accent hover:border-accent-hover hover:border-fg hover:text-accent hover:text-accent-hover hover:text-fg hover:underline hr href hrefs html i i18n/theme. i2 i3 i4 icon identical idle idx if iframe image image-enlarge image/png img import important imports in inactive inbox includes index index-load info inherit initial initialised injected inline inline-block inline-flex input input-clear ins inset-0 inside install instance instanceof instead instructions intended intent into invalid inverse invoke is issues it italic item item- items items-baseline items-center items-start its itself javascript justify-between justify-center justify-end justify-start katex kbd keep keeps kept keyboard keyboard-shortcut keydown keystroke keywords khroma label landing language-switcher last:border-b-0 later launch leading-relaxed leading-snug leading-tight leaf- leak leaves leaving left left-0 left:calc legend legitimate lg lg:block lg:border lg:border-fg lg:border-solid lg:flex lg:flex-col lg:flex-row lg:gap-hsp-xl lg:grid-cols-[repeat(auto-fit,minmax(12rem,1fr))] lg:h-[90vh] lg:hidden lg:justify-start lg:m-auto lg:max-h-[90vh] lg:max-w-[52.5rem] lg:ml-[var(--zd-sidebar-w)] lg:pt-vsp-2xl lg:px-hsp-2xl lg:py-vsp-2xl lg:text-left lg:w-[90vw] lg:w-[clamp(16rem,25%,22rem)] li li2 light like likely line linger link link- links list-disc list-none listener literal literals lives llms llms-txt load local locale locales log longest-match look lostpointercapture lower lowercased luminance m m-0 m21 m6 main make malformed malicious maps mark marks matches matching math math-display math-inline max max-h-[80vh] max-h-[85vh] max-h-[90vh] max-h-full max-h-none max-w-[46rem] max-w-[80rem] max-w-[85%] max-w-[85vw] max-w-[90vw] max-w-[clamp(50rem,75vw,90rem)] max-w-full max-w-none may mb-0 mb-vsp-2xs mb-vsp-lg mb-vsp-md mb-vsp-sm mb-vsp-xl mb-vsp-xs measures measuring mechanism menu merged mermaid message messages meta metadata min-h-[60vh] min-h-[calc(100vh-3.5rem)] min-h-screen min-w-0 min-w-[10rem] min-w-[8rem] mirror mirrors missing ml-auto ml-hsp-2xl ml-hsp-lg ml-hsp-sm ml-hsp-xl mod mode mounted mouseenter mouseleave mr-hsp-sm mt-0 mt-vsp-2xl mt-vsp-2xs mt-vsp-3xs mt-vsp-lg mt-vsp-md mt-vsp-sm mt-vsp-xl must mutates mutation mutations mx-auto my-vsp-lg my-vsp-md name named native nav nav-card- navigating navigation navigations near needs new newly-swapped next no no-enlarge no-op no-repeat no-underline node node:buffer node:fs node:fs/promises node:module node:path nodes nofollow noindex non-empty non-persisted non-string none noopener noreferrer normal not not-object note now null number numeric object object-contain observe observer occurred of off og:description og:image og:image:alt og:image:height og:image:width og:title og:type og:url ol old older on once one only opacity-60 open open/close option or original other others out outline-none over overflow overflow-auto overflow-hidden overflow-x-auto overflow-y-auto overflow-y:auto overwrite own p p-0 p-hsp-lg p-hsp-md p-hsp-sm p-hsp-xl package package-owned packages padding page page-loading page-loading-overlay page-loading-spinner page-navigate-end pages pages/. paint pan panel panels parent parse parsed pass passed path paths pattern pb-[50vh] pb-vsp-md pb-vsp-xl pb-vsp-xs per per-call per-link permanently persisted pi pick picked picks picocolors pins pipelines pl-[1.25rem] pl-hsp-lg pl-hsp-sm pl-hsp-xl place placeholder placeholder:text-muted plain plural plus pnpm pointer-events-none pointercancel pointerdown pointermove pointerup polite polygon polyline populates port position position:fixed pr-[4px] pr-hsp-lg pr-hsp-md pr-hsp-sm pr-hsp-xl pre pre-lowercased preact preact/compat preact/hooks preact/jsx-runtime preload pres preserved produce produced produces production project propagating properties property props provided proxy pt-[0.15rem] pt-[2px] pt-vsp-3xs pt-vsp-md pt-vsp-sm pt-vsp-xl pt-vsp-xs ptag- public purely px px-hsp-2xl px-hsp-2xs px-hsp-lg px-hsp-md px-hsp-sm px-hsp-xl px-hsp-xs py-0 py-[2px] py-[calc(var(--spacing-vsp-xs)+0.15rem)] py-hsp-2xs py-hsp-sm py-hsp-xs py-vsp-2xs py-vsp-3xs py-vsp-lg py-vsp-md py-vsp-sm py-vsp-xl py-vsp-xs q query question r raw re-encode/decode re-lowercase re-querying re-render re-renders re-run re-running re-selects reach reached reaches read reading reads ready real real-value recorded recovers ref- references refetch refreshes regenerate regenerates reinit reinits relative reload remains remove removed render rendered renders reorder repaint repeated repeating replaced replaces repopulate requires reserved resize resize-x resolve resolved resolves response restore restores result result-click results results-area retry return returns revision revisions rewrite right- right-0 ro robots role root rotate-180 rotate-90 round round-trip rounded rounded-[0.75rem] rounded-bl-[0.25rem] rounded-bl-[1rem] rounded-br-[0.25rem] rounded-br-[1rem] rounded-full rounded-lg rounded-t-[1rem] route router routes routes-src running runs runtime s safe safer same same-locale samp scanned schema-mismatch schema-missing scheme score scored script script- script-eval script-evaluation script-injection scripts scroll scrollbar scrolled scrollend search search-index searched section section- see sel-bg sel-fg select select-none self self-start sentinel separator server server-rendered set sets setup shadow-[0_1px_3px_color-mix(in_srgb,var(--color-fg)_8%,transparent)] shadow-lg shared shiki ship ships short shortcut should show shown shrink-0 sidebar sidebar- sidebar-toggle-island sidebar-tree-island signal similarity single singular site-search site-tree-nav-island sitemap- sites size skill skills skipping skips slash slug sm:border sm:border-muted sm:flex-row sm:grid-cols-2 sm:h-auto sm:items-center sm:justify-between sm:max-h-[80vh] sm:max-w-[52rem] sm:mx-auto sm:my-[10vh] sm:rounded-lg small smooth snapshot snapshots so soft soft-nav solid some somehow source sources space-y-vsp-2xs spacing span spans spec specifier specifiers sr-only src stale start state status stay sticky still stop stored stray string strings strip stroke-linecap stroke-linejoin stroke-width strong style style-attribute styles stylesheet sub subagents subsequent success successful summary sup support survives svg swap swapped swaps synchronous synchronously syntactically syntect t tab tab-item tab-panel tabindex table tablist tabpanel tabs tabs-container tabs-content tabs-nav tag tag- tag-item- tags tags:audit tbody td temp temp-element template temporary temporary-element terminal terms test-results text text-accent text-bg text-body text-caption text-center text-chat-assistant-text text-chat-user-text text-code-fg text-danger text-display text-fg text-heading text-info text-left text-micro text-muted text-muted/50 text-right text-small text-title text-warning text/plain textarea tfoot th that the thead their them theme theme-color theme-toggle theme/token then there these they this through throw time tip title to toggle toggle-ai-chat toggle-design-token-panel toggles token tokens tolerates too toolbar top-0 top-[3.5rem] top-full top-level total touches tp tr tracked tracking-wider trade-off transition transition-[background,color,border-color] transition-[left,color] transition-colors transition-transform translate-x-0 translations transparent treats tree tree-child- tree-item- tree-top- trick trigger trigger:ai-chat trigger:design-token-panel triggers true truncate truncated try turn twitter:card twitter:creator twitter:description twitter:image twitter:site twitter:title two type u ul unavailable unchanged undefined under underline understand unknown unmaintained unobserve unreadable unrelated unreleased unreliable unset unsupported up uppercase usage use used user uses utf-8 utf8 utilities v v2 val value value-reader values var variable version version- version-menu version-switcher vertical via video viewport virtual:zudo-doc-route-context visible vitesse-dark vocabulary w w-1/2 w-[0.5rem] w-[0.625rem] w-[0.875rem] w-[1.125rem] w-[1.575rem] w-[1.5rem] w-[1.75rem] w-[14px] w-[16px] w-[16rem] w-[18px] w-[280px] w-[2rem] w-[320px] w-[90vw] w-[var(--zd-sidebar-w)] w-dvw w-full w-icon-lg w-icon-md w-icon-sm w-icon-xs want warning was watching wbr wbr- we website went were what when where whereas whether which while whitespace-nowrap whitespace-pre whole will with without word working worktrees would wrap wrapper wrappers written wrong wrote xl:flex xl:hidden y-scrollbar yet you your z-dropdown z-local-1 z-modal z-modal-backdrop z-sidebar z-toolbar zd-content zd-desktop-sidebar-toggle zd-doc-content-band zd-enlarge-btn zd-enlarge-dialog zd-enlarge-dialog-close zd-enlargeable zd-html-preview-code zd-mermaid-dialog zd-mermaid-enlargeable zd-mermaid-tool-btn zd-mermaid-toolbar zd-mermaid-transform zd-mermaid-viewport zd-sidebar-content-wrapper zd-sidebar-open zfb zfb:after-swap zfb:before-preparation zod zoom zudo-doc-design-tokens/v1 zudo-doc-sidebar-visible zudo-doc-sidebar-width zudo-doc-theme zudo-doc-theme-bridge");
@@ -254,17 +254,19 @@ export interface Settings {
254
254
  headerRightItems: HeaderRightItem[];
255
255
  /**
256
256
  * Build-time package-owned route injection (epic Package-First Finale #2356,
257
- * ADR `docs/adr/route-injection-seam.md`). When `true`, the preset adds the
258
- * `@takazudo/zudo-doc/plugins/routes` plugin, which injects the doc routes
259
- * from the package instead of requiring project-shipped `pages/*.tsx` stubs.
257
+ * ADR `docs/adr/route-injection-seam.md`). When `true` (the **default** when
258
+ * omitted — #2404), the preset adds the `@takazudo/zudo-doc/plugins/routes`
259
+ * plugin, which injects the doc routes from the package instead of requiring
260
+ * project-shipped `pages/*.tsx` stubs.
260
261
  *
261
- * On by default since the fast-follow (#2372 upstream zfb dev-render support
262
- * landed): the showcase and create-zudo-doc emit `packageOwnedRoutes: true` into
263
- * their generated `settings.ts`. The preset reads this value truthily with no
264
- * central defaulting, so a hand-written settings object that omits the field is
265
- * treated as off. Any injected route whose URL shape collides with a kept
266
- * `pages/` stub is silently dropped (user `pages/` wins), so existing stubs
267
- * are a harmless no-op.
262
+ * Set explicitly to `false` only if your project ships its own `pages/*.tsx`
263
+ * stubs for every doc route. Any injected route whose URL shape collides with
264
+ * a kept `pages/` stub is silently dropped (user `pages/` wins), so existing
265
+ * stubs are a harmless no-op if you later re-enable this.
266
+ *
267
+ * When `false` AND doc content is configured (`docsDir` non-empty and/or
268
+ * locales/versions set), a single `console.warn` is emitted per build to
269
+ * flag a potentially-empty build (#2404). No throw — the build succeeds.
268
270
  */
269
271
  packageOwnedRoutes?: boolean;
270
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
6
6
  "license": "MIT",
@@ -392,6 +392,10 @@
392
392
  "types": "./dist/doc-body-end/index.d.ts",
393
393
  "default": "./dist/doc-body-end/index.js"
394
394
  },
395
+ "./doc-body-end-islands": {
396
+ "types": "./dist/doc-body-end-islands/index.d.ts",
397
+ "default": "./dist/doc-body-end-islands/index.js"
398
+ },
395
399
  "./doc-history-area": {
396
400
  "types": "./dist/doc-history-area/index.d.ts",
397
401
  "default": "./dist/doc-history-area/index.js"
@@ -511,9 +515,9 @@
511
515
  ],
512
516
  "peerDependencies": {
513
517
  "preact": "^10.29.1",
514
- "@takazudo/zfb": "^0.1.0-next.67",
515
- "@takazudo/zfb-runtime": "^0.1.0-next.67",
516
- "@takazudo/zudo-doc-history-server": "^1.0.2",
518
+ "@takazudo/zfb": "^0.1.0-next.70",
519
+ "@takazudo/zfb-runtime": "^0.1.0-next.70",
520
+ "@takazudo/zudo-doc-history-server": "^1.2.0",
517
521
  "@takazudo/zdtp": "^0.3.3",
518
522
  "shiki": "^4.0.2",
519
523
  "zod": "^4.3.6",
@@ -555,8 +559,8 @@
555
559
  "typescript": "^5.0.0",
556
560
  "vitest": "^4.1.0",
557
561
  "zod": "^4.3.6",
558
- "@takazudo/zfb": "0.1.0-next.67",
559
- "@takazudo/zfb-runtime": "0.1.0-next.67"
562
+ "@takazudo/zfb": "0.1.0-next.70",
563
+ "@takazudo/zfb-runtime": "0.1.0-next.70"
560
564
  },
561
565
  "scripts": {
562
566
  "build": "tsup && tsc -p tsconfig.build.json",
@@ -10,7 +10,7 @@ import {
10
10
  HeadWithDefaults,
11
11
  HeaderWithDefaults,
12
12
  FooterWithDefaults,
13
- BodyEndIslandsStub,
13
+ BodyEndIslands,
14
14
  composeMetaTitle,
15
15
  } from "./_chrome.js";
16
16
 
@@ -31,7 +31,7 @@ export default function NotFoundPage(): JSX.Element {
31
31
  sidebarOverride={<></>}
32
32
  headerOverride={<HeaderWithDefaults lang={locale} />}
33
33
  footerOverride={<FooterWithDefaults lang={locale} />}
34
- bodyEndComponents={<BodyEndIslandsStub basePath={settings.base ?? "/"} />}
34
+ bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
35
35
  enableClientRouter={settings.dynamicPageTransition}
36
36
  >
37
37
  <div class="min-h-[60vh] flex flex-col items-center justify-center px-hsp-2xl py-vsp-xl">
@@ -52,6 +52,7 @@ import {
52
52
  firstRoutedHref,
53
53
  collectTags,
54
54
  stableDocs,
55
+ colorSchemes,
55
56
  } from "./_context.js";
56
57
  import type { CategoryMeta, DocNavNode } from "./_docs-helpers.js";
57
58
 
@@ -67,6 +68,7 @@ import { createFooterWithDefaults } from "@takazudo/zudo-doc/footer-with-default
67
68
  import { createSidebarWithDefaults } from "@takazudo/zudo-doc/sidebar-with-defaults";
68
69
  import { createSidebarPrepaint } from "@takazudo/zudo-doc/sidebar-prepaint";
69
70
  import { createDocBodyEnd } from "@takazudo/zudo-doc/doc-body-end";
71
+ import { createBodyEndIslands } from "@takazudo/zudo-doc/doc-body-end-islands";
70
72
  import { createDocPager } from "@takazudo/zudo-doc/doc-pager";
71
73
  import { createDocPageShell } from "@takazudo/zudo-doc/doc-page-shell";
72
74
  import { createDocContentHeader } from "@takazudo/zudo-doc/doc-content-header";
@@ -128,12 +130,12 @@ const docHistoryMeta: Record<string, never> = {};
128
130
  * tree. The project `sidebars` config is host-bound (A2). */
129
131
  const sidebarsConfig: Record<string, never> = {};
130
132
 
131
- /** Package no-op body-end islands. The host's `BodyEndIslands` wires project
132
- * island bootstraps (client-router / design-token-panel) that cannot live in
133
- * the package; the package default renders nothing. */
134
- function BodyEndIslandsStub(_props: { basePath: string }): JSX.Element {
135
- return <></>;
136
- }
133
+ /** Package-default body-end islands the package-island subset of the host's
134
+ * `BodyEndIslands` (AiChatModal / ImageEnlarge / MermaidEnlarge), reconstructed
135
+ * from the serializable virtual-module `settings` flags (#2406 / #2401(c)).
136
+ * The host-owned bootstraps (client-router / design-token-panel) cannot live
137
+ * in the package, so they are NOT included here — A2/S4 re-homes those. */
138
+ const BodyEndIslands = createBodyEndIslands({ settings });
137
139
 
138
140
  /** Package no-op DocHistory island stub — renders an empty fragment (the
139
141
  * `DocHistoryComponent` contract requires a VNode, not null). */
@@ -162,14 +164,38 @@ function SearchWidgetBound(props: {
162
164
 
163
165
  export const composeMetaTitle = createComposeMetaTitle(settings.siteName);
164
166
 
167
+ // Scheme-selection: resolve the active color scheme(s) from the host
168
+ // `colorSchemes` map supplied via the virtual module. Falls back to
169
+ // `DEFAULT_SCHEME` for any missing key or when `colorSchemes` is absent.
170
+ // `generateCssCustomProperties` is called in single-scheme mode
171
+ // (`settings.colorMode` is false); `generateLightDarkCssProperties` in
172
+ // light/dark mode. Both delegate to DEFAULT_SCHEME when `colorSchemes` is null.
173
+ function resolveHostScheme(key: string): ColorScheme {
174
+ if (!colorSchemes) return DEFAULT_SCHEME;
175
+ return colorSchemes[key] ?? DEFAULT_SCHEME;
176
+ }
177
+
165
178
  export const HeadWithDefaults = createHeadWithDefaults({
166
179
  settings,
167
180
  composeMetaTitle,
168
181
  withBase,
169
182
  absoluteUrl,
170
- generateCssCustomProperties: () => generateCssCustomProperties(DEFAULT_SCHEME),
171
- generateLightDarkCssProperties: () =>
172
- generateLightDarkCssProperties(DEFAULT_SCHEME, DEFAULT_SCHEME),
183
+ // Called only in single-scheme mode (colorMode false): resolve via settings.colorScheme.
184
+ generateCssCustomProperties: () =>
185
+ generateCssCustomProperties(resolveHostScheme(settings.colorScheme)),
186
+ // Called only in light/dark mode (colorMode truthy): resolve the pair.
187
+ generateLightDarkCssProperties: () => {
188
+ const cm = settings.colorMode;
189
+ if (cm) {
190
+ return generateLightDarkCssProperties(
191
+ resolveHostScheme(cm.lightScheme),
192
+ resolveHostScheme(cm.darkScheme),
193
+ );
194
+ }
195
+ // Should not be reached (head-with-defaults only calls this when colorMode
196
+ // is truthy), but guard the contract by falling back to DEFAULT_SCHEME.
197
+ return generateLightDarkCssProperties(DEFAULT_SCHEME, DEFAULT_SCHEME);
198
+ },
173
199
  });
174
200
 
175
201
  // ---------------------------------------------------------------------------
@@ -272,7 +298,7 @@ export const SidebarWithDefaults = createSidebarWithDefaults({
272
298
  });
273
299
 
274
300
  const SidebarPrepaint = createSidebarPrepaint({ sidebarToggle: settings.sidebarToggle });
275
- const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands: BodyEndIslandsStub });
301
+ const DocBodyEnd = createDocBodyEnd({ settings, BodyEndIslands });
276
302
  const DocPager = createDocPager({ t });
277
303
 
278
304
  // ---------------------------------------------------------------------------
@@ -495,7 +521,7 @@ export const VersionsPageView = createVersionsPageView({
495
521
  HeadWithDefaults,
496
522
  HeaderWithDefaults,
497
523
  FooterWithDefaults,
498
- BodyEndIslands: BodyEndIslandsStub,
524
+ BodyEndIslands,
499
525
  },
500
526
  });
501
527
 
@@ -513,15 +539,16 @@ const tagPages = createTagPages({
513
539
  HeadWithDefaults,
514
540
  HeaderWithDefaults,
515
541
  FooterWithDefaults,
516
- BodyEndIslands: BodyEndIslandsStub,
542
+ BodyEndIslands,
517
543
  DocHistoryArea: DocHistoryArea as never,
518
544
  },
519
545
  });
520
546
 
521
547
  export const { collectTagMapForLocale, TagDetailPageView, TagsIndexPageView } = tagPages;
522
548
 
523
- // Re-export the locale-aware site-tree wrapper for the index entrypoints.
524
- export { SiteTreeNavWrapper, BodyEndIslandsStub };
549
+ // Re-export the locale-aware site-tree wrapper + the package-default body-end
550
+ // islands for the index entrypoints.
551
+ export { SiteTreeNavWrapper, BodyEndIslands };
525
552
 
526
553
  // Bring the doc-route-entries builder + types into the chrome surface so the
527
554
  // doc entrypoints import a single module.
@@ -34,6 +34,7 @@
34
34
  import { routeContext } from "virtual:zudo-doc-route-context";
35
35
 
36
36
  import type { Settings } from "@takazudo/zudo-doc/settings";
37
+ import type { ColorScheme } from "@takazudo/zudo-doc/color-scheme-utils";
37
38
  import type { FactoryI18n } from "@takazudo/zudo-doc/factory-context";
38
39
  import { makeUrlHelpers, type UrlHelpers } from "@takazudo/zudo-doc/url-helpers";
39
40
  import {
@@ -84,6 +85,7 @@ export interface RouteContextPayload {
84
85
  settings: Settings;
85
86
  translations: Record<string, Record<string, string>>;
86
87
  tagVocabulary: readonly TagVocabularyEntry[];
88
+ colorSchemes: Record<string, ColorScheme> | null;
87
89
  }
88
90
 
89
91
  /** The serializable route-context (from the virtual module). */
@@ -91,6 +93,9 @@ export const ctx = routeContext as unknown as RouteContextPayload;
91
93
  export const settings: Settings = ctx.settings;
92
94
  const translations = ctx.translations;
93
95
  const tagVocabulary = ctx.tagVocabulary;
96
+ /** Host color-scheme palette map. `null` when not supplied — `_chrome.tsx`
97
+ * falls back to `DEFAULT_SCHEME` in that case. */
98
+ export const colorSchemes: Record<string, ColorScheme> | null = ctx.colorSchemes;
94
99
 
95
100
  // ---------------------------------------------------------------------------
96
101
  // i18n — reconstruct a FactoryI18n from settings + translations (Decision 1).
@@ -4,12 +4,16 @@
4
4
  // — so this declaration gives the package route entrypoints a typed import.
5
5
  //
6
6
  // The payload is SERIALIZABLE DATA ONLY (ADR Decision 1): `settings`,
7
- // `translations`, `tagVocabulary`. `_context.ts` narrows it to the concrete
8
- // `RouteContextPayload` at the seam.
7
+ // `translations`, `tagVocabulary`, `colorSchemes`. `_context.ts` narrows it
8
+ // to the concrete `RouteContextPayload` at the seam.
9
9
  declare module "virtual:zudo-doc-route-context" {
10
10
  export const routeContext: {
11
11
  settings: unknown;
12
12
  translations: Record<string, Record<string, string>>;
13
13
  tagVocabulary: ReadonlyArray<Record<string, unknown>>;
14
+ /** Host color-scheme palette map. `null` when the caller did not pass
15
+ * `colorSchemes` to `zudoDocPreset` — `_chrome.tsx` falls back to
16
+ * `DEFAULT_SCHEME` in that case. */
17
+ colorSchemes: Record<string, unknown> | null;
14
18
  };
15
19
  }
@@ -28,7 +28,7 @@ import {
28
28
  HeadWithDefaults,
29
29
  HeaderWithDefaults,
30
30
  FooterWithDefaults,
31
- BodyEndIslandsStub,
31
+ BodyEndIslands,
32
32
  composeMetaTitle,
33
33
  } from "./_chrome.js";
34
34
 
@@ -67,7 +67,7 @@ export default function IndexPage(): JSX.Element {
67
67
  sidebarOverride={<></>}
68
68
  headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase("/")} />}
69
69
  footerOverride={<FooterWithDefaults lang={locale} />}
70
- bodyEndComponents={<BodyEndIslandsStub basePath={settings.base ?? "/"} />}
70
+ bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
71
71
  enableClientRouter={settings.dynamicPageTransition}
72
72
  >
73
73
  <div class="flex justify-center mb-vsp-xl">
@@ -28,7 +28,7 @@ import {
28
28
  HeadWithDefaults,
29
29
  HeaderWithDefaults,
30
30
  FooterWithDefaults,
31
- BodyEndIslandsStub,
31
+ BodyEndIslands,
32
32
  composeMetaTitle,
33
33
  } from "./_chrome.js";
34
34
 
@@ -83,7 +83,7 @@ export default function LocaleIndexPage({ params }: PageArgs): JSX.Element {
83
83
  sidebarOverride={<></>}
84
84
  headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`/${locale}/`)} />}
85
85
  footerOverride={<FooterWithDefaults lang={locale} />}
86
- bodyEndComponents={<BodyEndIslandsStub basePath={settings.base ?? "/"} />}
86
+ bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
87
87
  enableClientRouter={settings.dynamicPageTransition}
88
88
  >
89
89
  <div class="flex justify-center mb-vsp-xl">