@farming-labs/docs 0.0.2-beta.2 → 0.0.2-beta.4

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.
@@ -10,10 +10,12 @@ function detectFramework(cwd) {
10
10
  const pkgPath = path.join(cwd, "package.json");
11
11
  if (!fs.existsSync(pkgPath)) return null;
12
12
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
13
- if ({
13
+ const allDeps = {
14
14
  ...pkg.dependencies,
15
15
  ...pkg.devDependencies
16
- }["next"]) return "nextjs";
16
+ };
17
+ if (allDeps["next"]) return "nextjs";
18
+ if (allDeps["@sveltejs/kit"]) return "sveltekit";
17
19
  return null;
18
20
  }
19
21
  function detectPackageManager(cwd) {
@@ -53,6 +55,25 @@ function readFileSafe(filePath) {
53
55
  if (!fs.existsSync(filePath)) return null;
54
56
  return fs.readFileSync(filePath, "utf-8");
55
57
  }
58
+ /** Common locations where global CSS files live in Next.js / SvelteKit projects. */
59
+ const GLOBAL_CSS_CANDIDATES = [
60
+ "app/globals.css",
61
+ "app/global.css",
62
+ "src/app/globals.css",
63
+ "src/app/global.css",
64
+ "src/app.css",
65
+ "styles/globals.css",
66
+ "styles/global.css",
67
+ "src/styles/globals.css",
68
+ "src/styles/global.css"
69
+ ];
70
+ /**
71
+ * Find existing global CSS files in the project.
72
+ * Returns relative paths that exist.
73
+ */
74
+ function detectGlobalCssFiles(cwd) {
75
+ return GLOBAL_CSS_CANDIDATES.filter((rel) => fs.existsSync(path.join(cwd, rel)));
76
+ }
56
77
  /**
57
78
  * Run a shell command synchronously, inheriting stdio.
58
79
  */
@@ -110,7 +131,7 @@ function spawnAndWaitFor(command, args, cwd, waitFor, timeoutMs = 6e4) {
110
131
  function docsConfigTemplate(cfg) {
111
132
  return `\
112
133
  import { defineDocs } from "@farming-labs/docs";
113
- import { fumadocs } from "@farming-labs/fumadocs";
134
+ import { fumadocs } from "@farming-labs/theme";
114
135
 
115
136
  export default defineDocs({
116
137
  entry: "${cfg.entry}",
@@ -151,12 +172,16 @@ function nextConfigMergedTemplate(existingContent) {
151
172
  }
152
173
  return lines.join("\n");
153
174
  }
154
- function rootLayoutTemplate() {
175
+ function rootLayoutTemplate(globalCssRelPath = "app/globals.css") {
176
+ let cssImport;
177
+ if (globalCssRelPath.startsWith("app/")) cssImport = "./" + globalCssRelPath.slice(4);
178
+ else if (globalCssRelPath.startsWith("src/app/")) cssImport = "./" + globalCssRelPath.slice(8);
179
+ else cssImport = "../" + globalCssRelPath;
155
180
  return `\
156
181
  import type { Metadata } from "next";
157
- import { RootProvider } from "@farming-labs/fumadocs";
182
+ import { RootProvider } from "@farming-labs/theme";
158
183
  import docsConfig from "@/docs.config";
159
- import "./global.css";
184
+ import "${cssImport}";
160
185
 
161
186
  export const metadata: Metadata = {
162
187
  title: {
@@ -203,7 +228,7 @@ function injectCssImport(existingContent, theme) {
203
228
  function docsLayoutTemplate() {
204
229
  return `\
205
230
  import docsConfig from "@/docs.config";
206
- import { createDocsLayout } from "@farming-labs/fumadocs";
231
+ import { createDocsLayout } from "@farming-labs/theme";
207
232
 
208
233
  export default createDocsLayout(docsConfig);
209
234
  `;
@@ -308,7 +333,7 @@ Your project includes a \`docs.config.ts\` at the root:
308
333
 
309
334
  \`\`\`ts
310
335
  import { defineDocs } from "@farming-labs/docs";
311
- import { fumadocs } from "@farming-labs/fumadocs";
336
+ import { fumadocs } from "@farming-labs/theme";
312
337
 
313
338
  export default defineDocs({
314
339
  entry: "${cfg.entry}",
@@ -416,6 +441,268 @@ Build your docs for production:
416
441
  pnpm build
417
442
  \`\`\`
418
443
 
444
+ Deploy to Vercel, Netlify, or any Node.js hosting platform.
445
+ `;
446
+ }
447
+ function svelteDocsConfigTemplate(cfg) {
448
+ return `\
449
+ import { defineDocs } from "@farming-labs/docs";
450
+ import { fumadocs } from "@farming-labs/svelte-theme";
451
+
452
+ export default defineDocs({
453
+ entry: "${cfg.entry}",
454
+ contentDir: "${cfg.entry}",
455
+ theme: fumadocs({
456
+ ui: {
457
+ colors: { primary: "#6366f1" },
458
+ },
459
+ }),
460
+
461
+ nav: {
462
+ title: "${cfg.projectName}",
463
+ url: "/${cfg.entry}",
464
+ },
465
+
466
+ breadcrumb: { enabled: true },
467
+
468
+ metadata: {
469
+ titleTemplate: "%s – ${cfg.projectName}",
470
+ description: "Documentation for ${cfg.projectName}",
471
+ },
472
+ });
473
+ `;
474
+ }
475
+ function svelteDocsServerTemplate(cfg) {
476
+ return `\
477
+ import { createDocsServer } from "@farming-labs/svelte/server";
478
+ import config from "../../docs.config.js";
479
+
480
+ export const { load, GET, POST } = createDocsServer(config);
481
+ `;
482
+ }
483
+ function svelteDocsLayoutTemplate(cfg) {
484
+ return `\
485
+ <script>
486
+ import { DocsLayout } from "@farming-labs/svelte-theme";
487
+ import config from "../../../docs.config.js";
488
+
489
+ let { data, children } = $props();
490
+ <\/script>
491
+
492
+ <DocsLayout tree={data.tree} {config}>
493
+ {@render children()}
494
+ </DocsLayout>
495
+ `;
496
+ }
497
+ function svelteDocsLayoutServerTemplate() {
498
+ return `\
499
+ export { load } from "$lib/docs.server.js";
500
+ `;
501
+ }
502
+ function svelteDocsPageTemplate(cfg) {
503
+ return `\
504
+ <script>
505
+ import { DocsContent } from "@farming-labs/svelte-theme";
506
+ import config from "../../../../docs.config.js";
507
+
508
+ let { data } = $props();
509
+ <\/script>
510
+
511
+ <DocsContent {data} {config} />
512
+ `;
513
+ }
514
+ function svelteRootLayoutTemplate(globalCssRelPath) {
515
+ let cssImport;
516
+ if (globalCssRelPath.startsWith("src/")) cssImport = "./" + globalCssRelPath.slice(4);
517
+ else cssImport = "../" + globalCssRelPath;
518
+ return `\
519
+ <script>
520
+ import "${cssImport}";
521
+
522
+ let { children } = $props();
523
+ <\/script>
524
+
525
+ {@render children()}
526
+ `;
527
+ }
528
+ function svelteGlobalCssTemplate(theme) {
529
+ return `\
530
+ @import "@farming-labs/svelte-theme/${theme}/css";
531
+ `;
532
+ }
533
+ function svelteCssImportLine(theme) {
534
+ return `@import "@farming-labs/svelte-theme/${theme}/css";`;
535
+ }
536
+ function injectSvelteCssImport(existingContent, theme) {
537
+ const importLine = svelteCssImportLine(theme);
538
+ if (existingContent.includes(importLine)) return null;
539
+ const lines = existingContent.split("\n");
540
+ const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("@import") ? i : acc, -1);
541
+ if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
542
+ else lines.unshift(importLine);
543
+ return lines.join("\n");
544
+ }
545
+ function svelteWelcomePageTemplate(cfg) {
546
+ return `\
547
+ ---
548
+ title: "Documentation"
549
+ description: "Welcome to ${cfg.projectName} documentation"
550
+ ---
551
+
552
+ # Welcome to ${cfg.projectName}
553
+
554
+ Get started with our documentation. Browse the pages on the left to learn more.
555
+
556
+ ## Overview
557
+
558
+ This documentation was generated by \`@farming-labs/docs\`. Edit the markdown files in \`${cfg.entry}/\` to customize.
559
+
560
+ ## Features
561
+
562
+ - **Markdown Support** — Write docs with standard Markdown
563
+ - **Syntax Highlighting** — Code blocks with automatic highlighting
564
+ - **Dark Mode** — Built-in theme switching
565
+ - **Search** — Full-text search across all pages
566
+ - **Responsive** — Works on any screen size
567
+
568
+ ---
569
+
570
+ ## Next Steps
571
+
572
+ Start by reading the [Installation](/${cfg.entry}/installation) guide, then follow the [Quickstart](/${cfg.entry}/quickstart) to build something.
573
+ `;
574
+ }
575
+ function svelteInstallationPageTemplate(cfg) {
576
+ return `\
577
+ ---
578
+ title: "Installation"
579
+ description: "How to install and set up ${cfg.projectName}"
580
+ ---
581
+
582
+ # Installation
583
+
584
+ Follow these steps to install and configure ${cfg.projectName}.
585
+
586
+ ## Prerequisites
587
+
588
+ - Node.js 18+
589
+ - A package manager (pnpm, npm, or yarn)
590
+
591
+ ## Install Dependencies
592
+
593
+ \`\`\`bash
594
+ pnpm add @farming-labs/docs @farming-labs/svelte @farming-labs/svelte-theme
595
+ \`\`\`
596
+
597
+ ## Configuration
598
+
599
+ Your project includes a \`docs.config.ts\` at the root:
600
+
601
+ \`\`\`ts title="docs.config.ts"
602
+ import { defineDocs } from "@farming-labs/docs";
603
+ import { fumadocs } from "@farming-labs/svelte-theme";
604
+
605
+ export default defineDocs({
606
+ entry: "${cfg.entry}",
607
+ contentDir: "${cfg.entry}",
608
+ theme: fumadocs({
609
+ ui: { colors: { primary: "#6366f1" } },
610
+ }),
611
+ });
612
+ \`\`\`
613
+
614
+ ## Project Structure
615
+
616
+ \`\`\`
617
+ ${cfg.entry}/ # Markdown content
618
+ page.md # /${cfg.entry}
619
+ installation/
620
+ page.md # /${cfg.entry}/installation
621
+ quickstart/
622
+ page.md # /${cfg.entry}/quickstart
623
+ src/
624
+ lib/
625
+ docs.server.ts # Server-side docs loader
626
+ routes/
627
+ ${cfg.entry}/
628
+ +layout.svelte # Docs layout
629
+ +layout.server.js # Layout data loader
630
+ [...slug]/
631
+ +page.svelte # Dynamic doc page
632
+ docs.config.ts # Docs configuration
633
+ \`\`\`
634
+
635
+ ## What's Next?
636
+
637
+ Head to the [Quickstart](/${cfg.entry}/quickstart) guide to start writing your first page.
638
+ `;
639
+ }
640
+ function svelteQuickstartPageTemplate(cfg) {
641
+ return `\
642
+ ---
643
+ title: "Quickstart"
644
+ description: "Get up and running in minutes"
645
+ ---
646
+
647
+ # Quickstart
648
+
649
+ This guide walks you through creating your first documentation page.
650
+
651
+ ## Creating a Page
652
+
653
+ Create a new folder under \`${cfg.entry}/\` with a \`page.md\` file:
654
+
655
+ \`\`\`bash
656
+ mkdir -p ${cfg.entry}/my-page
657
+ \`\`\`
658
+
659
+ Then create \`${cfg.entry}/my-page/page.md\`:
660
+
661
+ \`\`\`md
662
+ ---
663
+ title: "My Page"
664
+ description: "A custom documentation page"
665
+ ---
666
+
667
+ # My Page
668
+
669
+ Write your content here using **Markdown**.
670
+ \`\`\`
671
+
672
+ Your page is now available at \`/${cfg.entry}/my-page\`.
673
+
674
+ ## Code Blocks
675
+
676
+ Code blocks are automatically syntax-highlighted:
677
+
678
+ \`\`\`typescript
679
+ function greet(name: string): string {
680
+ return \\\`Hello, \\\${name}!\\\`;
681
+ }
682
+
683
+ console.log(greet("World"));
684
+ \`\`\`
685
+
686
+ ## Customizing the Theme
687
+
688
+ Edit \`docs.config.ts\` to change colors, typography, and component defaults:
689
+
690
+ \`\`\`ts title="docs.config.ts"
691
+ theme: fumadocs({
692
+ ui: {
693
+ colors: { primary: "#22c55e" },
694
+ },
695
+ }),
696
+ \`\`\`
697
+
698
+ ## Deploying
699
+
700
+ Build your docs for production:
701
+
702
+ \`\`\`bash
703
+ pnpm build
704
+ \`\`\`
705
+
419
706
  Deploy to Vercel, Netlify, or any Node.js hosting platform.
420
707
  `;
421
708
  }
@@ -425,19 +712,30 @@ Deploy to Vercel, Netlify, or any Node.js hosting platform.
425
712
  async function init() {
426
713
  const cwd = process.cwd();
427
714
  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");
715
+ const framework = detectFramework(cwd);
716
+ if (!framework) {
717
+ p.log.error("Could not detect a supported framework.\n Make sure you have a " + pc.cyan("package.json") + " with " + pc.cyan("next") + " or " + pc.cyan("@sveltejs/kit") + " installed.\n Supported frameworks: Next.js, SvelteKit");
430
718
  p.outro(pc.red("Init cancelled."));
431
719
  process.exit(1);
432
720
  }
433
- p.log.success(`Detected framework: ${pc.cyan("Next.js")}`);
721
+ const frameworkName = framework === "nextjs" ? "Next.js" : "SvelteKit";
722
+ p.log.success(`Detected framework: ${pc.cyan(frameworkName)}`);
723
+ const themeOptions = framework === "sveltekit" ? [{
724
+ value: "default",
725
+ label: "Default",
726
+ hint: "Clean, modern docs theme with sidebar, search, and dark mode"
727
+ }, {
728
+ value: "pixel-border",
729
+ label: "Pixel Border",
730
+ hint: "Sharp pixel-art inspired theme with monospace text"
731
+ }] : [{
732
+ value: "fumadocs",
733
+ label: "Fumadocs",
734
+ hint: "Clean, modern docs theme with sidebar, search, and dark mode"
735
+ }];
434
736
  const theme = await p.select({
435
737
  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
- }]
738
+ options: themeOptions
441
739
  });
442
740
  if (p.isCancel(theme)) {
443
741
  p.outro(pc.red("Init cancelled."));
@@ -458,11 +756,47 @@ async function init() {
458
756
  process.exit(0);
459
757
  }
460
758
  const entryPath = entry;
759
+ const detectedCssFiles = detectGlobalCssFiles(cwd);
760
+ let globalCssRelPath;
761
+ const defaultCssPath = framework === "sveltekit" ? "src/app.css" : "app/globals.css";
762
+ if (detectedCssFiles.length === 1) {
763
+ globalCssRelPath = detectedCssFiles[0];
764
+ p.log.info(`Found global CSS at ${pc.cyan(globalCssRelPath)}`);
765
+ } else if (detectedCssFiles.length > 1) {
766
+ const picked = await p.select({
767
+ message: "Multiple global CSS files found. Which one should we use?",
768
+ options: detectedCssFiles.map((f) => ({
769
+ value: f,
770
+ label: f
771
+ }))
772
+ });
773
+ if (p.isCancel(picked)) {
774
+ p.outro(pc.red("Init cancelled."));
775
+ process.exit(0);
776
+ }
777
+ globalCssRelPath = picked;
778
+ } else {
779
+ const cssPath = await p.text({
780
+ message: "Where is your global CSS file?",
781
+ placeholder: defaultCssPath,
782
+ defaultValue: defaultCssPath,
783
+ validate: (value) => {
784
+ if (!value) return "CSS file path is required";
785
+ if (!value.endsWith(".css")) return "Path must end with .css";
786
+ }
787
+ });
788
+ if (p.isCancel(cssPath)) {
789
+ p.outro(pc.red("Init cancelled."));
790
+ process.exit(0);
791
+ }
792
+ globalCssRelPath = cssPath;
793
+ }
461
794
  const pkgJson = JSON.parse(readFileSafe(path.join(cwd, "package.json")));
462
795
  const cfg = {
463
796
  entry: entryPath,
464
797
  theme,
465
- projectName: pkgJson.name || "My Project"
798
+ projectName: pkgJson.name || "My Project",
799
+ framework
466
800
  };
467
801
  const s = p.spinner();
468
802
  s.start("Scaffolding docs files");
@@ -472,32 +806,8 @@ async function init() {
472
806
  if (writeFileSafe(path.join(cwd, rel), content, overwrite)) written.push(rel);
473
807
  else skipped.push(rel);
474
808
  }
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));
809
+ if (framework === "sveltekit") scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written);
810
+ else scaffoldNextJs(cwd, cfg, globalCssRelPath, write, skipped, written);
501
811
  s.stop("Files scaffolded");
502
812
  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
813
  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"));
@@ -506,20 +816,23 @@ async function init() {
506
816
  const s2 = p.spinner();
507
817
  s2.start("Installing dependencies");
508
818
  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);
819
+ if (framework === "sveltekit") exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/svelte @farming-labs/svelte-theme`, cwd);
820
+ else {
821
+ exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/next @farming-labs/theme`, cwd);
822
+ const devDeps = [
823
+ "@tailwindcss/postcss",
824
+ "postcss",
825
+ "tailwindcss",
826
+ "@types/mdx",
827
+ "@types/node"
828
+ ];
829
+ const allDeps = {
830
+ ...pkgJson.dependencies,
831
+ ...pkgJson.devDependencies
832
+ };
833
+ const missingDevDeps = devDeps.filter((d) => !allDeps[d]);
834
+ if (missingDevDeps.length > 0) exec(`${devInstallCommand(pm)} ${missingDevDeps.join(" ")}`, cwd);
835
+ }
523
836
  } catch {
524
837
  s2.stop("Failed to install dependencies");
525
838
  p.log.error(`Dependency installation failed. Run the install command manually:
@@ -539,13 +852,23 @@ async function init() {
539
852
  process.exit(0);
540
853
  }
541
854
  p.log.step("Starting dev server...");
542
- try {
543
- const child = await spawnAndWaitFor("npx", [
855
+ const devCommand = framework === "sveltekit" ? {
856
+ cmd: "npx",
857
+ args: ["vite", "dev"],
858
+ waitFor: "ready"
859
+ } : {
860
+ cmd: "npx",
861
+ args: [
544
862
  "next",
545
863
  "dev",
546
864
  "--webpack"
547
- ], cwd, "Ready", 6e4);
548
- const url = `http://localhost:3000/${entryPath}`;
865
+ ],
866
+ waitFor: "Ready"
867
+ };
868
+ const defaultPort = framework === "sveltekit" ? "5173" : "3000";
869
+ try {
870
+ const child = await spawnAndWaitFor(devCommand.cmd, devCommand.args, cwd, devCommand.waitFor, 6e4);
871
+ const url = `http://localhost:${defaultPort}/${entryPath}`;
549
872
  console.log();
550
873
  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
874
  p.outro(pc.green("Happy documenting!"));
@@ -561,12 +884,66 @@ async function init() {
561
884
  });
