@farming-labs/theme 0.0.2-beta.10

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 (41) hide show
  1. package/dist/_virtual/_rolldown/runtime.mjs +7 -0
  2. package/dist/ai-search-dialog.d.mts +37 -0
  3. package/dist/ai-search-dialog.mjs +937 -0
  4. package/dist/darksharp/index.d.mts +97 -0
  5. package/dist/darksharp/index.mjs +111 -0
  6. package/dist/default/index.d.mts +97 -0
  7. package/dist/default/index.mjs +110 -0
  8. package/dist/docs-ai-features.d.mts +23 -0
  9. package/dist/docs-ai-features.mjs +81 -0
  10. package/dist/docs-api.d.mts +68 -0
  11. package/dist/docs-api.mjs +204 -0
  12. package/dist/docs-layout.d.mts +33 -0
  13. package/dist/docs-layout.mjs +331 -0
  14. package/dist/docs-page-client.d.mts +46 -0
  15. package/dist/docs-page-client.mjs +128 -0
  16. package/dist/index.d.mts +11 -0
  17. package/dist/index.mjs +12 -0
  18. package/dist/mdx.d.mts +38 -0
  19. package/dist/mdx.mjs +27 -0
  20. package/dist/page-actions.d.mts +21 -0
  21. package/dist/page-actions.mjs +155 -0
  22. package/dist/pixel-border/index.d.mts +87 -0
  23. package/dist/pixel-border/index.mjs +95 -0
  24. package/dist/provider.d.mts +14 -0
  25. package/dist/provider.mjs +29 -0
  26. package/dist/search.d.mts +34 -0
  27. package/dist/search.mjs +36 -0
  28. package/dist/serialize-icon.d.mts +4 -0
  29. package/dist/serialize-icon.mjs +16 -0
  30. package/dist/theme.d.mts +2 -0
  31. package/dist/theme.mjs +3 -0
  32. package/package.json +90 -0
  33. package/styles/ai.css +894 -0
  34. package/styles/base.css +298 -0
  35. package/styles/darksharp.css +433 -0
  36. package/styles/default.css +88 -0
  37. package/styles/fumadocs.css +2 -0
  38. package/styles/pixel-border.css +671 -0
  39. package/styles/presets/base.css +14 -0
  40. package/styles/presets/black.css +14 -0
  41. package/styles/presets/neutral.css +14 -0
