@docubook/flame 1.7.2 → 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.docu/components/Context.tsx +1 -1
  2. package/.docu/components/Pagination.tsx +9 -1
  3. package/.docu/components/Search.tsx +6 -6
  4. package/.docu/components/Sidebar.tsx +37 -8
  5. package/.docu/components/Theme.tsx +2 -2
  6. package/.docu/components/Toc.tsx +52 -6
  7. package/.docu/components/Typography.tsx +1 -1
  8. package/.docu/components/home/Hero.tsx +4 -4
  9. package/.docu/components/registry.ts +1 -1
  10. package/.docu/lib/build.deno.js +3 -3
  11. package/.docu/lib/{build.impl-TSIF3F7O.js → build.impl-VXB4KL4D.js} +3 -3
  12. package/.docu/lib/build.node.js +3 -3
  13. package/.docu/lib/{chunk-SPHVBXRR.js → chunk-7PRQ3RQB.js} +6 -6
  14. package/.docu/lib/{chunk-DITXUPUV.js → chunk-A6FIEG3H.js} +61 -10
  15. package/.docu/lib/{chunk-PJIJEPNR.js → chunk-JRERMREW.js} +5 -4
  16. package/.docu/lib/{chunk-KMDGSD57.js → chunk-LZDEWK25.js} +3 -3
  17. package/.docu/lib/{chunk-HRO7ONJQ.js → chunk-MQWWCO6O.js} +70 -27
  18. package/.docu/lib/{chunk-B6LGUADD.js → chunk-TRT6WQZG.js} +346 -183
  19. package/.docu/lib/chunk-UISOJ4RW.js +114 -0
  20. package/.docu/lib/clean.js +1 -1
  21. package/.docu/lib/deploy.deno.js +1 -1
  22. package/.docu/lib/deploy.node.js +1 -1
  23. package/.docu/lib/preview.deno.js +5 -3
  24. package/.docu/lib/preview.node.js +5 -3
  25. package/.docu/lib/server.deno.js +6 -4
  26. package/.docu/lib/server.node.js +6 -4
  27. package/.docu/node/build.impl.ts +75 -26
  28. package/.docu/node/build.ts +70 -17
  29. package/.docu/node/client.ts +64 -27
  30. package/.docu/node/deploy.shared.ts +5 -5
  31. package/.docu/node/deploy.ts +1 -1
  32. package/.docu/node/html.shared.ts +7 -2
  33. package/.docu/node/html.ts +4 -2
  34. package/.docu/node/hydrate.node.ts +69 -4
  35. package/.docu/node/hydrate.ts +50 -1
  36. package/.docu/node/mdx-manifest.d.ts +5 -0
  37. package/.docu/node/mdx.ts +103 -17
  38. package/.docu/node/preview.deno.ts +1 -1
  39. package/.docu/node/preview.impl.ts +3 -3
  40. package/.docu/node/preview.node.ts +1 -1
  41. package/.docu/node/preview.ts +2 -2
  42. package/.docu/node/route.ts +57 -1
  43. package/.docu/node/runtime/bun.ts +28 -0
  44. package/.docu/node/runtime/deno.ts +34 -0
  45. package/.docu/node/runtime/index.ts +4 -0
  46. package/.docu/node/runtime/node.ts +107 -0
  47. package/.docu/node/runtime/types.ts +19 -0
  48. package/.docu/node/search-indexer.ts +3 -2
  49. package/.docu/node/seo.ts +2 -2
  50. package/.docu/node/server-routes.ts +32 -13
  51. package/.docu/node/server.deno.ts +1 -1
  52. package/.docu/node/server.impl.ts +49 -3
  53. package/.docu/node/server.node.ts +1 -1
  54. package/.docu/node/server.ts +45 -1
  55. package/.docu/pages/docs/[[...slug]].tsx +13 -4
  56. package/.docu/pages/index.tsx +1 -1
  57. package/.docu/styles/globals.css +38 -20
  58. package/README.md +7 -48
  59. package/bin/cli.js +25 -7
  60. package/package.json +7 -6
  61. package/template/README.md +7 -48
  62. package/template/docs/getting-started/configuration.mdx +16 -4
  63. package/template/docs/getting-started/overview.mdx +34 -42
  64. package/template/docs/guide/components.mdx +46 -228
  65. package/template/docs/guide/routing.mdx +8 -6
  66. package/template/docs/index.mdx +9 -8
  67. package/template/docu.json +4 -4