562
885
  });
563
886
  } catch (err) {
887
+ const manualCmd = framework === "sveltekit" ? "npx vite dev" : "npx next dev --webpack";
564
888
  p.log.error(`Could not start dev server. Try running manually:
565
- ${pc.cyan("npx next dev --webpack")}`);
889
+ ${pc.cyan(manualCmd)}`);
566
890
  p.outro(pc.yellow("Setup complete. Start the server manually."));
567
891
  process.exit(1);
568
892
  }
569
893
  }
894
+ function scaffoldNextJs(cwd, cfg, globalCssRelPath, write, skipped, written) {
895
+ write("docs.config.ts", docsConfigTemplate(cfg));
896
+ const existingNextConfig = readFileSafe(path.join(cwd, "next.config.ts")) ?? readFileSafe(path.join(cwd, "next.config.mjs")) ?? readFileSafe(path.join(cwd, "next.config.js"));
897
+ if (existingNextConfig) {
898
+ 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";
899
+ const merged = nextConfigMergedTemplate(existingNextConfig);
900
+ if (merged !== existingNextConfig) {
901
+ writeFileSafe(path.join(cwd, configFile), merged, true);
902
+ written.push(configFile + " (updated)");
903
+ } else skipped.push(configFile + " (already configured)");
904
+ } else write("next.config.ts", nextConfigTemplate());
905
+ write("app/layout.tsx", rootLayoutTemplate(globalCssRelPath));
906
+ const globalCssAbsPath = path.join(cwd, globalCssRelPath);
907
+ const existingGlobalCss = readFileSafe(globalCssAbsPath);
908
+ if (existingGlobalCss) {
909
+ const injected = injectCssImport(existingGlobalCss, cfg.theme);
910
+ if (injected) {
911
+ writeFileSafe(globalCssAbsPath, injected, true);
912
+ written.push(globalCssRelPath + " (updated)");
913
+ } else skipped.push(globalCssRelPath + " (already configured)");
914
+ } else write(globalCssRelPath, globalCssTemplate(cfg.theme));
915
+ write(`app/${cfg.entry}/layout.tsx`, docsLayoutTemplate());
916
+ write("postcss.config.mjs", postcssConfigTemplate());
917
+ if (!fileExists(path.join(cwd, "tsconfig.json"))) write("tsconfig.json", tsconfigTemplate());
918
+ write(`app/${cfg.entry}/page.mdx`, welcomePageTemplate(cfg));
919
+ write(`app/${cfg.entry}/installation/page.mdx`, installationPageTemplate(cfg));
920
+ write(`app/${cfg.entry}/quickstart/page.mdx`, quickstartPageTemplate(cfg));
921
+ }
922
+ function scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written) {
923
+ write("docs.config.ts", svelteDocsConfigTemplate(cfg));
924
+ write("src/lib/docs.server.ts", svelteDocsServerTemplate(cfg));
925
+ write(`src/routes/${cfg.entry}/+layout.svelte`, svelteDocsLayoutTemplate(cfg));
926
+ write(`src/routes/${cfg.entry}/+layout.server.js`, svelteDocsLayoutServerTemplate());
927
+ write(`src/routes/${cfg.entry}/[...slug]/+page.svelte`, svelteDocsPageTemplate(cfg));
928
+ if (!readFileSafe(path.join(cwd, "src/routes/+layout.svelte"))) write("src/routes/+layout.svelte", svelteRootLayoutTemplate(globalCssRelPath));
929
+ const globalCssAbsPath = path.join(cwd, globalCssRelPath);
930
+ const existingGlobalCss = readFileSafe(globalCssAbsPath);
931
+ const cssTheme = {
932
+ default: "fumadocs",
933
+ "pixel-border": "pixel-border",
934
+ fumadocs: "fumadocs"
935
+ }[cfg.theme] || "fumadocs";
936
+ if (existingGlobalCss) {
937
+ const injected = injectSvelteCssImport(existingGlobalCss, cssTheme);
938
+ if (injected) {
939
+ writeFileSafe(globalCssAbsPath, injected, true);
940
+ written.push(globalCssRelPath + " (updated)");
941
+ } else skipped.push(globalCssRelPath + " (already configured)");
942
+ } else write(globalCssRelPath, svelteGlobalCssTemplate(cssTheme));
943
+ write(`${cfg.entry}/page.md`, svelteWelcomePageTemplate(cfg));
944
+ write(`${cfg.entry}/installation/page.md`, svelteInstallationPageTemplate(cfg));
945
+ write(`${cfg.entry}/quickstart/page.md`, svelteQuickstartPageTemplate(cfg));
946
+ }
570
947
 
571
948
  //#endregion
572
949
  //#region src/cli/index.ts
@@ -592,6 +969,9 @@ ${pc.dim("Usage:")}
592
969
  ${pc.dim("Commands:")}
593
970
  ${pc.cyan("init")} Scaffold docs in your project (default)
594
971
 
972
+ ${pc.dim("Supported frameworks:")}
973
+ Next.js, SvelteKit
974
+
595
975
  ${pc.dim("Options:")}
596
976
  ${pc.cyan("-h, --help")} Show this help message
597
977
  ${pc.cyan("-v, --version")} Show version
package/dist/index.d.mts CHANGED
@@ -442,9 +442,246 @@ interface GithubConfig {
442
442
  */
443
443
  directory?: string;
444
444
  }
445
+ /**
446
+ * Configuration for "Ask AI" — a RAG-powered chat that lets users
447
+ * ask questions about the documentation content.
448
+ *
449
+ * The AI handler searches relevant doc pages, builds context, and
450
+ * streams a response from an LLM (OpenAI-compatible API).
451
+ *
452
+ * The API key is **never** stored in the config. It is read from the
453
+ * `OPENAI_API_KEY` environment variable at runtime on the server.
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * ai: {
458
+ * enabled: true,
459
+ * model: "gpt-4o-mini",
460
+ * systemPrompt: "You are a helpful assistant for our developer docs.",
461
+ * }
462
+ * ```
463
+ */
464
+ interface AIConfig {
465
+ /**
466
+ * Whether to enable "Ask AI" functionality.
467
+ * When enabled, the unified `/api/docs` route handler will accept
468
+ * POST requests for AI chat.
469
+ * @default false
470
+ */
471
+ enabled?: boolean;
472
+ /**
473
+ * How the AI chat UI is presented.
474
+ *
475
+ * - `"search"` — AI tab integrated into the Cmd+K search dialog (default)
476
+ * - `"floating"` — A floating chat widget (bubble button + slide-out panel)
477
+ *
478
+ * @default "search"
479
+ *
480
+ * @example
481
+ * ```ts
482
+ * // Floating chat bubble in the bottom-right corner
483
+ * ai: {
484
+ * enabled: true,
485
+ * mode: "floating",
486
+ * position: "bottom-right",
487
+ * }
488
+ * ```
489
+ */
490
+ mode?: "search" | "floating";
491
+ /**
492
+ * Position of the floating chat button on screen.
493
+ * Only used when `mode` is `"floating"`.
494
+ *
495
+ * - `"bottom-right"` — bottom-right corner (default)
496
+ * - `"bottom-left"` — bottom-left corner
497
+ * - `"bottom-center"` — bottom center
498
+ *
499
+ * @default "bottom-right"
500
+ */
501
+ position?: "bottom-right" | "bottom-left" | "bottom-center";
502
+ /**
503
+ * Visual style of the floating chat when opened.
504
+ * Only used when `mode` is `"floating"`.
505
+ *
506
+ * - `"panel"` — A tall panel that slides up from the button position (default).
507
+ * Stays anchored near the floating button. No backdrop overlay.
508
+ *
509
+ * - `"modal"` — A centered modal dialog with a backdrop overlay,
510
+ * similar to the Cmd+K search dialog. Feels more focused and immersive.
511
+ *
512
+ * - `"popover"` — A compact popover near the button. Smaller than the
513
+ * panel, suitable for quick questions without taking much screen space.
514
+ *
515
+ * - `"full-modal"` — A full-screen immersive overlay (inspired by better-auth).
516
+ * Messages scroll in the center, input is pinned at the bottom.
517
+ * Suggested questions appear as horizontal pills. Best for
518
+ * documentation-heavy sites that want a premium AI experience.
519
+ *
520
+ * @default "panel"
521
+ *
522
+ * @example
523
+ * ```ts
524
+ * ai: {
525
+ * enabled: true,
526
+ * mode: "floating",
527
+ * position: "bottom-right",
528
+ * floatingStyle: "full-modal",
529
+ * }
530
+ * ```
531
+ */
532
+ floatingStyle?: "panel" | "modal" | "popover" | "full-modal";
533
+ /**
534
+ * Custom trigger component for the floating chat button.
535
+ * Only used when `mode` is `"floating"`.
536
+ *
537
+ * Pass a React element to replace the default sparkles button.
538
+ * The element receives an `onClick` handler automatically.
539
+ *
540
+ * @example
541
+ * ```tsx
542
+ * ai: {
543
+ * enabled: true,
544
+ * mode: "floating",
545
+ * triggerComponent: <button className="my-chat-btn">💬 Help</button>,
546
+ * }
547
+ * ```
548
+ */
549
+ triggerComponent?: unknown;
550
+ /**
551
+ * The LLM model to use for chat completions.
552
+ * Must be compatible with the OpenAI Chat Completions API.
553
+ * @default "gpt-4o-mini"
554
+ */
555
+ model?: string;
556
+ /**
557
+ * Custom system prompt prepended to the AI conversation.
558
+ * The documentation context is automatically appended after this prompt.
559
+ *
560
+ * @default "You are a helpful documentation assistant. Answer questions
561
+ * based on the provided documentation context. Be concise and accurate.
562
+ * If the answer is not in the context, say so honestly."
563
+ */
564
+ systemPrompt?: string;
565
+ /**
566
+ * Base URL for an OpenAI-compatible API endpoint.
567
+ * Use this to point to a self-hosted model, Azure OpenAI, or any
568
+ * compatible provider (e.g. Groq, Together, OpenRouter).
569
+ * @default "https://api.openai.com/v1"
570
+ */
571
+ baseUrl?: string;
572
+ /**
573
+ * API key for the LLM provider.
574
+ * Pass it via an environment variable to keep it out of source control.
575
+ *
576
+ * @default process.env.OPENAI_API_KEY
577
+ *
578
+ * @example
579
+ * ```ts
580
+ * // Default — reads OPENAI_API_KEY automatically
581
+ * ai: { enabled: true }
582
+ *
583
+ * // Custom provider key
584
+ * ai: {
585
+ * enabled: true,
586
+ * apiKey: process.env.GROQ_API_KEY,
587
+ * }
588
+ * ```
589
+ */
590
+ apiKey?: string;
591
+ /**
592
+ * Maximum number of search results to include as context for the AI.
593
+ * More results = more context but higher token usage.
594
+ * @default 5
595
+ */
596
+ maxResults?: number;
597
+ /**
598
+ * Pre-filled suggested questions shown in the AI chat when empty.
599
+ * When a user clicks one, it fills the input and submits automatically.
600
+ *
601
+ * @example
602
+ * ```ts
603
+ * ai: {
604
+ * enabled: true,
605
+ * suggestedQuestions: [
606
+ * "How do I install this?",
607
+ * "What themes are available?",
608
+ * "How do I create a custom component?",
609
+ * ],
610
+ * }
611
+ * ```
612
+ */
613
+ suggestedQuestions?: string[];
614
+ /**
615
+ * Display name for the AI assistant in the chat UI.
616
+ * Shown as the message label, header title, and passed to the loading component.
617
+ *
618
+ * @default "AI"
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * ai: {
623
+ * enabled: true,
624
+ * aiLabel: "DocsBot",
625
+ * }
626
+ * ```
627
+ */
628
+ aiLabel?: string;
629
+ /**
630
+ * The npm package name used in import examples.
631
+ * The AI will use this in code snippets instead of generic placeholders.
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * ai: {
636
+ * enabled: true,
637
+ * packageName: "@farming-labs/docs",
638
+ * }
639
+ * ```
640
+ */
641
+ packageName?: string;
642
+ /**
643
+ * The public URL of the documentation site.
644
+ * The AI will use this for links instead of relative paths.
645
+ *
646
+ * @example
647
+ * ```ts
648
+ * ai: {
649
+ * enabled: true,
650
+ * docsUrl: "https://docs.farming-labs.dev",
651
+ * }
652
+ * ```
653
+ */
654
+ docsUrl?: string;
655
+ /**
656
+ * Custom loading indicator shown while the AI is generating a response.
657
+ * Replaces the default "AI is thinking..." indicator.
658
+ *
659
+ * Pass a function that receives `{ name }` (the `aiLabel` value) and
660
+ * returns a React element. This way you don't need to duplicate the label.
661
+ *
662
+ * @example
663
+ * ```tsx
664
+ * ai: {
665
+ * enabled: true,
666
+ * aiLabel: "Sage",
667
+ * loadingComponent: ({ name }) => (
668
+ * <div className="flex items-center gap-2 text-sm text-zinc-400">
669
+ * <span className="animate-pulse">🤔</span>
670
+ * <span>{name} is thinking...</span>
671
+ * </div>
672
+ * ),
673
+ * }
674
+ * ```
675
+ */
676
+ loadingComponent?: (props: {
677
+ name: string;
678
+ }) => unknown;
679
+ }
445
680
  interface DocsConfig {
446
681
  /** Entry folder for docs (e.g. "docs" → /docs) */
447
682
  entry: string;
683
+ /** Path to the content directory. Defaults to `entry` value. */
684
+ contentDir?: string;
448
685
  /** Theme configuration - single source of truth for UI */
449
686
  theme?: DocsTheme;
450
687
  /**
@@ -579,6 +816,39 @@ interface DocsConfig {
579
816
  * ```
580
817
  */
581
818
  pageActions?: PageActionsConfig;
819
+ /**
820
+ * AI-powered "Ask AI" chat for documentation.
821
+ *
822
+ * When enabled, the unified API route handler (`/api/docs`) accepts
823
+ * POST requests for AI chat. The handler uses RAG (Retrieval-Augmented
824
+ * Generation) — it searches relevant docs, builds context, and streams
825
+ * a response from an LLM.
826
+ *
827
+ * The API key defaults to `process.env.OPENAI_API_KEY`. For other providers,
828
+ * pass the key via `apiKey: process.env.YOUR_KEY`.
829
+ *
830
+ * @example
831
+ * ```ts
832
+ * // Enable with defaults (gpt-4o-mini, OPENAI_API_KEY)
833
+ * ai: { enabled: true }
834
+ *
835
+ * // Custom model + system prompt
836
+ * ai: {
837
+ * enabled: true,
838
+ * model: "gpt-4o",
839
+ * systemPrompt: "You are an expert on our SDK. Be concise.",
840
+ * }
841
+ *
842
+ * // Use a different provider (e.g. Groq)
843
+ * ai: {
844
+ * enabled: true,
845
+ * baseUrl: "https://api.groq.com/openai/v1",
846
+ * apiKey: process.env.GROQ_API_KEY,
847
+ * model: "llama-3.1-70b-versatile",
848
+ * }
849
+ * ```
850
+ */
851
+ ai?: AIConfig;
582
852
  /** SEO metadata - separate from theme */
583
853
  metadata?: DocsMetadata;
584
854
  /** Open Graph image handling */
@@ -632,7 +902,7 @@ declare function createTheme(baseTheme: DocsTheme): (overrides?: Partial<DocsThe
632
902
  * @example
633
903
  * ```ts
634
904
  * import { extendTheme } from "@farming-labs/docs";
635
- * import { fumadocs } from "@farming-labs/fumadocs/default";
905
+ * import { fumadocs } from "@farming-labs/theme/default";
636
906
  *
637
907
  * // Start with fumadocs defaults, override some values
638
908
  * export const myTheme = extendTheme(fumadocs(), {
@@ -654,4 +924,4 @@ declare function resolveTitle(pageTitle: string, metadata?: DocsMetadata): strin
654
924
  */
655
925
  declare function resolveOGImage(page: PageFrontmatter, ogConfig?: OGConfig, baseUrl?: string): string | undefined;
656
926
  //#endregion
657
- export { type BreadcrumbConfig, type CopyMarkdownConfig, type DocsConfig, type DocsMetadata, type DocsNav, type DocsTheme, type FontStyle, type GithubConfig, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, type PageActionsConfig, type PageFrontmatter, type SidebarConfig, type ThemeToggleConfig, type TypographyConfig, type UIConfig, createTheme, deepMerge, defineDocs, extendTheme, resolveOGImage, resolveTitle };
927
+ export { type AIConfig, type BreadcrumbConfig, type CopyMarkdownConfig, type DocsConfig, type DocsMetadata, type DocsNav, type DocsTheme, type FontStyle, type GithubConfig, 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 CHANGED
@@ -5,6 +5,7 @@
5
5
  function defineDocs(config) {
6
6
  return {
7
7
  entry: config.entry ?? "docs",
8
+ contentDir: config.contentDir,
8
9
  theme: config.theme,
9
10
  nav: config.nav,
10
11
  github: config.github,
@@ -14,6 +15,7 @@ function defineDocs(config) {
14
15
  components: config.components,
15
16
  icons: config.icons,
16
17
  pageActions: config.pageActions,
18
+ ai: config.ai,
17
19
  metadata: config.metadata,
18
20
  og: config.og
19
21
  };
@@ -81,7 +83,7 @@ function createTheme(baseTheme) {
81
83
  * @example
82
84
  * ```ts
83
85
  * import { extendTheme } from "@farming-labs/docs";
84
- * import { fumadocs } from "@farming-labs/fumadocs/default";
86
+ * import { fumadocs } from "@farming-labs/theme/default";
85
87
  *
86
88
  * // Start with fumadocs defaults, override some values
87
89
  * export const myTheme = extendTheme(fumadocs(), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/docs",
3
- "version": "0.0.2-beta.2",
3
+ "version": "0.0.2-beta.4",
4
4
  "description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",