@@ -0,0 +1,204 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ import { createSearchAPI } from "fumadocs-core/search/server";
5
+
6
+ //#region src/docs-api.ts
7
+ /**
8
+ * Unified docs API handler for @farming-labs/theme.
9
+ *
10
+ * A single route handler that serves **both** search and AI chat:
11
+ *
12
+ * - `GET /api/docs?query=…` → full-text search over indexed MDX pages
13
+ * - `POST /api/docs` → RAG-powered "Ask AI" (searches relevant docs,
14
+ * then streams an LLM response using the docs as context)
15
+ *
16
+ * This replaces the old `createDocsSearchAPI` — one handler, one route.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * // app/api/docs/route.ts (auto-generated by withDocs)
21
+ * import { createDocsAPI } from "@farming-labs/theme/api";
22
+ * export const { GET, POST } = createDocsAPI();
23
+ * export const revalidate = false;
24
+ * ```
25
+ */
26
+ const FILE_EXTS = [
27
+ "tsx",
28
+ "ts",
29
+ "jsx",
30
+ "js"
31
+ ];
32
+ function readEntry(root) {
33
+ for (const ext of FILE_EXTS) {
34
+ const configPath = path.join(root, `docs.config.${ext}`);
35
+ if (fs.existsSync(configPath)) try {
36
+ const match = fs.readFileSync(configPath, "utf-8").match(/entry\s*:\s*["']([^"']+)["']/);
37
+ if (match) return match[1];
38
+ } catch {}
39
+ }
40
+ return "docs";
41
+ }
42
+ /**
43
+ * Read AI config from docs.config by parsing the file for the `ai` block.
44
+ * This avoids importing the config (which may use JSX/React).
45
+ */
46
+ function readAIConfig(root) {
47
+ for (const ext of FILE_EXTS) {
48
+ const configPath = path.join(root, `docs.config.${ext}`);
49
+ if (fs.existsSync(configPath)) try {
50
+ const content = fs.readFileSync(configPath, "utf-8");
51
+ if (!content.includes("ai:") && !content.includes("ai :")) return {};
52
+ const enabledMatch = content.match(/ai\s*:\s*\{[^}]*enabled\s*:\s*(true|false)/s);
53
+ if (enabledMatch && enabledMatch[1] === "false") return {};
54
+ const modelMatch = content.match(/ai\s*:\s*\{[^}]*model\s*:\s*["']([^"']+)["']/s);
55
+ const baseUrlMatch = content.match(/ai\s*:\s*\{[^}]*baseUrl\s*:\s*["']([^"']+)["']/s);
56
+ const apiKeyMatch = content.match(/ai\s*:\s*\{[^}]*apiKey\s*:\s*process\.env\.(\w+)/s);
57
+ const maxResultsMatch = content.match(/ai\s*:\s*\{[^}]*maxResults\s*:\s*(\d+)/s);
58
+ const systemPromptMatch = content.match(/ai\s*:\s*\{[^}]*systemPrompt\s*:\s*["'`]([^"'`]+)["'`]/s);
59
+ return {
60
+ enabled: true,
61
+ model: modelMatch?.[1],
62
+ baseUrl: baseUrlMatch?.[1],
63
+ apiKey: apiKeyMatch?.[1] ? process.env[apiKeyMatch[1]] : void 0,
64
+ maxResults: maxResultsMatch ? parseInt(maxResultsMatch[1], 10) : void 0,
65
+ systemPrompt: systemPromptMatch?.[1]
66
+ };
67
+ } catch {}
68
+ }
69
+ return {};
70
+ }
71
+ function stripMdx(raw) {
72
+ const { content } = matter(raw);
73
+ return content.replace(/^(import|export)\s.*$/gm, "").replace(/<[^>]+\/>/g, "").replace(/<\/?[A-Z][^>]*>/g, "").replace(/<\/?[a-z][^>]*>/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2").replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
74
+ }
75
+ function scanDocsDir(docsDir, entry) {
76
+ const indexes = [];
77
+ function scan(dir, slugParts) {
78
+ if (!fs.existsSync(dir)) return;
79
+ const pagePath = path.join(dir, "page.mdx");
80
+ if (fs.existsSync(pagePath)) try {
81
+ const raw = fs.readFileSync(pagePath, "utf-8");
82
+ const { data } = matter(raw);
83
+ const title = data.title || slugParts[slugParts.length - 1]?.replace(/-/g, " ") || "Documentation";
84
+ const description = data.description;
85
+ const content = stripMdx(raw);
86
+ const url = slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`;
87
+ indexes.push({
88
+ title,
89
+ description,
90
+ content,
91
+ url
92
+ });
93
+ } catch {}
94
+ let entries;
95
+ try {
96
+ entries = fs.readdirSync(dir);
97
+ } catch {
98
+ return;
99
+ }
100
+ for (const name of entries.sort()) {
101
+ const full = path.join(dir, name);
102
+ try {
103
+ if (fs.statSync(full).isDirectory()) scan(full, [...slugParts, name]);
104
+ } catch {}
105
+ }
106
+ }
107
+ scan(docsDir, []);
108
+ return indexes;
109
+ }
110
+ const DEFAULT_SYSTEM_PROMPT = `You are a helpful documentation assistant. Answer questions based on the provided documentation context. Be concise and accurate. If the answer is not in the context, say so honestly. Use markdown formatting for code examples and links.`;
111
+ async function handleAskAI(request, indexes, searchServer, aiConfig) {
112
+ const apiKey = aiConfig.apiKey ?? process.env.OPENAI_API_KEY;
113
+ if (!apiKey) return Response.json({ error: `AI is enabled but no API key was found. Either set apiKey in your docs.config or add OPENAI_API_KEY to your .env.local file.` }, { status: 500 });
114
+ let body;
115
+ try {
116
+ body = await request.json();
117
+ } catch {
118
+ return Response.json({ error: "Invalid JSON body. Expected { messages: [...] }" }, { status: 400 });
119
+ }
120
+ const messages = body.messages;
121
+ if (!Array.isArray(messages) || messages.length === 0) return Response.json({ error: "messages array is required and must not be empty." }, { status: 400 });
122
+ const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
123
+ if (!lastUserMessage) return Response.json({ error: "At least one user message is required." }, { status: 400 });
124
+ const maxResults = aiConfig.maxResults ?? 5;
125
+ const query = lastUserMessage.content;
126
+ const context = indexes.map((doc) => {
127
+ const q = query.toLowerCase();
128
+ const titleMatch = doc.title.toLowerCase().includes(q) ? 10 : 0;
129
+ const contentMatch = q.split(/\s+/).reduce((score, word) => {
130
+ return score + (doc.content.toLowerCase().includes(word) ? 1 : 0);
131
+ }, 0);
132
+ return {
133
+ ...doc,
134
+ score: titleMatch + contentMatch
135
+ };
136
+ }).filter((d) => d.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults).map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`).join("\n\n---\n\n");
137
+ const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
138
+ const llmMessages = [{
139
+ role: "system",
140
+ content: context ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${context}` : systemPrompt
141
+ }, ...messages.filter((m) => m.role !== "system")];
142
+ const baseUrl = (aiConfig.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
143
+ const model = aiConfig.model ?? "gpt-4o-mini";
144
+ const llmResponse = await fetch(`${baseUrl}/chat/completions`, {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ Authorization: `Bearer ${apiKey}`
149
+ },
150
+ body: JSON.stringify({
151
+ model,
152
+ stream: true,
153
+ messages: llmMessages
154
+ })
155
+ });
156
+ if (!llmResponse.ok) {
157
+ const errText = await llmResponse.text().catch(() => "Unknown error");
158
+ return Response.json({ error: `LLM API error (${llmResponse.status}): ${errText}` }, { status: 502 });
159
+ }
160
+ return new Response(llmResponse.body, { headers: {
161
+ "Content-Type": "text/event-stream",
162
+ "Cache-Control": "no-cache",
163
+ Connection: "keep-alive"
164
+ } });
165
+ }
166
+ /**
167
+ * Create a unified docs API route handler.
168
+ *
169
+ * Returns `{ GET, POST }` for use in a Next.js route handler:
170
+ * - **GET** → full-text search (same as the old `createDocsSearchAPI`)
171
+ * - **POST** → AI-powered chat with RAG (when AI is enabled in config)
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * // app/api/docs/route.ts
176
+ * import { createDocsAPI } from "@farming-labs/theme/api";
177
+ * export const { GET, POST } = createDocsAPI();
178
+ * export const revalidate = false;
179
+ * ```
180
+ *
181
+ * @param options - Optional overrides (entry, language, ai config)
182
+ */
183
+ function createDocsAPI(options) {
184
+ const root = process.cwd();
185
+ const entry = options?.entry ?? readEntry(root);
186
+ const docsDir = path.join(root, "app", entry);
187
+ const language = options?.language ?? "english";
188
+ const aiConfig = options?.ai ?? readAIConfig(root);
189
+ const indexes = scanDocsDir(docsDir, entry);
190
+ const searchAPI = createSearchAPI("simple", {
191
+ language,
192
+ indexes
193
+ });
194
+ return {
195
+ GET: searchAPI.GET,
196
+ async POST(request) {
197
+ if (!aiConfig.enabled) return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
198
+ return handleAskAI(request, indexes, searchAPI, aiConfig);
199
+ }
200
+ };
201
+ }
202
+
203
+ //#endregion
204
+ export { createDocsAPI };
@@ -0,0 +1,33 @@
1
+ import { ReactNode } from "react";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
+ import { DocsConfig } from "@farming-labs/docs";
4
+
5
+ //#region src/docs-layout.d.ts
6
+ /**
7
+ * Build a Next.js Metadata object from the docs config.
8
+ *
9
+ * Returns layout-level metadata including `title.template` so each page's
10
+ * frontmatter `title` is formatted (e.g. "Getting Started – Docs").
11
+ *
12
+ * Usage in `app/docs/layout.tsx`:
13
+ * ```ts
14
+ * export const metadata = createDocsMetadata(docsConfig);
15
+ * ```
16
+ */
17
+ declare function createDocsMetadata(config: DocsConfig): {
18
+ twitter?: {
19
+ card: "summary" | "summary_large_image";
20
+ } | undefined;
21
+ description?: string | undefined;
22
+ title: {
23
+ template: string;
24
+ default: string;
25
+ };
26
+ };
27
+ declare function createDocsLayout(config: DocsConfig): ({
28
+ children
29
+ }: {
30
+ children: ReactNode;
31
+ }) => react_jsx_runtime0.JSX.Element;
32
+ //#endregion
33
+ export { createDocsLayout, createDocsMetadata };
@@ -0,0 +1,331 @@
1
+ import { DocsAIFeatures } from "./docs-ai-features.mjs";
2
+ import { serializeIcon } from "./serialize-icon.mjs";
3
+ import { DocsPageClient } from "./docs-page-client.mjs";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import matter from "gray-matter";
8
+ import { DocsLayout } from "fumadocs-ui/layouts/docs";
9
+
10
+ //#region src/docs-layout.tsx
11
+ /** Resolve a frontmatter `icon` string to a ReactNode via the icon registry. */
12
+ function resolveIcon(iconKey, registry) {
13
+ if (!iconKey || !registry) return void 0;
14
+ return registry[iconKey] ?? void 0;
15
+ }
16
+ /** Read frontmatter from a page.mdx file. */
17
+ function readFrontmatter(filePath) {
18
+ try {
19
+ const { data } = matter(fs.readFileSync(filePath, "utf-8"));
20
+ return data;
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+ /** Check if a directory has any subdirectories that contain page.mdx. */
26
+ function hasChildPages(dir) {
27
+ if (!fs.existsSync(dir)) return false;
28
+ for (const name of fs.readdirSync(dir)) {
29
+ const full = path.join(dir, name);
30
+ if (fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, "page.mdx"))) return true;
31
+ }
32
+ return false;
33
+ }
34
+ function buildTree(config) {
35
+ const docsDir = path.join(process.cwd(), "app", config.entry);
36
+ const icons = config.icons;
37
+ const rootChildren = [];
38
+ if (fs.existsSync(path.join(docsDir, "page.mdx"))) {
39
+ const data = readFrontmatter(path.join(docsDir, "page.mdx"));
40
+ rootChildren.push({
41
+ type: "page",
42
+ name: data.title ?? "Documentation",
43
+ url: `/${config.entry}`,
44
+ icon: resolveIcon(data.icon, icons)
45
+ });
46
+ }
47
+ /**
48
+ * Recursively scan a directory and return tree nodes.
49
+ *
50
+ * - If a subdirectory has its own children (nested pages), it becomes a
51
+ * **folder** node with collapsible children. Its own `page.mdx` becomes
52
+ * the folder's `index` page.
53
+ * - Otherwise it becomes a simple **page** node.
54
+ */
55
+ function scan(dir, baseSlug) {
56
+ if (!fs.existsSync(dir)) return [];
57
+ const nodes = [];
58
+ const entries = fs.readdirSync(dir).sort();
59
+ for (const name of entries) {
60
+ const full = path.join(dir, name);
61
+ if (!fs.statSync(full).isDirectory()) continue;
62
+ const pagePath = path.join(full, "page.mdx");
63
+ if (!fs.existsSync(pagePath)) continue;
64
+ const data = readFrontmatter(pagePath);
65
+ const slug = [...baseSlug, name];
66
+ const url = `/${config.entry}/${slug.join("/")}`;
67
+ const icon = resolveIcon(data.icon, icons);
68
+ const displayName = data.title ?? name.replace(/-/g, " ");
69
+ if (hasChildPages(full)) {
70
+ const folderChildren = scan(full, slug);
71
+ nodes.push({
72
+ type: "folder",
73
+ name: displayName,
74
+ icon,
75
+ index: {
76
+ type: "page",
77
+ name: displayName,
78
+ url,
79
+ icon
80
+ },
81
+ children: folderChildren
82
+ });
83
+ } else nodes.push({
84
+ type: "page",
85
+ name: displayName,
86
+ url,
87
+ icon
88
+ });
89
+ }
90
+ return nodes;
91
+ }
92
+ rootChildren.push(...scan(docsDir, []));
93
+ return {
94
+ name: "Docs",
95
+ children: rootChildren
96
+ };
97
+ }
98
+ /**
99
+ * Scan all page.mdx files under the docs entry directory and build
100
+ * a map of URL pathname → formatted last-modified date string.
101
+ */
102
+ function buildLastModifiedMap(entry) {
103
+ const docsDir = path.join(process.cwd(), "app", entry);
104
+ const map = {};
105
+ function formatDate(date) {
106
+ return date.toLocaleDateString("en-US", {
107
+ year: "numeric",
108
+ month: "long",
109
+ day: "numeric"
110
+ });
111
+ }
112
+ function scan(dir, slugParts) {
113
+ if (!fs.existsSync(dir)) return;
114
+ const pagePath = path.join(dir, "page.mdx");
115
+ if (fs.existsSync(pagePath)) {
116
+ const url = slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`;
117
+ map[url] = formatDate(fs.statSync(pagePath).mtime);
118
+ }
119
+ for (const name of fs.readdirSync(dir)) {
120
+ const full = path.join(dir, name);
121
+ if (fs.statSync(full).isDirectory()) scan(full, [...slugParts, name]);
122
+ }
123
+ }
124
+ scan(docsDir, []);
125
+ return map;
126
+ }
127
+ /**
128
+ * Build a Next.js Metadata object from the docs config.
129
+ *
130
+ * Returns layout-level metadata including `title.template` so each page's
131
+ * frontmatter `title` is formatted (e.g. "Getting Started – Docs").
132
+ *
133
+ * Usage in `app/docs/layout.tsx`:
134
+ * ```ts
135
+ * export const metadata = createDocsMetadata(docsConfig);
136
+ * ```
137
+ */
138
+ function createDocsMetadata(config) {
139
+ const meta = config.metadata;
140
+ const template = meta?.titleTemplate ?? "%s";
141
+ return {
142
+ title: {
143
+ template,
144
+ default: template.replace("%s", "").replace(/^[\s–—-]+/, "").trim() || "Docs"
145
+ },
146
+ ...meta?.description ? { description: meta.description } : {},
147
+ ...meta?.twitterCard ? { twitter: { card: meta.twitterCard } } : {}
148
+ };
149
+ }
150
+ /** Resolve the themeToggle config into fumadocs-ui's `themeSwitch` prop. */
151
+ function resolveThemeSwitch(toggle) {
152
+ if (toggle === void 0 || toggle === true) return { enabled: true };
153
+ if (toggle === false) return { enabled: false };
154
+ return {
155
+ enabled: toggle.enabled !== false,
156
+ mode: toggle.mode
157
+ };
158
+ }
159
+ /** Resolve sidebar config. */
160
+ function resolveSidebar(sidebar) {
161
+ if (sidebar === void 0 || sidebar === true) return {};
162
+ if (sidebar === false) return { enabled: false };
163
+ return {
164
+ enabled: sidebar.enabled !== false,
165
+ component: sidebar.component,
166
+ footer: sidebar.footer,
167
+ banner: sidebar.banner,
168
+ collapsible: sidebar.collapsible
169
+ };
170
+ }
171
+ const COLOR_MAP = {
172
+ primary: "--color-fd-primary",
173
+ primaryForeground: "--color-fd-primary-foreground",
174
+ background: "--color-fd-background",
175
+ foreground: "--color-fd-foreground",
176
+ muted: "--color-fd-muted",
177
+ mutedForeground: "--color-fd-muted-foreground",
178
+ border: "--color-fd-border",
179
+ card: "--color-fd-card",
180
+ cardForeground: "--color-fd-card-foreground",
181
+ accent: "--color-fd-accent",
182
+ accentForeground: "--color-fd-accent-foreground",
183
+ popover: "--color-fd-popover",
184
+ popoverForeground: "--color-fd-popover-foreground",
185
+ secondary: "--color-fd-secondary",
186
+ secondaryForeground: "--color-fd-secondary-foreground",
187
+ ring: "--color-fd-ring"
188
+ };
189
+ function buildColorsCSS(colors) {
190
+ if (!colors) return "";
191
+ const vars = [];
192
+ for (const [key, value] of Object.entries(colors)) {
193
+ if (!value || !COLOR_MAP[key]) continue;
194
+ vars.push(`${COLOR_MAP[key]}: ${value};`);
195
+ }
196
+ if (vars.length === 0) return "";
197
+ return `:root, .dark {\n ${vars.join("\n ")}\n}`;
198
+ }
199
+ function ColorStyle({ colors }) {
200
+ const css = buildColorsCSS(colors);
201
+ if (!css) return null;
202
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } });
203
+ }
204
+ function buildFontStyleVars(prefix, style) {
205
+ if (!style) return "";
206
+ const parts = [];
207
+ if (style.size) parts.push(`${prefix}-size: ${style.size};`);
208
+ if (style.weight != null) parts.push(`${prefix}-weight: ${style.weight};`);
209
+ if (style.lineHeight) parts.push(`${prefix}-line-height: ${style.lineHeight};`);
210
+ if (style.letterSpacing) parts.push(`${prefix}-letter-spacing: ${style.letterSpacing};`);
211
+ return parts.join("\n ");
212
+ }
213
+ function buildTypographyCSS(typo) {
214
+ if (!typo?.font) return "";
215
+ const vars = [];
216
+ const fontStyle = typo.font.style;
217
+ if (fontStyle?.sans) vars.push(`--fd-font-sans: ${fontStyle.sans};`);
218
+ if (fontStyle?.mono) vars.push(`--fd-font-mono: ${fontStyle.mono};`);
219
+ for (const el of [
220
+ "h1",
221
+ "h2",
222
+ "h3",
223
+ "h4",
224
+ "body",
225
+ "small"
226
+ ]) {
227
+ const style = typo.font[el];
228
+ if (style) {
229
+ const cssVars = buildFontStyleVars(`--fd-${el}`, style);
230
+ if (cssVars) vars.push(cssVars);
231
+ }
232
+ }
233
+ if (vars.length === 0) return "";
234
+ return `:root {\n ${vars.join("\n ")}\n}`;
235
+ }
236
+ function TypographyStyle({ typography }) {
237
+ const css = buildTypographyCSS(typography);
238
+ if (!css) return null;
239
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } });
240
+ }
241
+ function createDocsLayout(config) {
242
+ const tocEnabled = (config.theme?.ui?.layout?.toc)?.enabled !== false;
243
+ const navTitle = config.nav?.title ?? "Docs";
244
+ const navUrl = config.nav?.url ?? `/${config.entry}`;
245
+ const themeSwitch = resolveThemeSwitch(config.themeToggle);
246
+ const toggleConfig = typeof config.themeToggle === "object" ? config.themeToggle : void 0;
247
+ const forcedTheme = themeSwitch.enabled === false && toggleConfig?.default && toggleConfig.default !== "system" ? toggleConfig.default : void 0;
248
+ const sidebarProps = resolveSidebar(config.sidebar);
249
+ const breadcrumbConfig = config.breadcrumb;
250
+ const breadcrumbEnabled = breadcrumbConfig === void 0 || breadcrumbConfig === true || typeof breadcrumbConfig === "object" && breadcrumbConfig.enabled !== false;
251
+ const colors = config.theme?._userColorOverrides;
252
+ const typography = config.theme?.ui?.typography;
253
+ const pageActions = config.pageActions;
254
+ const copyMarkdownEnabled = resolveBool(pageActions?.copyMarkdown);
255
+ const openDocsEnabled = resolveBool(pageActions?.openDocs);
256
+ const pageActionsPosition = pageActions?.position ?? "below-title";
257
+ const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
258
+ name: p.name,
259
+ urlTemplate: p.urlTemplate,
260
+ iconHtml: p.icon ? serializeIcon(p.icon) : void 0
261
+ }));
262
+ const githubRaw = config.github;
263
+ const githubUrl = typeof githubRaw === "string" ? githubRaw.replace(/\/$/, "") : githubRaw?.url.replace(/\/$/, "");
264
+ const githubBranch = typeof githubRaw === "object" ? githubRaw.branch ?? "main" : "main";
265
+ const githubDirectory = typeof githubRaw === "object" ? githubRaw.directory?.replace(/^\/|\/$/g, "") : void 0;
266
+ const aiConfig = config.ai;
267
+ const aiEnabled = !!aiConfig?.enabled;
268
+ const aiMode = aiConfig?.mode ?? "search";
269
+ const aiPosition = aiConfig?.position ?? "bottom-right";
270
+ const aiFloatingStyle = aiConfig?.floatingStyle ?? "panel";
271
+ const aiTriggerComponentHtml = aiConfig?.triggerComponent ? serializeIcon(aiConfig.triggerComponent) : void 0;
272
+ const aiSuggestedQuestions = aiConfig?.suggestedQuestions;
273
+ const aiLabel = aiConfig?.aiLabel;
274
+ const aiLoadingComponentHtml = typeof aiConfig?.loadingComponent === "function" ? serializeIcon(aiConfig.loadingComponent({ name: aiLabel || "AI" })) : void 0;
275
+ const lastModifiedMap = buildLastModifiedMap(config.entry);
276
+ return function DocsLayoutWrapper({ children }) {
277
+ return /* @__PURE__ */ jsxs(DocsLayout, {
278
+ tree: buildTree(config),
279
+ nav: {
280
+ title: navTitle,
281
+ url: navUrl
282
+ },
283
+ themeSwitch,
284
+ sidebar: sidebarProps,
285
+ children: [
286
+ /* @__PURE__ */ jsx(ColorStyle, { colors }),
287
+ /* @__PURE__ */ jsx(TypographyStyle, { typography }),
288
+ forcedTheme && /* @__PURE__ */ jsx(ForcedThemeScript, { theme: forcedTheme }),
289
+ aiEnabled && /* @__PURE__ */ jsx(DocsAIFeatures, {
290
+ mode: aiMode,
291
+ position: aiPosition,
292
+ floatingStyle: aiFloatingStyle,
293
+ triggerComponentHtml: aiTriggerComponentHtml,
294
+ suggestedQuestions: aiSuggestedQuestions,
295
+ aiLabel,
296
+ loadingComponentHtml: aiLoadingComponentHtml
297
+ }),
298
+ /* @__PURE__ */ jsx(DocsPageClient, {
299
+ tocEnabled,
300
+ breadcrumbEnabled,
301
+ entry: config.entry,
302
+ copyMarkdown: copyMarkdownEnabled,
303
+ openDocs: openDocsEnabled,
304
+ openDocsProviders,
305
+ pageActionsPosition,
306
+ githubUrl,
307
+ githubBranch,
308
+ githubDirectory,
309
+ lastModifiedMap,
310
+ children
311
+ })
312
+ ]
313
+ });
314
+ };
315
+ }
316
+ /** Resolve `boolean | { enabled?: boolean }` to a simple boolean. */
317
+ function resolveBool(v) {
318
+ if (v === void 0) return false;
319
+ if (typeof v === "boolean") return v;
320
+ return v.enabled !== false;
321
+ }
322
+ /**
323
+ * Tiny inline script to force a theme when the toggle is hidden.
324
+ * Sets the class on <html> before React hydrates to avoid FOUC.
325
+ */
326
+ function ForcedThemeScript({ theme }) {
327
+ return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: `document.documentElement.classList.remove('light','dark');document.documentElement.classList.add('${theme}');` } });
328
+ }
329
+
330
+ //#endregion
331
+ export { createDocsLayout, createDocsMetadata };
@@ -0,0 +1,46 @@
1
+ import { ReactNode } from "react";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
+
4
+ //#region src/docs-page-client.d.ts
5
+ /** Serializable provider — icon is an HTML string, not JSX. */
6
+ interface SerializedProvider {
7
+ name: string;
8
+ iconHtml?: string;
9
+ urlTemplate: string;
10
+ }
11
+ interface DocsPageClientProps {
12
+ tocEnabled: boolean;
13
+ breadcrumbEnabled?: boolean;
14
+ /** The docs entry folder name (e.g. "docs") — used to strip from breadcrumb */
15
+ entry?: string;
16
+ copyMarkdown?: boolean;
17
+ openDocs?: boolean;
18
+ openDocsProviders?: SerializedProvider[];
19
+ /** Where to render page actions relative to the title */
20
+ pageActionsPosition?: "above-title" | "below-title";
21
+ /** GitHub repository URL (e.g. "https://github.com/user/repo") */
22
+ githubUrl?: string;
23
+ /** GitHub branch name @default "main" */
24
+ githubBranch?: string;
25
+ /** Subdirectory in the repo where the docs site lives (for monorepos) */
26
+ githubDirectory?: string;
27
+ /** Map of pathname → formatted last-modified date string */
28
+ lastModifiedMap?: Record<string, string>;
29
+ children: ReactNode;
30
+ }
31
+ declare function DocsPageClient({
32
+ tocEnabled,
33
+ breadcrumbEnabled,
34
+ entry,
35
+ copyMarkdown,
36
+ openDocs,
37
+ openDocsProviders,
38
+ pageActionsPosition,
39
+ githubUrl,
40
+ githubBranch,
41
+ githubDirectory,
42
+ lastModifiedMap,
43
+ children
44
+ }: DocsPageClientProps): react_jsx_runtime0.JSX.Element;
45
+ //#endregion
46
+ export { DocsPageClient };