@farming-labs/docs 0.0.2-beta.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.
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,610 @@
1
+ #!/usr/bin/env node
2
+ import pc from "picocolors";
3
+ import path from "node:path";
4
+ import * as p from "@clack/prompts";
5
+ import fs from "node:fs";
6
+ import { execSync, spawn } from "node:child_process";
7
+
8
+ //#region src/cli/utils.ts
9
+ function detectFramework(cwd) {
10
+ const pkgPath = path.join(cwd, "package.json");
11
+ if (!fs.existsSync(pkgPath)) return null;
12
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
13
+ if ({
14
+ ...pkg.dependencies,
15
+ ...pkg.devDependencies
16
+ }["next"]) return "nextjs";
17
+ return null;
18
+ }
19
+ function detectPackageManager(cwd) {
20
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
21
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
22
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
23
+ return "npm";
24
+ }
25
+ function installCommand(pm) {
26
+ return pm === "yarn" ? "yarn add" : `${pm} add`;
27
+ }
28
+ function devInstallCommand(pm) {
29
+ if (pm === "yarn") return "yarn add -D";
30
+ if (pm === "npm") return "npm install -D";
31
+ return `${pm} add -D`;
32
+ }
33
+ /**
34
+ * Write a file, creating parent directories as needed.
35
+ * Returns true if the file was written, false if it already existed and was skipped.
36
+ */
37
+ function writeFileSafe(filePath, content, overwrite = false) {
38
+ if (fs.existsSync(filePath) && !overwrite) return false;
39
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
40
+ fs.writeFileSync(filePath, content, "utf-8");
41
+ return true;
42
+ }
43
+ /**
44
+ * Check if a file exists.
45
+ */
46
+ function fileExists(filePath) {
47
+ return fs.existsSync(filePath);
48
+ }
49
+ /**
50
+ * Read a file, returning null if it does not exist.
51
+ */
52
+ function readFileSafe(filePath) {
53
+ if (!fs.existsSync(filePath)) return null;
54
+ return fs.readFileSync(filePath, "utf-8");
55
+ }
56
+ /**
57
+ * Run a shell command synchronously, inheriting stdio.
58
+ */
59
+ function exec(command, cwd) {
60
+ execSync(command, {
61
+ cwd,
62
+ stdio: "inherit"
63
+ });
64
+ }
65
+ /**
66
+ * Spawn a process and wait for a specific string in stdout,
67
+ * then resolve with the child process (still running).
68
+ */
69
+ function spawnAndWaitFor(command, args, cwd, waitFor, timeoutMs = 6e4) {
70
+ return new Promise((resolve, reject) => {
71
+ const child = spawn(command, args, {
72
+ cwd,
73
+ stdio: [
74
+ "ignore",
75
+ "pipe",
76
+ "pipe"
77
+ ],
78
+ shell: true
79
+ });
80
+ let output = "";
81
+ const timer = setTimeout(() => {
82
+ child.kill();
83
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for "${waitFor}" after ${timeoutMs}ms`));
84
+ }, timeoutMs);
85
+ child.stdout?.on("data", (data) => {
86
+ const text = data.toString();
87
+ output += text;
88
+ process.stdout.write(text);
89
+ if (output.includes(waitFor)) {
90
+ clearTimeout(timer);
91
+ resolve(child);
92
+ }
93
+ });
94
+ child.stderr?.on("data", (data) => {
95
+ process.stderr.write(data.toString());
96
+ });
97
+ child.on("error", (err) => {
98
+ clearTimeout(timer);
99
+ reject(err);
100
+ });
101
+ child.on("close", (code) => {
102
+ clearTimeout(timer);
103
+ if (!output.includes(waitFor)) reject(/* @__PURE__ */ new Error(`Process exited with code ${code} before "${waitFor}" appeared`));
104
+ });
105
+ });
106
+ }
107
+
108
+ //#endregion
109
+ //#region src/cli/templates.ts
110
+ function docsConfigTemplate(cfg) {
111
+ return `\
112
+ import { defineDocs } from "@farming-labs/docs";
113
+ import { fumadocs } from "@farming-labs/fumadocs";
114
+
115
+ export default defineDocs({
116
+ entry: "${cfg.entry}",
117
+ theme: fumadocs({
118
+ ui: {
119
+ colors: { primary: "#6366f1" },
120
+ },
121
+ }),
122
+
123
+ metadata: {
124
+ titleTemplate: "%s – ${cfg.projectName}",
125
+ description: "Documentation for ${cfg.projectName}",
126
+ },
127
+ });
128
+ `;
129
+ }
130
+ function nextConfigTemplate() {
131
+ return `\
132
+ import { withDocs } from "@farming-labs/next/config";
133
+
134
+ export default withDocs();
135
+ `;
136
+ }
137
+ function nextConfigMergedTemplate(existingContent) {
138
+ if (existingContent.includes("withDocs")) return existingContent;
139
+ const lines = existingContent.split("\n");
140
+ const importLine = "import { withDocs } from \"@farming-labs/next/config\";";
141
+ const exportIdx = lines.findIndex((l) => l.match(/export\s+default/));
142
+ if (exportIdx === -1) return `${importLine}\n\n${existingContent}\n\nexport default withDocs();\n`;
143
+ const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("import ") ? i : acc, -1);
144
+ if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
145
+ else lines.unshift(importLine, "");
146
+ const adjustedExportIdx = exportIdx + (lastImportIdx >= 0 && exportIdx > lastImportIdx ? 1 : 0);
147
+ const simpleMatch = lines[adjustedExportIdx].match(/^(\s*export\s+default\s+)(.*?)(;?\s*)$/);
148
+ if (simpleMatch) {
149
+ const [, prefix, value, suffix] = simpleMatch;
150
+ lines[adjustedExportIdx] = `${prefix}withDocs(${value})${suffix}`;
151
+ }
152
+ return lines.join("\n");
153
+ }
154
+ function rootLayoutTemplate() {
155
+ return `\
156
+ import type { Metadata } from "next";
157
+ import { RootProvider } from "@farming-labs/fumadocs";
158
+ import docsConfig from "@/docs.config";
159
+ import "./global.css";
160
+
161
+ export const metadata: Metadata = {
162
+ title: {
163
+ default: "Docs",
164
+ template: docsConfig.metadata?.titleTemplate ?? "%s",
165
+ },
166
+ description: docsConfig.metadata?.description,
167
+ };
168
+
169
+ export default function RootLayout({
170
+ children,
171
+ }: {
172
+ children: React.ReactNode;
173
+ }) {
174
+ return (
175
+ <html lang="en" suppressHydrationWarning>
176
+ <body>
177
+ <RootProvider>{children}</RootProvider>
178
+ </body>
179
+ </html>
180
+ );
181
+ }
182
+ `;
183
+ }
184
+ function globalCssTemplate(theme) {
185
+ return `\
186
+ @import "tailwindcss";
187
+ @import "@farming-labs/${theme}/css";
188
+ `;
189
+ }
190
+ /**
191
+ * Inject the fumadocs CSS import into an existing global.css.
192
+ * Returns the modified content, or null if already present.
193
+ */
194
+ function injectCssImport(existingContent, theme) {
195
+ const importLine = `@import "@farming-labs/${theme}/css";`;
196
+ if (existingContent.includes(importLine)) return null;
197
+ const lines = existingContent.split("\n");
198
+ const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("@import") ? i : acc, -1);
199
+ if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
200
+ else lines.unshift(importLine);
201
+ return lines.join("\n");
202
+ }
203
+ function docsLayoutTemplate() {
204
+ return `\
205
+ import docsConfig from "@/docs.config";
206
+ import { createDocsLayout } from "@farming-labs/fumadocs";
207
+
208
+ export default createDocsLayout(docsConfig);
209
+ `;
210
+ }
211
+ function postcssConfigTemplate() {
212
+ return `\
213
+ const config = {
214
+ plugins: {
215
+ "@tailwindcss/postcss": {},
216
+ },
217
+ };
218
+
219
+ export default config;
220
+ `;
221
+ }
222
+ function tsconfigTemplate() {
223
+ return `\
224
+ {
225
+ "compilerOptions": {
226
+ "target": "ES2017",
227
+ "lib": ["dom", "dom.iterable", "esnext"],
228
+ "allowJs": true,
229
+ "skipLibCheck": true,
230
+ "strict": true,
231
+ "noEmit": true,
232
+ "esModuleInterop": true,
233
+ "module": "esnext",
234
+ "moduleResolution": "bundler",
235
+ "resolveJsonModule": true,
236
+ "isolatedModules": true,
237
+ "jsx": "react-jsx",
238
+ "incremental": true,
239
+ "plugins": [{ "name": "next" }],
240
+ "paths": { "@/*": ["./*"] }
241
+ },
242
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
243
+ "exclude": ["node_modules"]
244
+ }
245
+ `;
246
+ }
247
+ function welcomePageTemplate(cfg) {
248
+ return `\
249
+ ---
250
+ title: "Documentation"
251
+ description: "Welcome to ${cfg.projectName} documentation"
252
+ ---
253
+
254
+ # Welcome to ${cfg.projectName}
255
+
256
+ Get started with our documentation. Browse the pages on the left to learn more.
257
+
258
+ <Callout type="info">
259
+ This documentation was generated by \`@farming-labs/docs\`. Edit the MDX files in \`app/${cfg.entry}/\` to customize.
260
+ </Callout>
261
+
262
+ ## Overview
263
+
264
+ This is your documentation home page. From here you can navigate to:
265
+
266
+ - [Installation](/${cfg.entry}/installation) — How to install and set up the project
267
+ - [Quickstart](/${cfg.entry}/quickstart) — Get up and running in minutes
268
+
269
+ ## Features
270
+
271
+ - **MDX Support** — Write docs with Markdown and React components
272
+ - **Syntax Highlighting** — Code blocks with automatic highlighting
273
+ - **Dark Mode** — Built-in theme switching
274
+ - **Search** — Full-text search across all pages
275
+ - **Responsive** — Works on any screen size
276
+
277
+ ---
278
+
279
+ ## Next Steps
280
+
281
+ Start by reading the [Installation](/${cfg.entry}/installation) guide, then follow the [Quickstart](/${cfg.entry}/quickstart) to build something.
282
+ `;
283
+ }
284
+ function installationPageTemplate(cfg) {
285
+ return `\
286
+ ---
287
+ title: "Installation"
288
+ description: "How to install and set up ${cfg.projectName}"
289
+ ---
290
+
291
+ # Installation
292
+
293
+ Follow these steps to install and configure ${cfg.projectName}.
294
+
295
+ <Callout type="info">
296
+ Prerequisites: Node.js 18+ and a package manager (pnpm, npm, or yarn).
297
+ </Callout>
298
+
299
+ ## Install Dependencies
300
+
301
+ \`\`\`bash
302
+ pnpm add @farming-labs/docs
303
+ \`\`\`
304
+
305
+ ## Configuration
306
+
307
+ Your project includes a \`docs.config.ts\` at the root:
308
+
309
+ \`\`\`ts
310
+ import { defineDocs } from "@farming-labs/docs";
311
+ import { fumadocs } from "@farming-labs/fumadocs";
312
+
313
+ export default defineDocs({
314
+ entry: "${cfg.entry}",
315
+ theme: fumadocs({
316
+ ui: { colors: { primary: "#6366f1" } },
317
+ }),
318
+ });
319
+ \`\`\`
320
+
321
+ ## Project Structure
322
+
323
+ \`\`\`
324
+ app/
325
+ ${cfg.entry}/
326
+ layout.tsx # Docs layout
327
+ page.mdx # /${cfg.entry}
328
+ installation/
329
+ page.mdx # /${cfg.entry}/installation
330
+ quickstart/
331
+ page.mdx # /${cfg.entry}/quickstart
332
+ docs.config.ts # Docs configuration
333
+ next.config.ts # Next.js config with withDocs()
334
+ \`\`\`
335
+
336
+ ## What's Next?
337
+
338
+ Head to the [Quickstart](/${cfg.entry}/quickstart) guide to start writing your first page.
339
+ `;
340
+ }
341
+ function quickstartPageTemplate(cfg) {
342
+ return `\
343
+ ---
344
+ title: "Quickstart"
345
+ description: "Get up and running in minutes"
346
+ ---
347
+
348
+ # Quickstart
349
+
350
+ This guide walks you through creating your first documentation page.
351
+
352
+ ## Creating a Page
353
+
354
+ Create a new folder under \`app/${cfg.entry}/\` with a \`page.mdx\` file:
355
+
356
+ \`\`\`bash
357
+ mkdir -p app/${cfg.entry}/my-page
358
+ \`\`\`
359
+
360
+ Then create \`app/${cfg.entry}/my-page/page.mdx\`:
361
+
362
+ \`\`\`mdx
363
+ ---
364
+ title: "My Page"
365
+ description: "A custom documentation page"
366
+ ---
367
+
368
+ # My Page
369
+
370
+ Write your content here using **Markdown** and JSX components.
371
+ \`\`\`
372
+
373
+ Your page is now available at \`/${cfg.entry}/my-page\`.
374
+
375
+ ## Using Components
376
+
377
+ ### Callouts
378
+
379
+ <Callout type="info">
380
+ This is an informational callout. Use it for tips and notes.
381
+ </Callout>
382
+
383
+ <Callout type="warn">
384
+ This is a warning callout. Use it for important caveats.
385
+ </Callout>
386
+
387
+ ### Code Blocks
388
+
389
+ Code blocks are automatically syntax-highlighted:
390
+
391
+ \`\`\`typescript
392
+ function greet(name: string): string {
393
+ return \\\`Hello, \\\${name}!\\\`;
394
+ }
395
+
396
+ console.log(greet("World"));
397
+ \`\`\`
398
+
399
+ ## Customizing the Theme
400
+
401
+ Edit \`docs.config.ts\` to change colors, typography, and component defaults:
402
+
403
+ \`\`\`ts
404
+ theme: fumadocs({
405
+ ui: {
406
+ colors: { primary: "#22c55e" },
407
+ },
408
+ }),
409
+ \`\`\`
410
+
411
+ ## Deploying
412
+
413
+ Build your docs for production:
414
+
415
+ \`\`\`bash
416
+ pnpm build
417
+ \`\`\`
418
+
419
+ Deploy to Vercel, Netlify, or any Node.js hosting platform.
420
+ `;
421
+ }
422
+
423
+ //#endregion
424
+ //#region src/cli/init.ts
425
+ async function init() {
426
+ const cwd = process.cwd();
427
+ p.intro(pc.bgCyan(pc.black(" @farming-labs/docs ")));
428
+ if (!detectFramework(cwd)) {
429
+ p.log.error("Could not detect a supported framework.\n Make sure you have a " + pc.cyan("package.json") + " with " + pc.cyan("next") + " installed.\n Supported frameworks: Next.js");
430
+ p.outro(pc.red("Init cancelled."));
431
+ process.exit(1);
432
+ }
433
+ p.log.success(`Detected framework: ${pc.cyan("Next.js")}`);
434
+ const theme = await p.select({
435
+ message: "Which theme would you like to use?",
436
+ options: [{
437
+ value: "fumadocs",
438
+ label: "Fumadocs",
439
+ hint: "Clean, modern docs theme with sidebar, search, and dark mode"
440
+ }]
441
+ });
442
+ if (p.isCancel(theme)) {
443
+ p.outro(pc.red("Init cancelled."));
444
+ process.exit(0);
445
+ }
446
+ const entry = await p.text({
447
+ message: "Where should your docs live?",
448
+ placeholder: "docs",
449
+ defaultValue: "docs",
450
+ validate: (value) => {
451
+ if (!value) return "Entry path is required";
452
+ if (value.startsWith("/")) return "Use a relative path (no leading /)";
453
+ if (value.includes(" ")) return "Path cannot contain spaces";
454
+ }
455
+ });
456
+ if (p.isCancel(entry)) {
457
+ p.outro(pc.red("Init cancelled."));
458
+ process.exit(0);
459
+ }
460
+ const entryPath = entry;
461
+ const pkgJson = JSON.parse(readFileSafe(path.join(cwd, "package.json")));
462
+ const cfg = {
463
+ entry: entryPath,
464
+ theme,
465
+ projectName: pkgJson.name || "My Project"
466
+ };
467
+ const s = p.spinner();
468
+ s.start("Scaffolding docs files");
469
+ const written = [];
470
+ const skipped = [];
471
+ function write(rel, content, overwrite = false) {
472
+ if (writeFileSafe(path.join(cwd, rel), content, overwrite)) written.push(rel);
473
+ else skipped.push(rel);
474
+ }
475
+ write("docs.config.ts", docsConfigTemplate(cfg));
476
+ const existingNextConfig = readFileSafe(path.join(cwd, "next.config.ts")) ?? readFileSafe(path.join(cwd, "next.config.mjs")) ?? readFileSafe(path.join(cwd, "next.config.js"));
477
+ if (existingNextConfig) {
478
+ const configFile = fileExists(path.join(cwd, "next.config.ts")) ? "next.config.ts" : fileExists(path.join(cwd, "next.config.mjs")) ? "next.config.mjs" : "next.config.js";
479
+ const merged = nextConfigMergedTemplate(existingNextConfig);
480
+ if (merged !== existingNextConfig) {
481
+ writeFileSafe(path.join(cwd, configFile), merged, true);
482
+ written.push(configFile + " (updated)");
483
+ } else skipped.push(configFile + " (already configured)");
484
+ } else write("next.config.ts", nextConfigTemplate());
485
+ write("app/layout.tsx", rootLayoutTemplate());
486
+ const globalCssPath = path.join(cwd, "app/global.css");
487
+ const existingGlobalCss = readFileSafe(globalCssPath);
488
+ if (existingGlobalCss) {
489
+ const injected = injectCssImport(existingGlobalCss, theme);
490
+ if (injected) {
491
+ writeFileSafe(globalCssPath, injected, true);
492
+ written.push("app/global.css (updated)");
493
+ } else skipped.push("app/global.css (already configured)");
494
+ } else write("app/global.css", globalCssTemplate(theme));
495
+ write(`app/${entryPath}/layout.tsx`, docsLayoutTemplate());
496
+ write("postcss.config.mjs", postcssConfigTemplate());
497
+ if (!fileExists(path.join(cwd, "tsconfig.json"))) write("tsconfig.json", tsconfigTemplate());
498
+ write(`app/${entryPath}/page.mdx`, welcomePageTemplate(cfg));
499
+ write(`app/${entryPath}/installation/page.mdx`, installationPageTemplate(cfg));
500
+ write(`app/${entryPath}/quickstart/page.mdx`, quickstartPageTemplate(cfg));
501
+ s.stop("Files scaffolded");
502
+ if (written.length > 0) p.log.success(`Created ${written.length} file${written.length > 1 ? "s" : ""}:\n` + written.map((f) => ` ${pc.green("+")} ${f}`).join("\n"));
503
+ if (skipped.length > 0) p.log.info(`Skipped ${skipped.length} existing file${skipped.length > 1 ? "s" : ""}:\n` + skipped.map((f) => ` ${pc.dim("-")} ${f}`).join("\n"));
504
+ const pm = detectPackageManager(cwd);
505
+ p.log.info(`Using ${pc.cyan(pm)} as package manager`);
506
+ const s2 = p.spinner();
507
+ s2.start("Installing dependencies");
508
+ try {
509
+ exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/next @farming-labs/fumadocs`, cwd);
510
+ const devDeps = [
511
+ "@tailwindcss/postcss",
512
+ "postcss",
513
+ "tailwindcss",
514
+ "@types/mdx",
515
+ "@types/node"
516
+ ];
517
+ const allDeps = {
518
+ ...pkgJson.dependencies,
519
+ ...pkgJson.devDependencies
520
+ };
521
+ const missingDevDeps = devDeps.filter((d) => !allDeps[d]);
522
+ if (missingDevDeps.length > 0) exec(`${devInstallCommand(pm)} ${missingDevDeps.join(" ")}`, cwd);
523
+ } catch {
524
+ s2.stop("Failed to install dependencies");
525
+ p.log.error(`Dependency installation failed. Run the install command manually:
526
+ ${pc.cyan(`${installCommand(pm)} @farming-labs/docs`)}`);
527
+ p.outro(pc.yellow("Setup partially complete. Install deps and run dev server manually."));
528
+ process.exit(1);
529
+ }
530
+ s2.stop("Dependencies installed");
531
+ const startDev = await p.confirm({
532
+ message: "Start the dev server now?",
533
+ initialValue: true
534
+ });
535
+ if (p.isCancel(startDev) || !startDev) {
536
+ p.log.info(`You can start the dev server later with:
537
+ ${pc.cyan(`${pm === "yarn" ? "yarn" : pm + " run"} dev`)}`);
538
+ p.outro(pc.green("Done! Happy documenting."));
539
+ process.exit(0);
540
+ }
541
+ p.log.step("Starting dev server...");
542
+ try {
543
+ const child = await spawnAndWaitFor("npx", [
544
+ "next",
545
+ "dev",
546
+ "--webpack"
547
+ ], cwd, "Ready", 6e4);
548
+ const url = `http://localhost:3000/${entryPath}`;
549
+ console.log();
550
+ p.log.success(`Dev server is running! Your docs are live at:\n\n ${pc.cyan(pc.underline(url))}\n\n Press ${pc.dim("Ctrl+C")} to stop the server.`);
551
+ p.outro(pc.green("Happy documenting!"));
552
+ await new Promise((resolve) => {
553
+ child.on("close", () => resolve());
554
+ process.on("SIGINT", () => {
555
+ child.kill("SIGINT");
556
+ resolve();
557
+ });
558
+ process.on("SIGTERM", () => {
559
+ child.kill("SIGTERM");
560
+ resolve();
561
+ });
562
+ });
563
+ } catch (err) {
564
+ p.log.error(`Could not start dev server. Try running manually:
565
+ ${pc.cyan("npx next dev --webpack")}`);
566
+ p.outro(pc.yellow("Setup complete. Start the server manually."));
567
+ process.exit(1);
568
+ }
569
+ }
570
+
571
+ //#endregion
572
+ //#region src/cli/index.ts
573
+ const command = process.argv.slice(2)[0];
574
+ async function main() {
575
+ if (!command || command === "init") await init();
576
+ else if (command === "--help" || command === "-h") printHelp();
577
+ else if (command === "--version" || command === "-v") printVersion();
578
+ else {
579
+ console.error(pc.red(`Unknown command: ${command}`));
580
+ console.error();
581
+ printHelp();
582
+ process.exit(1);
583
+ }
584
+ }
585
+ function printHelp() {
586
+ console.log(`
587
+ ${pc.bold("@farming-labs/docs")} — Documentation framework CLI
588
+
589
+ ${pc.dim("Usage:")}
590
+ farming-docs ${pc.cyan("<command>")}
591
+
592
+ ${pc.dim("Commands:")}
593
+ ${pc.cyan("init")} Scaffold docs in your project (default)
594
+
595
+ ${pc.dim("Options:")}
596
+ ${pc.cyan("-h, --help")} Show this help message
597
+ ${pc.cyan("-v, --version")} Show version
598
+ `);
599
+ }
600
+ function printVersion() {
601
+ console.log("0.1.0");
602
+ }
603
+ main().catch((err) => {
604
+ console.error(pc.red("An unexpected error occurred:"));
605
+ console.error(err);
606
+ process.exit(1);
607
+ });
608
+
609
+ //#endregion
610
+ export { };
@@ -0,0 +1,597 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Theme / UI configuration types for the docs framework.
4
+ * Inspired by Fumadocs: https://github.com/fuma-nama/fumadocs
5
+ */
6
+ /**
7
+ * Fine-grained UI configuration for docs themes.
8
+ *
9
+ * Theme authors define defaults for these values in their `createTheme` call.
10
+ * End users can override any value when calling the theme factory.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const myTheme = createTheme({
15
+ * name: "my-theme",
16
+ * ui: {
17
+ * colors: { primary: "#6366f1", background: "#0a0a0a" },
18
+ * radius: "0px",
19
+ * codeBlock: { showLineNumbers: true, theme: "github-dark" },
20
+ * sidebar: { width: 280, style: "bordered" },
21
+ * },
22
+ * });
23
+ * ```
24
+ */
25
+ /**
26
+ * Font style configuration for a single text element (heading, body, etc.).
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * h1: { size: "2.25rem", weight: 700, lineHeight: "1.2", letterSpacing: "-0.02em" }
31
+ * ```
32
+ */
33
+ interface FontStyle {
34
+ /** CSS `font-size` value (e.g. "2.25rem", "36px", "clamp(1.8rem, 3vw, 2.5rem)") */
35
+ size?: string;
36
+ /** CSS `font-weight` value (e.g. 700, "bold", "600") */
37
+ weight?: string | number;
38
+ /** CSS `line-height` value (e.g. "1.2", "1.5", "28px") */
39
+ lineHeight?: string;
40
+ /** CSS `letter-spacing` value (e.g. "-0.02em", "0.05em") */
41
+ letterSpacing?: string;
42
+ }
43
+ /**
44
+ * Typography configuration for the docs.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * typography: {
49
+ * font: {
50
+ * style: { sans: "Inter, sans-serif", mono: "JetBrains Mono, monospace" },
51
+ * h1: { size: "2.25rem", weight: 700, letterSpacing: "-0.02em" },
52
+ * h2: { size: "1.75rem", weight: 600 },
53
+ * body: { size: "1rem", lineHeight: "1.75" },
54
+ * },
55
+ * }
56
+ * ```
57
+ */
58
+ interface TypographyConfig {
59
+ /**
60
+ * Font configuration.
61
+ */
62
+ font?: {
63
+ /**
64
+ * Font family definitions.
65
+ */
66
+ style?: {
67
+ /** Sans-serif font family — used for body text, headings, and UI elements. */sans?: string; /** Monospace font family — used for code blocks, inline code, and terminal output. */
68
+ mono?: string;
69
+ }; /** Heading 1 (`<h1>`) style overrides */
70
+ h1?: FontStyle; /** Heading 2 (`<h2>`) style overrides */
71
+ h2?: FontStyle; /** Heading 3 (`<h3>`) style overrides */
72
+ h3?: FontStyle; /** Heading 4 (`<h4>`) style overrides */
73
+ h4?: FontStyle; /** Body text style */
74
+ body?: FontStyle; /** Small text style (captions, meta text) */
75
+ small?: FontStyle;
76
+ };
77
+ }
78
+ interface UIConfig {
79
+ /**
80
+ * Theme color tokens.
81
+ *
82
+ * These are mapped to `--color-fd-*` CSS variables at runtime.
83
+ * Accepts any valid CSS color value (hex, rgb, oklch, hsl, etc.).
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * colors: {
88
+ * primary: "oklch(0.72 0.19 149)", // green primary
89
+ * primaryForeground: "#ffffff", // white text on primary
90
+ * accent: "hsl(220 80% 60%)", // blue accent
91
+ * }
92
+ * ```
93
+ */
94
+ colors?: {
95
+ primary?: string;
96
+ primaryForeground?: string;
97
+ background?: string;
98
+ foreground?: string;
99
+ muted?: string;
100
+ mutedForeground?: string;
101
+ border?: string;
102
+ card?: string;
103
+ cardForeground?: string;
104
+ accent?: string;
105
+ accentForeground?: string;
106
+ secondary?: string;
107
+ secondaryForeground?: string;
108
+ popover?: string;
109
+ popoverForeground?: string;
110
+ ring?: string;
111
+ };
112
+ /**
113
+ * Typography settings — font families, heading sizes, weights, etc.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * typography: {
118
+ * font: {
119
+ * style: { sans: "Inter, sans-serif", mono: "JetBrains Mono, monospace" },
120
+ * h1: { size: "2.25rem", weight: 700, letterSpacing: "-0.02em" },
121
+ * body: { size: "1rem", lineHeight: "1.75" },
122
+ * },
123
+ * }
124
+ * ```
125
+ */
126
+ typography?: TypographyConfig;
127
+ /**
128
+ * Global border-radius. Maps to CSS `--radius`.
129
+ * Use "0px" for sharp corners, "0.5rem" for rounded, etc.
130
+ */
131
+ radius?: string;
132
+ /** Layout dimensions */
133
+ layout?: {
134
+ contentWidth?: number;
135
+ sidebarWidth?: number;
136
+ tocWidth?: number;
137
+ toc?: {
138
+ enabled?: boolean;
139
+ depth?: number;
140
+ };
141
+ header?: {
142
+ height?: number;
143
+ sticky?: boolean;
144
+ };
145
+ };
146
+ /** Code block rendering config */
147
+ codeBlock?: {
148
+ /** Show line numbers in code blocks @default false */showLineNumbers?: boolean; /** Show copy button @default true */
149
+ showCopyButton?: boolean; /** Shiki theme name for syntax highlighting */
150
+ theme?: string; /** Dark mode shiki theme (for dual-theme setups) */
151
+ darkTheme?: string;
152
+ };
153
+ /** Sidebar styling hints (consumed by theme CSS) */
154
+ sidebar?: {
155
+ /**
156
+ * Visual style of the sidebar.
157
+ * - "default" — standard fumadocs sidebar
158
+ * - "bordered" — visible bordered sections (like better-auth)
159
+ * - "floating" — floating card sidebar
160
+ */
161
+ style?: "default" | "bordered" | "floating"; /** Background color override */
162
+ background?: string; /** Border color override */
163
+ borderColor?: string;
164
+ };
165
+ /** Card styling */
166
+ card?: {
167
+ /** Whether cards have visible borders @default true */bordered?: boolean; /** Card background color override */
168
+ background?: string;
169
+ };
170
+ /** Default props/variants for MDX components (Callout, CodeBlock, Tabs, etc.) */
171
+ components?: {
172
+ [key: string]: Record<string, unknown> | ((defaults: unknown) => unknown);
173
+ };
174
+ }
175
+ /**
176
+ * A docs theme configuration.
177
+ *
178
+ * Theme authors create these with `createTheme()`. The `name` identifies the
179
+ * theme (useful for CSS scoping, debugging, analytics). The `ui` object holds
180
+ * all visual configuration.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * import { createTheme } from "@farming-labs/docs";
185
+ *
186
+ * export const myTheme = createTheme({
187
+ * name: "my-theme",
188
+ * ui: {
189
+ * colors: { primary: "#ff4d8d" },
190
+ * radius: "0px",
191
+ * },
192
+ * });
193
+ * ```
194
+ */
195
+ interface DocsTheme {
196
+ /** Unique name for this theme (used for CSS scoping and debugging) */
197
+ name?: string;
198
+ /** UI configuration — colors, typography, layout, components */
199
+ ui?: UIConfig;
200
+ /**
201
+ * @internal
202
+ * User-provided color overrides tracked by `createTheme`.
203
+ * Only these colors are emitted as inline CSS variables at runtime.
204
+ * Preset defaults stay in the theme's CSS file.
205
+ */
206
+ _userColorOverrides?: Record<string, string>;
207
+ }
208
+ interface DocsMetadata {
209
+ titleTemplate?: string;
210
+ description?: string;
211
+ twitterCard?: "summary" | "summary_large_image";
212
+ }
213
+ interface OGConfig {
214
+ enabled?: boolean;
215
+ type?: "static" | "dynamic";
216
+ endpoint?: string;
217
+ defaultImage?: string;
218
+ }
219
+ interface PageFrontmatter {
220
+ title: string;
221
+ description?: string;
222
+ tags?: string[];
223
+ icon?: string;
224
+ /** Path to custom OG image for this page */
225
+ ogImage?: string;
226
+ }
227
+ interface DocsNav {
228
+ /**
229
+ * Sidebar title — a plain string or a React element (e.g. a div with an icon).
230
+ *
231
+ * @example
232
+ * ```tsx
233
+ * // Simple string
234
+ * nav: { title: "My Docs" }
235
+ *
236
+ * // React element with icon
237
+ * nav: {
238
+ * title: <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
239
+ * <Rocket size={18} /> Example Docs
240
+ * </div>
241
+ * }
242
+ * ```
243
+ */
244
+ title?: unknown;
245
+ /** URL the title links to. Defaults to `/{entry}`. */
246
+ url?: string;
247
+ }
248
+ interface ThemeToggleConfig {
249
+ /**
250
+ * Whether to show the light/dark theme toggle in the sidebar.
251
+ * @default true
252
+ */
253
+ enabled?: boolean;
254
+ /**
255
+ * The default / forced theme when the toggle is hidden.
256
+ * Only applies when `enabled` is `false`.
257
+ * @default "system"
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * // Hide toggle, force dark mode
262
+ * themeToggle: { enabled: false, default: "dark" }
263
+ * ```
264
+ */
265
+ default?: "light" | "dark" | "system";
266
+ /**
267
+ * Toggle mode — show only light/dark, or include a system option.
268
+ * @default "light-dark"
269
+ */
270
+ mode?: "light-dark" | "light-dark-system";
271
+ }
272
+ interface BreadcrumbConfig {
273
+ /**
274
+ * Whether to show the breadcrumb navigation above page content.
275
+ * @default true
276
+ */
277
+ enabled?: boolean;
278
+ /**
279
+ * Custom breadcrumb component. Receives the default breadcrumb as children
280
+ * so you can wrap/modify it.
281
+ *
282
+ * @example
283
+ * ```tsx
284
+ * breadcrumb: {
285
+ * component: ({ items }) => <MyBreadcrumb items={items} />,
286
+ * }
287
+ * ```
288
+ */
289
+ component?: unknown;
290
+ }
291
+ interface SidebarConfig {
292
+ /**
293
+ * Whether to show the sidebar.
294
+ * @default true
295
+ */
296
+ enabled?: boolean;
297
+ /**
298
+ * Custom sidebar component to completely replace the default sidebar.
299
+ * Receives the page tree and config as context.
300
+ *
301
+ * @example
302
+ * ```tsx
303
+ * sidebar: {
304
+ * component: MySidebar,
305
+ * }
306
+ * ```
307
+ */
308
+ component?: unknown;
309
+ /**
310
+ * Sidebar footer content (rendered below navigation items).
311
+ */
312
+ footer?: unknown;
313
+ /**
314
+ * Sidebar banner content (rendered above navigation items).
315
+ */
316
+ banner?: unknown;
317
+ /**
318
+ * Whether the sidebar is collapsible on desktop.
319
+ * @default true
320
+ */
321
+ collapsible?: boolean;
322
+ }
323
+ /**
324
+ * A single "Open in …" provider shown in the Open dropdown.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * { name: "Claude", icon: <ClaudeIcon />, urlTemplate: "https://claude.ai?url={url}" }
329
+ * ```
330
+ */
331
+ interface OpenDocsProvider {
332
+ /** Display name (e.g. "ChatGPT", "Claude", "Cursor") */
333
+ name: string;
334
+ /** Icon element rendered next to the name */
335
+ icon?: unknown;
336
+ /**
337
+ * URL template. `{url}` is replaced with the current page URL.
338
+ * `{mdxUrl}` is replaced with the `.mdx` variant of the page URL.
339
+ *
340
+ * @example "https://claude.ai/new?q=Read+this+doc:+{url}"
341
+ */
342
+ urlTemplate: string;
343
+ }
344
+ /**
345
+ * Configuration for the "Open in …" dropdown that lets users
346
+ * send the current page to an LLM or external tool.
347
+ *
348
+ * @example
349
+ * ```ts
350
+ * openDocs: {
351
+ * enabled: true,
352
+ * providers: [
353
+ * { name: "ChatGPT", icon: <ChatGPTIcon />, urlTemplate: "https://chatgpt.com/?q={url}" },
354
+ * { name: "Claude", icon: <ClaudeIcon />, urlTemplate: "https://claude.ai/new?q={url}" },
355
+ * ],
356
+ * }
357
+ * ```
358
+ */
359
+ interface OpenDocsConfig {
360
+ /** Whether to show the "Open" dropdown. @default false */
361
+ enabled?: boolean;
362
+ /**
363
+ * List of LLM / tool providers to show in the dropdown.
364
+ * If not provided, a sensible default list is used.
365
+ */
366
+ providers?: OpenDocsProvider[];
367
+ }
368
+ /**
369
+ * Configuration for the "Copy Markdown" button that copies
370
+ * the current page's content as Markdown to the clipboard.
371
+ */
372
+ interface CopyMarkdownConfig {
373
+ /** Whether to show the "Copy Markdown" button. @default false */
374
+ enabled?: boolean;
375
+ }
376
+ /**
377
+ * Page-level action buttons shown above the page content
378
+ * (e.g. "Copy Markdown", "Open in …" dropdown).
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * pageActions: {
383
+ * copyMarkdown: { enabled: true },
384
+ * openDocs: {
385
+ * enabled: true,
386
+ * providers: [
387
+ * { name: "Claude", urlTemplate: "https://claude.ai/new?q={url}" },
388
+ * ],
389
+ * },
390
+ * }
391
+ * ```
392
+ */
393
+ interface PageActionsConfig {
394
+ /** "Copy Markdown" button */
395
+ copyMarkdown?: boolean | CopyMarkdownConfig;
396
+ /** "Open in …" dropdown with LLM / tool providers */
397
+ openDocs?: boolean | OpenDocsConfig;
398
+ /**
399
+ * Where to render the page action buttons relative to the page title.
400
+ *
401
+ * - `"below-title"` — render below the first `<h1>` heading (default)
402
+ * - `"above-title"` — render above the page title / content
403
+ *
404
+ * @default "below-title"
405
+ */
406
+ position?: "above-title" | "below-title";
407
+ }
408
+ interface DocsConfig {
409
+ /** Entry folder for docs (e.g. "docs" → /docs) */
410
+ entry: string;
411
+ /** Theme configuration - single source of truth for UI */
412
+ theme?: DocsTheme;
413
+ /**
414
+ * Sidebar navigation header.
415
+ * Customise the title shown at the top of the sidebar.
416
+ */
417
+ nav?: DocsNav;
418
+ /**
419
+ * Theme toggle (light/dark mode switcher) in the sidebar.
420
+ *
421
+ * - `true` or `undefined` → toggle is shown (default)
422
+ * - `false` → toggle is hidden, defaults to system theme
423
+ * - `{ enabled: false, default: "dark" }` → toggle hidden, force dark
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * // Hide toggle, force dark mode
428
+ * themeToggle: { enabled: false, default: "dark" }
429
+ *
430
+ * // Show toggle with system option
431
+ * themeToggle: { mode: "light-dark-system" }
432
+ * ```
433
+ */
434
+ themeToggle?: boolean | ThemeToggleConfig;
435
+ /**
436
+ * Breadcrumb navigation above page content.
437
+ *
438
+ * - `true` or `undefined` → breadcrumb is shown (default)
439
+ * - `false` → breadcrumb is hidden
440
+ * - `{ enabled: false }` → breadcrumb is hidden
441
+ * - `{ component: MyBreadcrumb }` → custom breadcrumb component
442
+ */
443
+ breadcrumb?: boolean | BreadcrumbConfig;
444
+ /**
445
+ * Sidebar customisation.
446
+ *
447
+ * - `true` or `undefined` → default sidebar
448
+ * - `false` → sidebar is hidden
449
+ * - `{ component: MySidebar }` → custom sidebar component
450
+ * - `{ footer: <MyFooter />, banner: <MyBanner /> }` → add footer/banner
451
+ */
452
+ sidebar?: boolean | SidebarConfig;
453
+ /**
454
+ * Custom MDX component overrides.
455
+ *
456
+ * Pass your own React components to replace defaults (e.g. Callout, CodeBlock).
457
+ * Components must match the expected props interface.
458
+ *
459
+ * @example
460
+ * ```ts
461
+ * import { MyCallout } from "./components/my-callout";
462
+ *
463
+ * export default defineDocs({
464
+ * entry: "docs",
465
+ * theme: fumadocs(),
466
+ * components: {
467
+ * Callout: MyCallout,
468
+ * },
469
+ * });
470
+ * ```
471
+ */
472
+ components?: Record<string, unknown>;
473
+ /**
474
+ * Icon registry for sidebar items.
475
+ *
476
+ * Map string labels to React elements. Reference them in page frontmatter
477
+ * with `icon: "label"` and the matching icon renders in the sidebar.
478
+ *
479
+ * @example
480
+ * ```tsx
481
+ * import { Book, Terminal, Rocket } from "lucide-react";
482
+ *
483
+ * export default defineDocs({
484
+ * entry: "docs",
485
+ * theme: fumadocs(),
486
+ * icons: {
487
+ * book: <Book />,
488
+ * terminal: <Terminal />,
489
+ * rocket: <Rocket />,
490
+ * },
491
+ * });
492
+ * ```
493
+ *
494
+ * Then in `page.mdx` frontmatter:
495
+ * ```yaml
496
+ * ---
497
+ * title: "CLI Reference"
498
+ * icon: "terminal"
499
+ * ---
500
+ * ```
501
+ */
502
+ icons?: Record<string, unknown>;
503
+ /**
504
+ * Page action buttons shown above the content area.
505
+ * Includes "Copy Markdown" and "Open in …" (LLM dropdown).
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * pageActions: {
510
+ * copyMarkdown: { enabled: true },
511
+ * openDocs: {
512
+ * enabled: true,
513
+ * providers: [
514
+ * { name: "ChatGPT", urlTemplate: "https://chatgpt.com/?q={url}" },
515
+ * { name: "Claude", urlTemplate: "https://claude.ai/new?q={url}" },
516
+ * ],
517
+ * },
518
+ * }
519
+ * ```
520
+ */
521
+ pageActions?: PageActionsConfig;
522
+ /** SEO metadata - separate from theme */
523
+ metadata?: DocsMetadata;
524
+ /** Open Graph image handling */
525
+ og?: OGConfig;
526
+ }
527
+ //#endregion
528
+ //#region src/define-docs.d.ts
529
+ /**
530
+ * Define docs configuration. Validates and returns the config.
531
+ */
532
+ declare function defineDocs(config: DocsConfig): DocsConfig;
533
+ //#endregion
534
+ //#region src/utils.d.ts
535
+ /**
536
+ * Deep merge utility for theme overrides.
537
+ * Merges objects recursively; later values override earlier ones.
538
+ */
539
+ declare function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Partial<T>[]): T;
540
+ //#endregion
541
+ //#region src/create-theme.d.ts
542
+ /**
543
+ * Create a theme preset factory.
544
+ *
545
+ * Returns a function that accepts optional overrides and deep-merges them
546
+ * with the base theme defaults. This is the same pattern used by the
547
+ * built-in `fumadocs()`, `darksharp()`, and `pixelBorder()` presets.
548
+ *
549
+ * @param baseTheme - The default theme configuration
550
+ * @returns A factory function `(overrides?) => DocsTheme`
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * import { createTheme } from "@farming-labs/docs";
555
+ *
556
+ * export const myTheme = createTheme({
557
+ * name: "my-theme",
558
+ * ui: {
559
+ * colors: { primary: "#6366f1" },
560
+ * layout: { contentWidth: 800 },
561
+ * },
562
+ * });
563
+ * ```
564
+ */
565
+ declare function createTheme(baseTheme: DocsTheme): (overrides?: Partial<DocsTheme>) => DocsTheme;
566
+ /**
567
+ * Extend an existing theme preset with additional defaults.
568
+ *
569
+ * Useful when you want to build on top of an existing theme (e.g. fumadocs)
570
+ * rather than starting from scratch.
571
+ *
572
+ * @example
573
+ * ```ts
574
+ * import { extendTheme } from "@farming-labs/docs";
575
+ * import { fumadocs } from "@farming-labs/fumadocs/default";
576
+ *
577
+ * // Start with fumadocs defaults, override some values
578
+ * export const myTheme = extendTheme(fumadocs(), {
579
+ * name: "my-custom-fumadocs",
580
+ * ui: { colors: { primary: "#22c55e" } },
581
+ * });
582
+ * ```
583
+ */
584
+ declare function extendTheme(baseTheme: DocsTheme, extensions: Partial<DocsTheme>): DocsTheme;
585
+ //#endregion
586
+ //#region src/metadata.d.ts
587
+ /**
588
+ * Resolve page title using metadata titleTemplate.
589
+ * %s is replaced with page title.
590
+ */
591
+ declare function resolveTitle(pageTitle: string, metadata?: DocsMetadata): string;
592
+ /**
593
+ * Resolve OG image URL for a page.
594
+ */
595
+ declare function resolveOGImage(page: PageFrontmatter, ogConfig?: OGConfig, baseUrl?: string): string | undefined;
596
+ //#endregion
597
+ export { type BreadcrumbConfig, type CopyMarkdownConfig, type DocsConfig, type DocsMetadata, type DocsNav, type DocsTheme, type FontStyle, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, type PageActionsConfig, type PageFrontmatter, type SidebarConfig, type ThemeToggleConfig, type TypographyConfig, type UIConfig, createTheme, deepMerge, defineDocs, extendTheme, resolveOGImage, resolveTitle };
package/dist/index.mjs ADDED
@@ -0,0 +1,116 @@
1
+ //#region src/define-docs.ts
2
+ /**
3
+ * Define docs configuration. Validates and returns the config.
4
+ */
5
+ function defineDocs(config) {
6
+ return {
7
+ entry: config.entry ?? "docs",
8
+ theme: config.theme,
9
+ nav: config.nav,
10
+ themeToggle: config.themeToggle,
11
+ breadcrumb: config.breadcrumb,
12
+ sidebar: config.sidebar,
13
+ components: config.components,
14
+ icons: config.icons,
15
+ pageActions: config.pageActions,
16
+ metadata: config.metadata,
17
+ og: config.og
18
+ };
19
+ }
20
+
21
+ //#endregion
22
+ //#region src/utils.ts
23
+ /**
24
+ * Deep merge utility for theme overrides.
25
+ * Merges objects recursively; later values override earlier ones.
26
+ */
27
+ function deepMerge(target, ...sources) {
28
+ if (!sources.length) return target;
29
+ const source = sources.shift();
30
+ if (!source) return target;
31
+ const result = { ...target };
32
+ for (const key of Object.keys(source)) {
33
+ const sourceVal = source[key];
34
+ const targetVal = result[key];
35
+ if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) result[key] = deepMerge(targetVal, sourceVal);
36
+ else if (sourceVal !== void 0) result[key] = sourceVal;
37
+ }
38
+ if (sources.length) return deepMerge(result, ...sources);
39
+ return result;
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/create-theme.ts
44
+ /**
45
+ * Create a theme preset factory.
46
+ *
47
+ * Returns a function that accepts optional overrides and deep-merges them
48
+ * with the base theme defaults. This is the same pattern used by the
49
+ * built-in `fumadocs()`, `darksharp()`, and `pixelBorder()` presets.
50
+ *
51
+ * @param baseTheme - The default theme configuration
52
+ * @returns A factory function `(overrides?) => DocsTheme`
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { createTheme } from "@farming-labs/docs";
57
+ *
58
+ * export const myTheme = createTheme({
59
+ * name: "my-theme",
60
+ * ui: {
61
+ * colors: { primary: "#6366f1" },
62
+ * layout: { contentWidth: 800 },
63
+ * },
64
+ * });
65
+ * ```
66
+ */
67
+ function createTheme(baseTheme) {
68
+ return function themeFactory(overrides = {}) {
69
+ const merged = deepMerge(baseTheme, overrides);
70
+ if (overrides.ui?.colors) merged._userColorOverrides = { ...overrides.ui.colors };
71
+ return merged;
72
+ };
73
+ }
74
+ /**
75
+ * Extend an existing theme preset with additional defaults.
76
+ *
77
+ * Useful when you want to build on top of an existing theme (e.g. fumadocs)
78
+ * rather than starting from scratch.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * import { extendTheme } from "@farming-labs/docs";
83
+ * import { fumadocs } from "@farming-labs/fumadocs/default";
84
+ *
85
+ * // Start with fumadocs defaults, override some values
86
+ * export const myTheme = extendTheme(fumadocs(), {
87
+ * name: "my-custom-fumadocs",
88
+ * ui: { colors: { primary: "#22c55e" } },
89
+ * });
90
+ * ```
91
+ */
92
+ function extendTheme(baseTheme, extensions) {
93
+ return deepMerge(baseTheme, extensions);
94
+ }
95
+
96
+ //#endregion
97
+ //#region src/metadata.ts
98
+ /**
99
+ * Resolve page title using metadata titleTemplate.
100
+ * %s is replaced with page title.
101
+ */
102
+ function resolveTitle(pageTitle, metadata) {
103
+ return (metadata?.titleTemplate ?? "%s").replace("%s", pageTitle);
104
+ }
105
+ /**
106
+ * Resolve OG image URL for a page.
107
+ */
108
+ function resolveOGImage(page, ogConfig, baseUrl) {
109
+ if (!ogConfig?.enabled) return void 0;
110
+ if (page.ogImage) return page.ogImage.startsWith("/") || page.ogImage.startsWith("http") ? page.ogImage : `${baseUrl ?? ""}${page.ogImage}`;
111
+ if (ogConfig.type === "dynamic" && ogConfig.endpoint) return `${baseUrl ?? ""}${ogConfig.endpoint}`;
112
+ return ogConfig.defaultImage;
113
+ }
114
+
115
+ //#endregion
116
+ export { createTheme, deepMerge, defineDocs, extendTheme, resolveOGImage, resolveTitle };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@farming-labs/docs",
3
+ "version": "0.0.2-beta.1",
4
+ "description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.mts",
12
+ "import": "./dist/index.mjs",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
16
+ "bin": {
17
+ "farming-docs": "./dist/cli/index.mjs"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "docs",
24
+ "mdx",
25
+ "documentation"
26
+ ],
27
+ "author": "Farming Labs",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@clack/prompts": "^0.9.1",
31
+ "gray-matter": "^4.0.3",
32
+ "picocolors": "^1.1.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.10.0",
36
+ "tsdown": "^0.20.3",
37
+ "typescript": "^5.9.3"
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown",
41
+ "dev": "tsdown --watch",
42
+ "typecheck": "tsc --noEmit"
43
+ }
44
+ }