@farming-labs/theme 0.0.2-beta.9 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ import { createTheme } from "@farming-labs/docs";
2
+
3
+ //#region src/darkbold/index.ts
4
+ /**
5
+ * DarkBold theme preset.
6
+ * Pure monochrome design, Geist typography, clean minimalism.
7
+ *
8
+ * CSS: `@import "@farming-labs/theme/darkbold/css";`
9
+ */
10
+ const DarkBoldUIDefaults = {
11
+ colors: {
12
+ primary: "#000",
13
+ background: "#fff",
14
+ muted: "#666",
15
+ border: "#eaeaea"
16
+ },
17
+ typography: { font: {
18
+ style: {
19
+ sans: "Geist, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
20
+ mono: "Geist Mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace"
21
+ },
22
+ h1: {
23
+ size: "2.5rem",
24
+ weight: 600,
25
+ lineHeight: "1.2",
26
+ letterSpacing: "-0.06em"
27
+ },
28
+ h2: {
29
+ size: "2rem",
30
+ weight: 600,
31
+ lineHeight: "1.25",
32
+ letterSpacing: "-0.04em"
33
+ },
34
+ h3: {
35
+ size: "1.5rem",
36
+ weight: 600,
37
+ lineHeight: "1.3",
38
+ letterSpacing: "-0.02em"
39
+ },
40
+ h4: {
41
+ size: "1.25rem",
42
+ weight: 600,
43
+ lineHeight: "1.4"
44
+ },
45
+ body: {
46
+ size: "1rem",
47
+ weight: 400,
48
+ lineHeight: "1.6"
49
+ },
50
+ small: {
51
+ size: "0.875rem",
52
+ weight: 400,
53
+ lineHeight: "1.5"
54
+ }
55
+ } },
56
+ layout: {
57
+ contentWidth: 768,
58
+ sidebarWidth: 260,
59
+ toc: {
60
+ enabled: true,
61
+ depth: 3,
62
+ style: "default"
63
+ },
64
+ header: {
65
+ height: 64,
66
+ sticky: true
67
+ }
68
+ },
69
+ components: {
70
+ Callout: {
71
+ variant: "soft",
72
+ icon: true
73
+ },
74
+ CodeBlock: { showCopyButton: true },
75
+ Tabs: { style: "default" }
76
+ }
77
+ };
78
+ const darkbold = createTheme({
79
+ name: "darkbold",
80
+ ui: DarkBoldUIDefaults
81
+ });
82
+
83
+ //#endregion
84
+ export { DarkBoldUIDefaults, darkbold };
@@ -2,12 +2,13 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
2
 
3
3
  //#region src/docs-ai-features.d.ts
4
4
  interface DocsAIFeaturesProps {
5
- mode: "search" | "floating";
5
+ mode: "search" | "floating" | "sidebar-icon";
6
6
  position?: "bottom-right" | "bottom-left" | "bottom-center";
7
7
  floatingStyle?: "panel" | "modal" | "popover" | "full-modal";
8
8
  triggerComponentHtml?: string;
9
9
  suggestedQuestions?: string[];
10
10
  aiLabel?: string;
11
+ loaderVariant?: string;
11
12
  loadingComponentHtml?: string;
12
13
  }