@@ -0,0 +1,107 @@
1
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import type { FetchHandler, RuntimeAdapter, ServerHandle, ServerOptions } from "./types";
3
+
4
+ function toWebRequest(req: IncomingMessage, port: number, hostname: string): Request {
5
+ const host = req.headers.host ?? `${hostname}:${port}`;
6
+ const url = `http://${host}${req.url ?? "/"}`;
7
+ const headers = new Headers();
8
+ for (const [key, value] of Object.entries(req.headers)) {
9
+ if (value === undefined) continue;
10
+ if (Array.isArray(value)) {
11
+ for (const v of value) headers.append(key, v);
12
+ } else {
13
+ headers.set(key, value);
14
+ }
15
+ }
16
+ const method = req.method ?? "GET";
17
+ const hasBody = method !== "GET" && method !== "HEAD";
18
+ return new Request(url, {
19
+ method,
20
+ headers,
21
+ // IncomingMessage is an async iterable of Buffer chunks; Request accepts
22
+ // an async iterable body when half-duplex is declared.
23
+ body: hasBody ? (req as unknown as BodyInit) : undefined,
24
+ // @ts-expect-error -- required by undici for streaming request bodies
25
+ duplex: hasBody ? "half" : undefined,
26
+ });
27
+ }
28
+
29
+ async function writeResponse(response: Response, res: ServerResponse): Promise<void> {
30
+ const headers: Record<string, string | string[]> = {};
31
+ const setCookie = response.headers.getSetCookie?.() ?? [];
32
+ response.headers.forEach((value, key) => {
33
+ if (key === "set-cookie") return;
34
+ headers[key] = value;
35
+ });
36
+ if (setCookie.length > 0) headers["set-cookie"] = setCookie;
37
+
38
+ // Flush headers immediately so streaming responses (SSE) reach the client
39
+ // before the first body chunk.
40
+ res.writeHead(response.status, headers);
41
+
42
+ if (!response.body) {
43
+ res.end();
44
+ return;
45
+ }
46
+
47
+ const reader = response.body.getReader();
48
+ try {
49
+ for (;;) {
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+ // Incremental piping — never buffer the full body (SSE streams forever).
53
+ const ok = res.write(value);
54
+ if (!ok) await new Promise<void>((resolve) => res.once("drain", resolve));
55
+ }
56
+ res.end();
57
+ } catch {
58
+ // Client disconnected mid-stream; cancel the source.
59
+ await reader.cancel().catch(() => {});
60
+ res.destroy();
61
+ }
62
+ }
63
+
64
+ export const nodeAdapter: RuntimeAdapter = {
65
+ name: "node",
66
+
67
+ serve(fetch: FetchHandler, options: ServerOptions): Promise<ServerHandle> {
68
+ const hostname = options.hostname ?? "localhost";
69
+
70
+ const server = createServer((req, res) => {
71
+ Promise.resolve()
72
+ .then(() => fetch(toWebRequest(req, options.port, hostname)))
73
+ .then((response) => writeResponse(response, res))
74
+ .catch((err) => {
75
+ console.error(err);
76
+ if (!res.headersSent) {
77
+ res.writeHead(500, { "Content-Type": "text/plain" });
78
+ }
79
+ res.end("Internal Server Error");
80
+ });
81
+ });
82
+
83
+ if (options.idleTimeout !== undefined) {
84
+ server.timeout = options.idleTimeout * 1000;
85
+ // keepAliveTimeout must not undercut idleTimeout or Node closes
86
+ // long-lived SSE connections early.
87
+ server.keepAliveTimeout = options.idleTimeout * 1000;
88
+ }
89
+
90
+ return new Promise((resolve, reject) => {
91
+ server.once("error", reject);
92
+ server.listen(options.port, () => {
93
+ const address = server.address();
94
+ const port = typeof address === "object" && address ? address.port : options.port;
95
+ resolve({
96
+ port,
97
+ hostname,
98
+ stop: () =>
99
+ new Promise<void>((res2, rej2) => {
100
+ server.closeAllConnections?.();
101
+ server.close((err) => (err ? rej2(err) : res2()));
102
+ }),
103
+ });
104
+ });
105
+ });
106
+ },
107
+ };
@@ -0,0 +1,19 @@
1
+ export interface ServerOptions {
2
+ port: number;
3
+ hostname?: string;
4
+ /** Seconds a connection may stay idle before being closed. */
5
+ idleTimeout?: number;
6
+ }
7
+
8
+ export interface ServerHandle {
9
+ port: number;
10
+ hostname: string;
11
+ stop(): void | Promise<void>;
12
+ }
13
+
14
+ export type FetchHandler = (req: Request) => Response | Promise<Response>;
15
+
16
+ export interface RuntimeAdapter {
17
+ name: string;
18
+ serve(fetch: FetchHandler, options: ServerOptions): ServerHandle | Promise<ServerHandle>;
19
+ }
@@ -14,6 +14,7 @@
14
14
  import { readFile, writeFile, mkdir } from "node:fs/promises";
15
15
  import { resolve, join } from "node:path";
16
16
  import { extractFrontmatterWithContent } from "@docubook/core";
17
+ import { frontmatterField } from "./mdx";
17
18
  import { DOCS_DIR, ASSETS_DIR, loadDocuConfig } from "./paths";
18
19
  import { scanMdxFiles, docsHtmlHref } from "./utils";
19
20
 
@@ -74,7 +75,7 @@ export function extractRecords(filePath: string, raw: string): SearchRecord[] {
74
75
  const records: SearchRecord[] = [];
75
76
  const url = docsHtmlHref(`/docs/${filePath}`);
76
77
  const lvl0 = getSectionTitle(filePath);
77
- const lvl1 = frontmatter.title || null;
78
+ const lvl1 = frontmatterField(frontmatter, "title") || null;
78
79
 
79
80
  const hierarchy = {
80
81
  lvl0,
@@ -90,7 +91,7 @@ export function extractRecords(filePath: string, raw: string): SearchRecord[] {
90
91
  records.push({
91
92
  url,
92
93
  hierarchy: { ...hierarchy },
93
- content: frontmatter.description || null,
94
+ content: frontmatterField(frontmatter, "description") || null,
94
95
  type: "lvl1",
95
96
  });
96
97
  }
package/.docu/node/seo.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { DocuConfig } from "./types";
2
+ import { frontmatterField } from "./mdx";
2
3
 
3
4
  export interface SeoMeta {
4
5
  /** Absolute canonical URL */
@@ -27,8 +28,7 @@ export function buildSeoMeta(
27
28
  };
28
29
 
29
30
  // Per-page image from frontmatter, fallback to global default from config
30
- const image =
31
- (typeof frontmatter.image === "string" && frontmatter.image) || config.meta?.ogImage;
31
+ const image = frontmatterField(frontmatter, "image") || config.meta?.ogImage;
32
32
  if (image) {
33
33
  // Resolve using URL constructor — handles absolute, root-relative, and relative paths
34
34
  try {
@@ -1,9 +1,9 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, stat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { readFileSync, statSync } from "node:fs";
4
4
  import React, { type ReactNode } from "react";
5
5
  import { renderToString } from "react-dom/server";
6
- import { compileMdx } from "./mdx";
6
+ import { compileMdx, frontmatterField } from "./mdx";
7
7
  import { getContentType } from "./utils";
8
8
  import { DOCS_DIR, DIST_DIR, PROJECT_ROOT } from "./paths";
9
9
  import { BuildPluginBuilder } from "./plugin-builder";
@@ -44,11 +44,25 @@ function createHtmlResponse(
44
44
  extraScripts: hmrScript(nonce),
45
45
  themeCss: state.inlineThemeCss,
46
46
  depth,
47
+ // 404 pages can be requested at arbitrary depths (e.g. a noLink section
48
+ // path typed in the address bar) — relative asset paths would resolve
49
+ // against the wrong directory and break CSS/JS.
50
+ absoluteAssets: status === 404,
47
51
  });
48
- /** unsafe-eval required by mdx-remote hydration — see build.impl.ts */
52
+ /** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */
49
53
  return htmlResponse(html, nonce, status, true);
50
54
  }
51
55
 
56
+ // Dev-only memo: compileMdx per request is ~70ms; repeat navigations with
57
+ // an unchanged file (same mtime) reuse the compiled result. The dev server
58
+ // has no other per-page cache — without this every navigation re-parses MDX.
59
+ // ponytail: unbounded Map is fine — docs sites have a handful of pages; cap
60
+ // with an LRU only if a project exceeds thousands of routes.
61
+ const docsCache = new Map<
62
+ string,
63
+ { mtimeMs: number; doc: NonNullable<Awaited<ReturnType<typeof getDocsForSlug>>> }
64
+ >();
65
+
52
66
  async function getDocsForSlug(
53
67
  slug: string,
54
68
  state: ServerState
@@ -92,6 +106,12 @@ async function getDocsForSlug(
92
106
 
93
107
  const relPath = filePath.replace(PROJECT_ROOT + "/", "");
94
108
 
109
+ const mtimeMs = (await stat(filePath)).mtimeMs;
110
+ const cached = docsCache.get(relPath);
111
+ if (cached && cached.mtimeMs === mtimeMs) {
112
+ return cached.doc;
113
+ }
114
+
95
115
  let content = raw;
96
116
  if (state.builder) {
97
117
  const transformed = await state.builder.runOnLoad(relPath, content);
@@ -113,7 +133,7 @@ async function getDocsForSlug(
113
133
  });
114
134
  }
115
135
 
116
- return {
136
+ const doc = {
117
137
  content: result.content,
118
138
  compiledSource: result.compiledSource,
119
139
  frontmatter,
@@ -121,6 +141,8 @@ async function getDocsForSlug(
121
141
  filePath: relPath,
122
142
  resolvedContent: content,
123
143
  };
144
+ docsCache.set(relPath, { mtimeMs, doc });
145
+ return doc;
124
146
  }
125
147
 
126
148
  async function renderDocsServerPage(
@@ -129,12 +151,8 @@ async function renderDocsServerPage(
129
151
  pathname: string,
130
152
  state: ServerState
131
153
  ): Promise<Response> {
132
- const title =
133
- (typeof doc.frontmatter.title === "string" ? doc.frontmatter.title : "") ||
134
- slug.join("/") ||
135
- "Docs";
136
- const description =
137
- typeof doc.frontmatter.description === "string" ? doc.frontmatter.description : "";
154
+ const title = frontmatterField(doc.frontmatter, "title") || slug.join("/") || "Docs";
155
+ const description = frontmatterField(doc.frontmatter, "description");
138
156
 
139
157
  const page = React.createElement(
140
158
  DocsLayout,
@@ -143,8 +161,9 @@ async function renderDocsServerPage(
143
161
  slug,
144
162
  title,
145
163
  description,
146
- date: doc.frontmatter.date as string | undefined,
147
- content: doc.content,
164
+ date: frontmatterField(doc.frontmatter, "date") || undefined,
165
+ // Same root-relative SSR as build — see build.ts.
166
+ content: renderToString(doc.content),
148
167
  tocs: doc.tocs,
149
168
  filePath: doc.filePath,
150
169
  repoUrl: state.docuConfig.repo?.url,
@@ -184,7 +203,7 @@ async function renderDocsServerPage(
184
203
  depth,
185
204
  });
186
205
  html = await state.builder.runTransformHtmlChain(html, ctx);
187
- /** unsafe-eval required by mdx-remote hydration — see build.impl.ts */
206
+ /** Dev-only: serves compiledSource → MDXRemote eval path (no CSP in production). */
188
207
  return htmlResponse(html, nonce, 200, true);
189
208
  }
190
209
 
@@ -1,4 +1,4 @@
1
- import { denoAdapter } from "@docubook/runt";
1
+ import { denoAdapter } from "./runtime";
2
2
  import { runServer } from "./server.impl";
3
3
 
4
4
  await runServer(denoAdapter);
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Runtime-neutral dev server — mirror of `server.ts` (Bun-only, protected)
3
- * driven by a `RuntimeAdapter` from `@docubook/runt` instead of `Bun.serve`,
3
+ * driven by a `RuntimeAdapter` (./runtime) instead of `Bun.serve`,
4
4
  * with manual route matching instead of `Bun.FileSystemRouter`. The page set
5
5
  * is static (`/`, `/docs/[[...slug]]`, `/404`), so a router is unnecessary.
6
6
  */
7
7
 
8
- import { watch } from "node:fs";
9
- import type { RuntimeAdapter, ServerHandle } from "@docubook/runt";
8
+ import { watch, existsSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { createRequire } from "node:module";
11
+ import type { RuntimeAdapter, ServerHandle } from "./runtime";
10
12
  import { DOCS_DIR, loadDocuConfig } from "./paths";
11
13
  import { loadPlugins } from "./plugin-loader";
12
14
  import { BuildPluginBuilder } from "./plugin-builder";
@@ -72,6 +74,18 @@ export async function runServer(adapter: RuntimeAdapter): Promise<ServerHandle>
72
74
 
73
75
  const hmrClients = new Set<ReadableStreamDefaultController>();
74
76
 
77
+ const require = createRequire(import.meta.url);
78
+ /** Locate the installed @docubook/markdown source dir (workspace link), if any. */
79
+ function resolveMdxContentSrc(): string | null {
80
+ try {
81
+ const pkgDir = dirname(require.resolve("@docubook/markdown/package.json"));
82
+ const src = join(pkgDir, "src");
83
+ return existsSync(src) ? src : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
75
89
  let hmrTimeout: ReturnType<typeof setTimeout> | null = null;
76
90
  const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
77
91
  if (!filename || (!filename.endsWith(".mdx") && !filename.endsWith(".md"))) return;
@@ -87,12 +101,44 @@ export async function runServer(adapter: RuntimeAdapter): Promise<ServerHandle>
87
101
  }, 300);
88
102
  });
89
103
 
104
+ // Rebuild the client bundle when component sources change (e.g. the
105
+ // @docubook/markdown workspace package) so interactive islands pick up
106
+ // edits, then reload connected clients. MDX edits are handled by the watcher
107
+ // above — no rebuild needed, content recompiles per request.
108
+ let srcHmrTimeout: ReturnType<typeof setTimeout> | null = null;
109
+ const mdxContentSrc = resolveMdxContentSrc();
110
+ const srcWatcher = mdxContentSrc
111
+ ? watch(mdxContentSrc, { recursive: true }, (_event, filename) => {
112
+ if (!filename || !/\.(ts|tsx|js|jsx|css)$/.test(filename)) return;
113
+ if (srcHmrTimeout) clearTimeout(srcHmrTimeout);
114
+ srcHmrTimeout = setTimeout(async () => {
115
+ try {
116
+ state.assetManifest = await buildClientBundle();
117
+ logger.warn("[hmr] client bundle rebuilt (component change)");
118
+ } catch (e) {
119
+ logger.warn(
120
+ `[hmr] bundle rebuild failed: ${e instanceof Error ? e.message : String(e)}`
121
+ );
122
+ }
123
+ for (const client of [...hmrClients]) {
124
+ try {
125
+ client.enqueue(new TextEncoder().encode("data: reload\n\n"));
126
+ } catch {
127
+ hmrClients.delete(client);
128
+ }
129
+ }
130
+ }, 300);
131
+ })
132
+ : null;
133
+
90
134
  process.on("SIGINT", () => {
91
135
  watcher.close();
136
+ srcWatcher?.close();
92
137
  process.exit(0);
93
138
  });
94
139
  process.on("SIGTERM", () => {
95
140
  watcher.close();
141
+ srcWatcher?.close();
96
142
  process.exit(0);
97
143
  });
98
144
 
@@ -1,4 +1,4 @@
1
- import { nodeAdapter } from "@docubook/runt";
1
+ import { nodeAdapter } from "./runtime";
2
2
  import { runServer } from "./server.impl";
3
3
 
4
4
  await runServer(nodeAdapter);
@@ -1,4 +1,6 @@
1
- import { watch } from "node:fs";
1
+ import { watch, existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { createRequire } from "node:module";
2
4
  import { DOCS_DIR, PAGES_DIR, loadDocuConfig } from "./paths";
3
5
  import { loadPlugins } from "./plugin-loader";
4
6
  import { BuildPluginBuilder } from "./plugin-builder";
@@ -73,6 +75,18 @@ try {
73
75
 
74
76
  const hmrClients = new Set<ReadableStreamDefaultController>();
75
77
 
78
+ const require = createRequire(import.meta.url);
79
+ /** Locate the installed @docubook/markdown source dir (workspace link), if any. */
80
+ function resolveMdxContentSrc(): string | null {
81
+ try {
82
+ const pkgDir = dirname(require.resolve("@docubook/markdown/package.json"));
83
+ const src = join(pkgDir, "src");
84
+ return existsSync(src) ? src : null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
76
90
  let hmrTimeout: ReturnType<typeof setTimeout> | null = null;
77
91
  const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
78
92
  if (!filename || (!filename.endsWith(".mdx") && !filename.endsWith(".md"))) return;
@@ -88,12 +102,42 @@ const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
88
102
  }, 300);
89
103
  });
90
104
 
105
+ // Rebuild the client bundle when component sources change (e.g. the
106
+ // @docubook/markdown workspace package) so interactive islands pick up
107
+ // edits, then reload connected clients. MDX edits are handled by the watcher
108
+ // above — no rebuild needed, content recompiles per request.
109
+ let srcHmrTimeout: ReturnType<typeof setTimeout> | null = null;
110
+ const mdxContentSrc = resolveMdxContentSrc();
111
+ const srcWatcher = mdxContentSrc
112
+ ? watch(mdxContentSrc, { recursive: true }, (_event, filename) => {
113
+ if (!filename || !/\.(ts|tsx|js|jsx|css)$/.test(filename)) return;
114
+ if (srcHmrTimeout) clearTimeout(srcHmrTimeout);
115
+ srcHmrTimeout = setTimeout(async () => {
116
+ try {
117
+ state.assetManifest = await buildClientBundle();
118
+ logger.warn("[hmr] client bundle rebuilt (component change)");
119
+ } catch (e) {
120
+ logger.warn(`[hmr] bundle rebuild failed: ${e instanceof Error ? e.message : String(e)}`);
121
+ }
122
+ for (const client of [...hmrClients]) {
123
+ try {
124
+ client.enqueue(new TextEncoder().encode("data: reload\n\n"));
125
+ } catch {
126
+ hmrClients.delete(client);
127
+ }
128
+ }
129
+ }, 300);
130
+ })
131
+ : null;
132
+
91
133
  process.on("SIGINT", () => {
92
134
  watcher.close();
135
+ srcWatcher?.close();
93
136
  process.exit(0);
94
137
  });
95
138
  process.on("SIGTERM", () => {
96
139
  watcher.close();
140
+ srcWatcher?.close();
97
141
  process.exit(0);
98
142
  });
99
143
 
@@ -1,4 +1,3 @@
1
- import type { ReactNode } from "react";
2
1
  import { ChevronLeft, ChevronRight } from "lucide-react";
3
2
  import DocsBreadcrumb from "../../components/Breadcrumb";
4
3
  import Pagination from "../../components/Pagination";
@@ -14,10 +13,15 @@ interface DocsPageProps {
14
13
  title: string;
15
14
  description?: string;
16
15
  date?: string;
17
- content: ReactNode;
16
+ /** SSR'd MDX content HTML — rendered as its own root so client hydration
17
+ * (separate island root) matches useId-based ids. */
18
+ content: string;
18
19
  tocs: TocItem[];
19
20
  filePath: string;
20
21
  repoUrl?: string;
22
+ /** Build-time slug keying into the bundled mdxModules manifest (client.ts). */
23
+ mdxSlug?: string;
24
+ /** Dev-only pre-compiled source for the legacy MDXRemote eval path. */
21
25
  compiledSource?: string;
22
26
  }
23
27
 
@@ -30,6 +34,7 @@ export default function DocsPage({
30
34
  tocs,
31
35
  filePath,
32
36
  repoUrl,
37
+ mdxSlug,
33
38
  compiledSource,
34
39
  }: DocsPageProps) {
35
40
  const pathname = slug.join("/");
@@ -39,7 +44,7 @@ export default function DocsPage({
39
44
  <div className="flex w-full flex-1 px-0 pb-4 lg:h-[calc(100vh-4rem)] lg:px-8 lg:pb-8">
40
45
  <div
41
46
  id="scroll-container"
42
- className="bg-base-100 border-base-300 max-lg:scroll-p-54 relative flex w-full flex-col items-start rounded-b-3xl border shadow-md lg:h-full lg:flex-row lg:overflow-y-auto lg:rounded-xl"
47
+ className="bg-base-100 border-base-300 relative flex w-full flex-col items-start rounded-b-3xl border shadow-md max-lg:scroll-p-54 lg:h-full lg:flex-row lg:overflow-y-auto lg:rounded-xl"
43
48
  >
44
49
  {/* Mobile bar - island */}
45
50
  <div
@@ -60,7 +65,11 @@ export default function DocsPage({
60
65
  {description && (
61
66
  <p className="text-muted-foreground -mt-4 text-[16.5px]">{description}</p>
62
67
  )}
63
- <div id="mdx-content-island">{content}</div>
68
+ <div
69
+ id="mdx-content-island"
70
+ data-mdx-slug={mdxSlug}
71
+ dangerouslySetInnerHTML={{ __html: content }}
72
+ />
64
73
  {compiledSource && (
65
74
  <script
66
75
  id="mdx-compiled-source"
@@ -61,7 +61,7 @@ export default function IndexPage() {
61
61
 
62
62
  return (
63
63
  <div className="bg-base-100 relative isolate min-h-screen overflow-hidden">
64
- <div className="absolute right-4 top-4 z-10" id="theme-island" />
64
+ <div className="absolute top-4 right-4 z-10" id="theme-island" />
65
65
 
66
66
  {/* Background gradient blobs */}
67
67
  <div
@@ -1,5 +1,5 @@
1
1
  @import "tailwindcss";
2
- @import "@docubook/mdx-content/styles.css";
2
+ @import "@docubook/markdown/styles.css";
3
3
 
4
4
  @plugin "daisyui" {
5
5
  themes:
@@ -9,16 +9,31 @@
9
9
  @plugin "@tailwindcss/typography";
10
10
  @source "../../.docu/components";
11
11
  @source "../../.docu/pages";
12
- @source inline("breadcrumbs collapse collapse-open collapse-close collapse-arrow collapse-plus collapse-title collapse-content");
12
+ @source "../../../ui-react/src";
13
+
14
+ /* daisyUI breadcrumbs underline every li child on hover (including plain
15
+ spans) — crumbs are not links, so strip the underline (and pointer cursor)
16
+ for non-anchor items; real anchors keep daisyUI's hover feedback. */
17
+ .breadcrumbs > ul > li > :not(a):hover {
18
+ text-decoration-line: none;
19
+ cursor: default;
20
+ }
21
+ @source inline(
22
+ "breadcrumbs collapse collapse-open collapse-close collapse-arrow collapse-plus collapse-title collapse-content"
23
+ );
13
24
  @source inline("modal modal-top modal-middle modal-bottom modal-box modal-backdrop");
14
25
  @source inline("drawer drawer-end drawer-toggle drawer-side drawer-overlay drawer-content");
15
26
  @source inline("sm:drawer-open md:drawer-open lg:drawer-open xl:drawer-open");
16
27
  @source inline("navbar menu menu-horizontal");
17
28
  @source inline("kbd kbd-xs kbd-sm kbd-md kbd-lg kbd-xl");
18
29
  @source inline("toggle theme-controller toggle-xs toggle-sm toggle-md toggle-lg");
19
- @source inline("toggle-primary toggle-secondary toggle-accent toggle-neutral toggle-success toggle-warning toggle-info toggle-error");
30
+ @source inline(
31
+ "toggle-primary toggle-secondary toggle-accent toggle-neutral toggle-success toggle-warning toggle-info toggle-error"
32
+ );
20
33
  @source inline("input input-ghost input-xs input-sm input-md input-lg input-xl");
21
- @source inline("input-primary input-secondary input-accent input-neutral input-success input-warning input-info input-error");
34
+ @source inline(
35
+ "input-primary input-secondary input-accent input-neutral input-success input-warning input-info input-error"
36
+ );
22
37
  @source inline("label");
23
38
 
24
39
  @custom-variant dark (&:is(.dark *));
@@ -43,6 +58,9 @@
43
58
  --color-muted-foreground: hsl(var(--muted-foreground));
44
59
  --color-accent: hsl(var(--accent));
45
60
  --color-accent-foreground: hsl(var(--accent-foreground));
61
+ --color-primary-content: hsl(var(--primary-foreground));
62
+ --color-secondary-content: hsl(var(--secondary-foreground));
63
+ --color-accent-content: hsl(var(--accent-foreground));
46
64
  --color-popover: hsl(var(--popover));
47
65
  --color-popover-foreground: hsl(var(--popover-foreground));
48
66
  --color-card: hsl(var(--card));
@@ -76,27 +94,27 @@
76
94
  @layer base {
77
95
  :root {
78
96
  --base-100: 100% 0 0;
79
- --base-200: 98% 0 2;
80
- --base-300: 95% 0 4;
81
- --base-content: 222% 47% 11%;
82
- --background: 210 40% 98%;
97
+ --base-200: 97% 0.002 260;
98
+ --base-300: 92% 0.004 260;
99
+ --base-content: 22% 0.03 260;
100
+ --background: 0 0% 100%;
83
101
  --foreground: 220 30% 15%;
84
102
  --card: 0 0% 100%;
85
103
  --card-foreground: 220 30% 15%;
86
104
  --popover: 0 0% 100%;
87
105
  --popover-foreground: 220 30% 15%;
88
106
  --primary: 210 81% 56%;
89
- --primary-foreground: 0 0% 100%;
90
- --secondary: 210 30% 90%;
107
+ --primary-foreground: 210 40% 98%;
108
+ --secondary: 210 20% 94%;
91
109
  --secondary-foreground: 220 30% 15%;
92
- --muted: 210 20% 92%;
93
- --muted-foreground: 220 15% 50%;
110
+ --muted: 210 20% 94%;
111
+ --muted-foreground: 220 15% 38%;
94
112
  --accent: 200 100% 40%;
95
- --accent-foreground: 0 0% 100%;
113
+ --accent-foreground: 200 40% 98%;
96
114
  --destructive: 0 85% 60%;
97
- --destructive-foreground: 0 0% 100%;
98
- --border-color: 210 20% 85%;
99
- --input: 210 20% 85%;
115
+ --destructive-foreground: 210 40% 98%;
116
+ --border-color: 210 20% 88%;
117
+ --input: 210 20% 88%;
100
118
  --ring: 210 81% 56%;
101
119
  --radius: 0.5rem;
102
120
  }
@@ -113,15 +131,15 @@
113
131
  --popover: 225 18% 14%;
114
132
  --popover-foreground: 210 20% 93%;
115
133
  --primary: 210 100% 67%;
116
- --primary-foreground: 225 20% 11%;
134
+ --primary-foreground: 210 0% 10%;
117
135
  --secondary: 220 15% 18%;
118
136
  --secondary-foreground: 210 20% 90%;
119
137
  --muted: 220 12% 20%;
120
138
  --muted-foreground: 215 12% 58%;
121
139
  --accent: 200 100% 62%;
122
- --accent-foreground: 0 0% 100%;
140
+ --accent-foreground: 200 0% 10%;
123
141
  --destructive: 0 80% 65%;
124
- --destructive-foreground: 0 0% 100%;
142
+ --destructive-foreground: 0 0% 10%;
125
143
  --border-color: 220 10% 28%;
126
144
  --input: 220 10% 28%;
127
145
  --ring: 210 100% 67%;
@@ -195,7 +213,7 @@
195
213
  }
196
214
 
197
215
  .rehype-code-title {
198
- @apply -mb-8 mt-5 w-full px-2 pb-5 text-sm font-medium;
216
+ @apply mt-5 -mb-8 w-full px-2 pb-5 text-sm font-medium;
199
217
  }
200
218
 
201
219
  .highlight-comp > code {