13
14
  declare function DocsAIFeatures({
@@ -17,6 +18,7 @@ declare function DocsAIFeatures({
17
18
  triggerComponentHtml,
18
19
  suggestedQuestions,
19
20
  aiLabel,
21
+ loaderVariant,
20
22
  loadingComponentHtml
21
23
  }: DocsAIFeaturesProps): react_jsx_runtime0.JSX.Element;
22
24
  //#endregion
@@ -1,26 +1,35 @@
1
1
  "use client";
2
2
 
3
- import { DocsSearchDialog, FloatingAIChat } from "./ai-search-dialog.mjs";
3
+ import { AIModalDialog, DocsSearchDialog, FloatingAIChat } from "./ai-search-dialog.mjs";
4
4
  import { useEffect, useState } from "react";
5
- import { jsx } from "react/jsx-runtime";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
 
7
7
  //#region src/docs-ai-features.tsx
8
8
  /**
9
9
  * Client component injected by `createDocsLayout` when `ai` is configured.
10
10
  *
11
- * Handles both modes:
11
+ * Handles multiple modes:
12
12
  * - "search": Intercepts Cmd+K / Ctrl+K and opens the custom search dialog
13
13
  * with Search + Ask AI tabs (prevents fumadocs' default dialog from opening).
14
14
  * - "floating": Renders the floating chat widget with configurable position,
15
15
  * style, and trigger component.
16
+ * - "sidebar-icon": Injects an AI trigger icon button next to the search bar
17
+ * in the sidebar header area (Mintlify-style).
16
18
  *
17
19
  * This component is rendered inside the docs layout so the user's root layout
18
20
  * never needs to be modified — AI features work purely from `docs.config.tsx`.
19
21
  */
20
- function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loadingComponentHtml }) {
22
+ function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
21
23
  if (mode === "search") return /* @__PURE__ */ jsx(SearchModeAI, {
22
24
  suggestedQuestions,
23
25
  aiLabel,
26
+ loaderVariant,
27
+ loadingComponentHtml
28
+ });
29
+ if (mode === "sidebar-icon") return /* @__PURE__ */ jsx(SidebarIconModeAI, {
30
+ suggestedQuestions,
31
+ aiLabel,
32
+ loaderVariant,
24
33
  loadingComponentHtml
25
34
  });
26
35
  return /* @__PURE__ */ jsx(FloatingAIChat, {
@@ -30,15 +39,11 @@ function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "pane
30
39
  triggerComponentHtml,
31
40
  suggestedQuestions,
32
41
  aiLabel,
42
+ loaderVariant,
33
43
  loadingComponentHtml
34
44
  });
35
45
  }
36
- /**
37
- * Search mode: intercepts Cmd+K / Ctrl+K globally and opens the
38
- * custom search dialog (with Search + Ask AI tabs) instead of
39
- * fumadocs' built-in search dialog.
40
- */
41
- function SearchModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }) {
46
+ function SearchModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
42
47
  const [open, setOpen] = useState(false);
43
48
  useEffect(() => {
44
49
  function handler(e) {
@@ -73,9 +78,57 @@ function SearchModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }) {
73
78
  api: "/api/docs",
74
79
  suggestedQuestions,
75
80
  aiLabel,
81
+ loaderVariant,
76
82
  loadingComponentHtml
77
83
  });
78
84
  }
85
+ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
86
+ const [searchOpen, setSearchOpen] = useState(false);
87
+ const [aiOpen, setAiOpen] = useState(false);
88
+ useEffect(() => {
89
+ function handler(e) {
90
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
91
+ e.preventDefault();
92
+ e.stopPropagation();
93
+ e.stopImmediatePropagation();
94
+ setSearchOpen(true);
95
+ }
96
+ }
97
+ document.addEventListener("keydown", handler, true);
98
+ return () => document.removeEventListener("keydown", handler, true);
99
+ }, []);
100
+ useEffect(() => {
101
+ function onSearch() {
102
+ setSearchOpen(true);
103
+ }
104
+ function onAI() {
105
+ setAiOpen(true);
106
+ }
107
+ window.addEventListener("fd-open-search", onSearch);
108
+ window.addEventListener("fd-open-ai", onAI);
109
+ return () => {
110
+ window.removeEventListener("fd-open-search", onSearch);
111
+ window.removeEventListener("fd-open-ai", onAI);
112
+ };
113
+ }, []);
114
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DocsSearchDialog, {
115
+ open: searchOpen,
116
+ onOpenChange: setSearchOpen,
117
+ api: "/api/docs",
118
+ suggestedQuestions,
119
+ aiLabel,
120
+ loaderVariant,
121
+ loadingComponentHtml
122
+ }), /* @__PURE__ */ jsx(AIModalDialog, {
123
+ open: aiOpen,
124
+ onOpenChange: setAiOpen,
125
+ api: "/api/docs",
126
+ suggestedQuestions,
127
+ aiLabel,
128
+ loaderVariant,
129
+ loadingComponentHtml
130
+ })] });
131
+ }
79
132
 
80
133
  //#endregion
81
134
  export { DocsAIFeatures };
@@ -38,12 +38,14 @@ interface DocsAPIOptions {
38
38
  * Create a unified docs API route handler.
39
39
  *
40
40
  * Returns `{ GET, POST }` for use in a Next.js route handler:
41
- * - **GET** → full-text search (same as the old `createDocsSearchAPI`)
42
- * - **POST** AI-powered chat with RAG (when AI is enabled in config)
41
+ * - **GET ?query=…** → full-text search
42
+ * - **GET ?format=llms** llms.txt (concise page listing)
43
+ * - **GET ?format=llms-full** → llms-full.txt (full page content)
44
+ * - **POST** → AI-powered chat with RAG
43
45
  *
44
46
  * @example
45
47
  * ```ts
46
- * // app/api/docs/route.ts
48
+ * // app/api/docs/route.ts (auto-generated by withDocs)
47
49
  * import { createDocsAPI } from "@farming-labs/theme/api";
48
50
  * export const { GET, POST } = createDocsAPI();
49
51
  * export const revalidate = false;
@@ -53,10 +55,9 @@ interface DocsAPIOptions {
53
55
  */
54
56
  declare function createDocsAPI(options?: DocsAPIOptions): {
55
57
  /**
56
- * GET handler — full-text search.
57
- * Query: `?query=search+term`
58
+ * GET handler — search, llms.txt, or llms-full.txt depending on query params.
58
59
  */
59
- GET: (request: Request) => Promise<Response>;
60
+ GET(request: Request): Response | Promise<Response>;
60
61
  /**
61
62
  * POST handler — AI chat with RAG.
62
63
  * Body: `{ messages: [{ role: "user", content: "How do I …?" }] }`
package/dist/docs-api.mjs CHANGED
@@ -163,16 +163,64 @@ async function handleAskAI(request, indexes, searchServer, aiConfig) {
163
163
  Connection: "keep-alive"
164
164
  } });
165
165
  }
166
+ function readLlmsTxtConfig(root) {
167
+ for (const ext of FILE_EXTS) {
168
+ const configPath = path.join(root, `docs.config.${ext}`);
169
+ if (fs.existsSync(configPath)) try {
170
+ const content = fs.readFileSync(configPath, "utf-8");
171
+ if (!content.includes("llmsTxt")) return { enabled: false };
172
+ if (/llmsTxt\s*:\s*true/.test(content)) return { enabled: true };
173
+ const enabledMatch = content.match(/llmsTxt\s*:\s*\{[^}]*enabled\s*:\s*(true|false)/s);
174
+ if (enabledMatch && enabledMatch[1] === "false") return { enabled: false };
175
+ const baseUrlMatch = content.match(/llmsTxt\s*:\s*\{[^}]*baseUrl\s*:\s*["']([^"']+)["']/s);
176
+ const siteTitleMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteTitle\s*:\s*["']([^"']+)["']/s);
177
+ const siteDescMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteDescription\s*:\s*["']([^"']+)["']/s);
178
+ const navTitleMatch = content.match(/nav\s*:\s*\{[^}]*title\s*:\s*["']([^"']+)["']/s);
179
+ return {
180
+ enabled: true,
181
+ baseUrl: baseUrlMatch?.[1],
182
+ siteTitle: siteTitleMatch?.[1] ?? navTitleMatch?.[1],
183
+ siteDescription: siteDescMatch?.[1]
184
+ };
185
+ } catch {}
186
+ }
187
+ return { enabled: false };
188
+ }
189
+ function generateLlmsTxt(indexes, options) {
190
+ const { siteTitle = "Documentation", siteDescription, baseUrl = "" } = options;
191
+ let llmsTxt = `# ${siteTitle}\n\n`;
192
+ if (siteDescription) llmsTxt += `> ${siteDescription}\n\n`;
193
+ llmsTxt += `## Pages\n\n`;
194
+ for (const page of indexes) {
195
+ llmsTxt += `- [${page.title}](${baseUrl}${page.url})`;
196
+ if (page.description) llmsTxt += `: ${page.description}`;
197
+ llmsTxt += `\n`;
198
+ }
199
+ let llmsFullTxt = `# ${siteTitle}\n\n`;
200
+ if (siteDescription) llmsFullTxt += `> ${siteDescription}\n\n`;
201
+ for (const page of indexes) {
202
+ llmsFullTxt += `## ${page.title}\n\n`;
203
+ llmsFullTxt += `URL: ${baseUrl}${page.url}\n\n`;
204
+ if (page.description) llmsFullTxt += `${page.description}\n\n`;
205
+ llmsFullTxt += `${page.content}\n\n---\n\n`;
206
+ }
207
+ return {
208
+ llmsTxt,
209
+ llmsFullTxt
210
+ };
211
+ }
166
212
  /**
167
213
  * Create a unified docs API route handler.
168
214
  *
169
215
  * 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)
216
+ * - **GET ?query=…** → full-text search
217
+ * - **GET ?format=llms** llms.txt (concise page listing)
218
+ * - **GET ?format=llms-full** → llms-full.txt (full page content)
219
+ * - **POST** → AI-powered chat with RAG
172
220
  *
173
221
  * @example
174
222
  * ```ts
175
- * // app/api/docs/route.ts
223
+ * // app/api/docs/route.ts (auto-generated by withDocs)
176
224
  * import { createDocsAPI } from "@farming-labs/theme/api";
177
225
  * export const { GET, POST } = createDocsAPI();
178
226
  * export const revalidate = false;
@@ -186,13 +234,34 @@ function createDocsAPI(options) {
186
234
  const docsDir = path.join(root, "app", entry);
187
235
  const language = options?.language ?? "english";
188
236
  const aiConfig = options?.ai ?? readAIConfig(root);
237
+ const llmsConfig = readLlmsTxtConfig(root);
189
238
  const indexes = scanDocsDir(docsDir, entry);
239
+ let _llmsCache = null;
240
+ function getLlmsContent() {
241
+ if (!_llmsCache) _llmsCache = generateLlmsTxt(indexes, {
242
+ siteTitle: llmsConfig.siteTitle ?? "Documentation",
243
+ siteDescription: llmsConfig.siteDescription,
244
+ baseUrl: llmsConfig.baseUrl ?? ""
245
+ });
246
+ return _llmsCache;
247
+ }
190
248
  const searchAPI = createSearchAPI("simple", {
191
249
  language,
192
250
  indexes
193
251
  });
194
252
  return {
195
- GET: searchAPI.GET,
253
+ GET(request) {
254
+ const format = new URL(request.url).searchParams.get("format");
255
+ if (format === "llms") return new Response(getLlmsContent().llmsTxt, { headers: {
256
+ "Content-Type": "text/plain; charset=utf-8",
257
+ "Cache-Control": "public, max-age=3600"
258
+ } });
259
+ if (format === "llms-full") return new Response(getLlmsContent().llmsFullTxt, { headers: {
260
+ "Content-Type": "text/plain; charset=utf-8",
261
+ "Cache-Control": "public, max-age=3600"
262
+ } });
263
+ return searchAPI.GET(request);
264
+ },
196
265
  async POST(request) {
197
266
  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
267
  return handleAskAI(request, indexes, searchAPI, aiConfig);
@@ -0,0 +1,10 @@
1
+ //#region src/docs-command-search.d.ts
2
+ /**
3
+ * Built-in docs search command palette.
4
+ * Intercepts Cmd+K and sidebar search button to provide an advanced
5
+ * fuzzy-search experience. Styled entirely via omni-* CSS classes
6
+ * so each theme provides its own visual variant.
7
+ */
8
+ declare function DocsCommandSearch(): any;
9
+ //#endregion
10
+ export { DocsCommandSearch };