@farming-labs/theme 0.0.2-beta.8 → 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.
@@ -1,6 +1,8 @@
1
1
  import { DocsAIFeatures } from "./docs-ai-features.mjs";
2
+ import { DocsCommandSearch } from "./docs-command-search.mjs";
2
3
  import { serializeIcon } from "./serialize-icon.mjs";
3
4
  import { DocsPageClient } from "./docs-page-client.mjs";
5
+ import { SidebarSearchWithAI } from "./sidebar-search-ai.mjs";
4
6
  import { jsx, jsxs } from "react/jsx-runtime";
5
7
  import fs from "node:fs";
6
8
  import path from "node:path";
@@ -31,9 +33,10 @@ function hasChildPages(dir) {
31
33
  }
32
34
  return false;
33
35
  }
34
- function buildTree(config) {
36
+ function buildTree(config, flat = false) {
35
37
  const docsDir = path.join(process.cwd(), "app", config.entry);
36
38
  const icons = config.icons;
39
+ const ordering = config.ordering;
37
40
  const rootChildren = [];
38
41
  if (fs.existsSync(path.join(docsDir, "page.mdx"))) {
39
42
  const data = readFrontmatter(path.join(docsDir, "page.mdx"));
@@ -44,52 +47,91 @@ function buildTree(config) {
44
47
  icon: resolveIcon(data.icon, icons)
45
48
  });
46
49
  }
47
- /**
48
- * Recursively scan a directory and return tree nodes.
49
- *
50
- * - If a subdirectory has its own children (nested pages), it becomes a
51
- * **folder** node with collapsible children. Its own `page.mdx` becomes
52
- * the folder's `index` page.
53
- * - Otherwise it becomes a simple **page** node.
54
- */
55
- function scan(dir, baseSlug) {
50
+ function buildNode(dir, name, baseSlug, slugOrder) {
51
+ const full = path.join(dir, name);
52
+ if (!fs.statSync(full).isDirectory()) return null;
53
+ const pagePath = path.join(full, "page.mdx");
54
+ if (!fs.existsSync(pagePath)) return null;
55
+ const data = readFrontmatter(pagePath);
56
+ const slug = [...baseSlug, name];
57
+ const url = `/${config.entry}/${slug.join("/")}`;
58
+ const icon = resolveIcon(data.icon, icons);
59
+ const displayName = data.title ?? name.replace(/-/g, " ");
60
+ if (hasChildPages(full)) {
61
+ const folderChildren = scanDir(full, slug, slugOrder);
62
+ return {
63
+ type: "folder",
64
+ name: displayName,
65
+ icon,
66
+ index: {
67
+ type: "page",
68
+ name: displayName,
69
+ url,
70
+ icon
71
+ },
72
+ children: folderChildren,
73
+ ...flat ? {
74
+ collapsible: false,
75
+ defaultOpen: true
76
+ } : {}
77
+ };
78
+ }
79
+ return {
80
+ type: "page",
81
+ name: displayName,
82
+ url,
83
+ icon
84
+ };
85
+ }
86
+ function scanDir(dir, baseSlug, slugOrder) {
56
87
  if (!fs.existsSync(dir)) return [];
57
- const nodes = [];
58
88
  const entries = fs.readdirSync(dir).sort();
59
- for (const name of entries) {
60
- const full = path.join(dir, name);
61
- if (!fs.statSync(full).isDirectory()) continue;
62
- const pagePath = path.join(full, "page.mdx");
63
- if (!fs.existsSync(pagePath)) continue;
64
- const data = readFrontmatter(pagePath);
65
- const slug = [...baseSlug, name];
66
- const url = `/${config.entry}/${slug.join("/")}`;
67
- const icon = resolveIcon(data.icon, icons);
68
- const displayName = data.title ?? name.replace(/-/g, " ");
69
- if (hasChildPages(full)) {
70
- const folderChildren = scan(full, slug);
71
- nodes.push({
72
- type: "folder",
73
- name: displayName,
74
- icon,
75
- index: {
76
- type: "page",
77
- name: displayName,
78
- url,
79
- icon
80
- },
81
- children: folderChildren
89
+ if (slugOrder) {
90
+ const nodes = [];
91
+ const slugMap = /* @__PURE__ */ new Map();
92
+ for (const item of slugOrder) slugMap.set(item.slug, item);
93
+ for (const item of slugOrder) {
94
+ if (!entries.includes(item.slug)) continue;
95
+ const node = buildNode(dir, item.slug, baseSlug, item.children);
96
+ if (node) nodes.push(node);
97
+ }
98
+ for (const name of entries) {
99
+ if (slugMap.has(name)) continue;
100
+ const node = buildNode(dir, name, baseSlug);
101
+ if (node) nodes.push(node);
102
+ }
103
+ return nodes;
104
+ }
105
+ if (ordering === "numeric") {
106
+ const nodes = [];
107
+ for (const name of entries) {
108
+ const full = path.join(dir, name);
109
+ if (!fs.statSync(full).isDirectory()) continue;
110
+ const pagePath = path.join(full, "page.mdx");
111
+ if (!fs.existsSync(pagePath)) continue;
112
+ const data = readFrontmatter(pagePath);
113
+ const order = typeof data.order === "number" ? data.order : Infinity;
114
+ const node = buildNode(dir, name, baseSlug);
115
+ if (node) nodes.push({
116
+ order,
117
+ node
82
118
  });
83
- } else nodes.push({
84
- type: "page",
85
- name: displayName,
86
- url,
87
- icon
119
+ }
120
+ nodes.sort((a, b) => {
121
+ if (a.order === b.order) return 0;
122
+ return a.order - b.order;
88
123
  });
124
+ return nodes.map((n) => n.node);
125
+ }
126
+ const nodes = [];
127
+ for (const name of entries) {
128
+ const node = buildNode(dir, name, baseSlug);
129
+ if (node) nodes.push(node);
89
130
  }
90
131
  return nodes;
91
132
  }
92
- rootChildren.push(...scan(docsDir, []));
133
+ const rootSlugOrder = Array.isArray(ordering) ? ordering : void 0;
134
+ rootChildren.push(...scanDir(docsDir, [], rootSlugOrder));
93
135
  return {
94
136
  name: "Docs",
95
137
  children: rootChildren
@@ -125,6 +167,31 @@ function buildLastModifiedMap(entry) {
125
167
  return map;
126
168
  }
127
169
  /**
170
+ * Scan all page.mdx files and build a map of URL pathname → description
171
+ * from the frontmatter `description` field.
172
+ */
173
+ function buildDescriptionMap(entry) {
174
+ const docsDir = path.join(process.cwd(), "app", entry);
175
+ const map = {};
176
+ function scan(dir, slugParts) {
177
+ if (!fs.existsSync(dir)) return;
178
+ const pagePath = path.join(dir, "page.mdx");
179
+ if (fs.existsSync(pagePath)) {
180
+ const desc = readFrontmatter(pagePath).description;
181
+ if (desc) {
182
+ const url = slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`;
183
+ map[url] = desc;
184
+ }
185
+ }
186
+ for (const name of fs.readdirSync(dir)) {
187
+ const full = path.join(dir, name);
188
+ if (fs.statSync(full).isDirectory()) scan(full, [...slugParts, name]);
189
+ }
190
+ }
191
+ scan(docsDir, []);
192
+ return map;
193
+ }
194
+ /**
128
195
  * Build a Next.js Metadata object from the docs config.
129
196
  *
130
197
  * Returns layout-level metadata including `title.template` so each page's
@@ -137,15 +204,71 @@ function buildLastModifiedMap(entry) {
137
204
  */
138
205
  function createDocsMetadata(config) {
139
206
  const meta = config.metadata;
207
+ const og = config.og;
140
208
  const template = meta?.titleTemplate ?? "%s";
141
- return {
209
+ const defaultTitle = template.replace("%s", "").replace(/^[\s–—-]+/, "").trim() || "Docs";
210
+ const result = {
142
211
  title: {
143
212
  template,
144
- default: template.replace("%s", "").replace(/^[\s–—-]+/, "").trim() || "Docs"
213
+ default: defaultTitle
145
214
  },
146
215
  ...meta?.description ? { description: meta.description } : {},
147
216
  ...meta?.twitterCard ? { twitter: { card: meta.twitterCard } } : {}
148
217
  };
218
+ if (og?.enabled !== false && og?.endpoint) {
219
+ const ogUrl = `${og.endpoint}?title=${encodeURIComponent(defaultTitle)}${meta?.description ? `&description=${encodeURIComponent(meta.description)}` : ""}`;
220
+ result.openGraph = { images: [{
221
+ url: ogUrl,
222
+ width: 1200,
223
+ height: 630
224
+ }] };
225
+ result.twitter = {
226
+ ...result.twitter,
227
+ card: meta?.twitterCard ?? "summary_large_image",
228
+ images: [ogUrl]
229
+ };
230
+ }
231
+ return result;
232
+ }
233
+ /**
234
+ * Generate page-level metadata with dynamic OG images.
235
+ *
236
+ * Usage in a docs page or [[...slug]] route:
237
+ * ```ts
238
+ * export function generateMetadata({ params }) {
239
+ * const page = getPage(params.slug);
240
+ * return createPageMetadata(docsConfig, {
241
+ * title: page.data.title,
242
+ * description: page.data.description,
243
+ * });
244
+ * }
245
+ * ```
246
+ */
247
+ function createPageMetadata(config, page) {
248
+ const og = config.og;
249
+ const result = {
250
+ title: page.title,
251
+ ...page.description ? { description: page.description } : {}
252
+ };
253
+ if (og?.enabled !== false && og?.endpoint) {
254
+ const ogUrl = `${og.endpoint}?title=${encodeURIComponent(page.title)}${page.description ? `&description=${encodeURIComponent(page.description)}` : ""}`;
255
+ result.openGraph = {
256
+ title: page.title,
257
+ description: page.description,
258
+ images: [{
259
+ url: ogUrl,
260
+ width: 1200,
261
+ height: 630
262
+ }]
263
+ };
264
+ result.twitter = {
265
+ card: "summary_large_image",
266
+ title: page.title,
267
+ description: page.description,
268
+ images: [ogUrl]
269
+ };
270
+ }
271
+ return result;
149
272
  }
150
273
  /** Resolve the themeToggle config into fumadocs-ui's `themeSwitch` prop. */
151
274
  function resolveThemeSwitch(toggle) {
@@ -162,10 +285,11 @@ function resolveSidebar(sidebar) {
162
285
  if (sidebar === false) return { enabled: false };
163
286
  return {
164
287
  enabled: sidebar.enabled !== false,
165
- component: sidebar.component,
288
+ componentFn: typeof sidebar.component === "function" ? sidebar.component : void 0,
166
289
  footer: sidebar.footer,
167
290
  banner: sidebar.banner,
168
- collapsible: sidebar.collapsible
291
+ collapsible: sidebar.collapsible,
292
+ flat: sidebar.flat
169
293
  };
170
294
  }
171
295
  const COLOR_MAP = {
@@ -194,7 +318,7 @@ function buildColorsCSS(colors) {
194
318
  vars.push(`${COLOR_MAP[key]}: ${value};`);
195
319
  }
196
320
  if (vars.length === 0) return "";
197
- return `:root, .dark {\n ${vars.join("\n ")}\n}`;
321
+ return `.dark {\n ${vars.join("\n ")}\n}`;
198
322
  }
199
323
  function ColorStyle({ colors }) {
200
324
  const css = buildColorsCSS(colors);
@@ -238,22 +362,59 @@ function TypographyStyle({ typography }) {
238
362
  if (!css) return null;
239
363
  return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } });
240
364
  }
365
+ function LayoutStyle({ layout }) {
366
+ if (!layout) return null;
367
+ const rootVars = [];
368
+ const desktopRootVars = [];
369
+ const desktopGridVars = [];
370
+ if (layout.sidebarWidth) {
371
+ const v = `--fd-sidebar-width: ${layout.sidebarWidth}px`;
372
+ desktopRootVars.push(`${v};`);
373
+ desktopGridVars.push(`${v} !important;`);
374
+ }
375
+ if (layout.contentWidth) rootVars.push(`--fd-content-width: ${layout.contentWidth}px;`);
376
+ if (layout.tocWidth) {
377
+ const v = `--fd-toc-width: ${layout.tocWidth}px`;
378
+ desktopRootVars.push(`${v};`);
379
+ desktopGridVars.push(`${v} !important;`);
380
+ }
381
+ if (rootVars.length === 0 && desktopRootVars.length === 0) return null;
382
+ const parts = [];
383
+ if (rootVars.length > 0) parts.push(`:root {\n ${rootVars.join("\n ")}\n}`);
384
+ if (desktopRootVars.length > 0) {
385
+ const inner = [`:root {\n ${desktopRootVars.join("\n ")}\n }`];
386
+ if (desktopGridVars.length > 0) inner.push(`[style*="fd-sidebar-col"] {\n ${desktopGridVars.join("\n ")}\n }`);
387
+ parts.push(`@media (min-width: 1024px) {\n ${inner.join("\n ")}\n}`);
388
+ }
389
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: parts.join("\n") } });
390
+ }
241
391
  function createDocsLayout(config) {
242
- const tocEnabled = (config.theme?.ui?.layout?.toc)?.enabled !== false;
392
+ const tocConfig = config.theme?.ui?.layout?.toc;
393
+ const tocEnabled = tocConfig?.enabled !== false;
394
+ const tocStyle = tocConfig?.style;
243
395
  const navTitle = config.nav?.title ?? "Docs";
244
396
  const navUrl = config.nav?.url ?? `/${config.entry}`;
245
397
  const themeSwitch = resolveThemeSwitch(config.themeToggle);
246
398
  const toggleConfig = typeof config.themeToggle === "object" ? config.themeToggle : void 0;
247
399
  const forcedTheme = themeSwitch.enabled === false && toggleConfig?.default && toggleConfig.default !== "system" ? toggleConfig.default : void 0;
248
- const sidebarProps = resolveSidebar(config.sidebar);
400
+ const resolvedSidebar = resolveSidebar(config.sidebar);
401
+ const sidebarFlat = resolvedSidebar.flat;
402
+ const sidebarComponentFn = resolvedSidebar.componentFn;
403
+ const { flat: _sidebarFlat, componentFn: _componentFn, ...sidebarProps } = resolvedSidebar;
249
404
  const breadcrumbConfig = config.breadcrumb;
250
405
  const breadcrumbEnabled = breadcrumbConfig === void 0 || breadcrumbConfig === true || typeof breadcrumbConfig === "object" && breadcrumbConfig.enabled !== false;
251
406
  const colors = config.theme?._userColorOverrides;
252
407
  const typography = config.theme?.ui?.typography;
408
+ const layoutDimensions = config.theme?.ui?.layout;
253
409
  const pageActions = config.pageActions;
254
410
  const copyMarkdownEnabled = resolveBool(pageActions?.copyMarkdown);
255
411
  const openDocsEnabled = resolveBool(pageActions?.openDocs);
256
412
  const pageActionsPosition = pageActions?.position ?? "below-title";
413
+ const pageActionsAlignment = pageActions?.alignment ?? "left";
414
+ const lastUpdatedRaw = config.lastUpdated;
415
+ const lastUpdatedEnabled = lastUpdatedRaw !== false && (typeof lastUpdatedRaw !== "object" || lastUpdatedRaw.enabled !== false);
416
+ const lastUpdatedPosition = typeof lastUpdatedRaw === "object" ? lastUpdatedRaw.position ?? "footer" : "footer";
417
+ const llmsTxtEnabled = resolveBool(config.llmsTxt);
257
418
  const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
258
419
  name: p.name,
259
420
  urlTemplate: p.urlTemplate,
@@ -271,21 +432,33 @@ function createDocsLayout(config) {
271
432
  const aiTriggerComponentHtml = aiConfig?.triggerComponent ? serializeIcon(aiConfig.triggerComponent) : void 0;
272
433
  const aiSuggestedQuestions = aiConfig?.suggestedQuestions;
273
434
  const aiLabel = aiConfig?.aiLabel;
435
+ const aiLoaderVariant = aiConfig?.loader;
274
436
  const aiLoadingComponentHtml = typeof aiConfig?.loadingComponent === "function" ? serializeIcon(aiConfig.loadingComponent({ name: aiLabel || "AI" })) : void 0;
275
437
  const lastModifiedMap = buildLastModifiedMap(config.entry);
438
+ const descriptionMap = buildDescriptionMap(config.entry);
276
439
  return function DocsLayoutWrapper({ children }) {
440
+ const tree = buildTree(config, !!sidebarFlat);
441
+ const finalSidebarProps = { ...sidebarProps };
442
+ if (sidebarComponentFn) finalSidebarProps.component = sidebarComponentFn({
443
+ tree,
444
+ collapsible: sidebarProps.collapsible !== false,
445
+ flat: !!sidebarFlat
446
+ });
277
447
  return /* @__PURE__ */ jsxs(DocsLayout, {
278
- tree: buildTree(config),
448
+ tree,
279
449
  nav: {
280
450
  title: navTitle,
281
451
  url: navUrl
282
452
  },
283
453
  themeSwitch,
284
- sidebar: sidebarProps,
454
+ sidebar: finalSidebarProps,
455
+ ...aiMode === "sidebar-icon" && aiEnabled ? { searchToggle: { components: { lg: /* @__PURE__ */ jsx(SidebarSearchWithAI, {}) } } } : {},
285
456
  children: [
286
457
  /* @__PURE__ */ jsx(ColorStyle, { colors }),
287
458
  /* @__PURE__ */ jsx(TypographyStyle, { typography }),
459
+ /* @__PURE__ */ jsx(LayoutStyle, { layout: layoutDimensions }),
288
460
  forcedTheme && /* @__PURE__ */ jsx(ForcedThemeScript, { theme: forcedTheme }),
461
+ /* @__PURE__ */ jsx(DocsCommandSearch, {}),
289
462
  aiEnabled && /* @__PURE__ */ jsx(DocsAIFeatures, {
290
463
  mode: aiMode,
291
464
  position: aiPosition,
@@ -293,20 +466,27 @@ function createDocsLayout(config) {
293
466
  triggerComponentHtml: aiTriggerComponentHtml,
294
467
  suggestedQuestions: aiSuggestedQuestions,
295
468
  aiLabel,
469
+ loaderVariant: aiLoaderVariant,
296
470
  loadingComponentHtml: aiLoadingComponentHtml
297
471
  }),
298
472
  /* @__PURE__ */ jsx(DocsPageClient, {
299
473
  tocEnabled,
474
+ tocStyle,
300
475
  breadcrumbEnabled,
301
476
  entry: config.entry,
302
477
  copyMarkdown: copyMarkdownEnabled,
303
478
  openDocs: openDocsEnabled,
304
479
  openDocsProviders,
305
480
  pageActionsPosition,
481
+ pageActionsAlignment,
306
482
  githubUrl,
307
483
  githubBranch,
308
484
  githubDirectory,
309
485
  lastModifiedMap,
486
+ lastUpdatedEnabled,
487
+ lastUpdatedPosition,
488
+ llmsTxtEnabled,
489
+ descriptionMap,
310
490
  children
311
491
  })
312
492
  ]
@@ -328,4 +508,4 @@ function ForcedThemeScript({ theme }) {
328
508
  }
329
509
 
330
510
  //#endregion
331
- export { createDocsLayout, createDocsMetadata };
511
+ export { createDocsLayout, createDocsMetadata, createPageMetadata };
@@ -10,6 +10,7 @@ interface SerializedProvider {
10
10
  }
11
11
  interface DocsPageClientProps {
12
12
  tocEnabled: boolean;
13
+ tocStyle?: "default" | "directional";
13
14
  breadcrumbEnabled?: boolean;
14
15
  /** The docs entry folder name (e.g. "docs") — used to strip from breadcrumb */
15
16
  entry?: string;
@@ -18,6 +19,8 @@ interface DocsPageClientProps {
18
19
  openDocsProviders?: SerializedProvider[];
19
20
  /** Where to render page actions relative to the title */
20
21
  pageActionsPosition?: "above-title" | "below-title";
22
+ /** Horizontal alignment of page action buttons */
23
+ pageActionsAlignment?: "left" | "right";
21
24
  /** GitHub repository URL (e.g. "https://github.com/user/repo") */
22
25
  githubUrl?: string;
23
26
  /** GitHub branch name @default "main" */
@@ -26,20 +29,37 @@ interface DocsPageClientProps {
26
29
  githubDirectory?: string;
27
30
  /** Map of pathname → formatted last-modified date string */
28
31
  lastModifiedMap?: Record<string, string>;
32
+ /** Whether to show "Last updated" at all */
33
+ lastUpdatedEnabled?: boolean;
34
+ /** Where to show the "Last updated" date: "footer" (next to Edit on GitHub) or "below-title" */
35
+ lastUpdatedPosition?: "footer" | "below-title";
36
+ /** Whether llms.txt is enabled — shows links in footer */
37
+ llmsTxtEnabled?: boolean;
38
+ /** Map of pathname → frontmatter description */
39
+ descriptionMap?: Record<string, string>;
40
+ /** Frontmatter description to display below the page title (overrides descriptionMap) */
41
+ description?: string;
29
42
  children: ReactNode;
30
43
  }
31
44
  declare function DocsPageClient({
32
45
  tocEnabled,
46
+ tocStyle,
33
47
  breadcrumbEnabled,
34
48
  entry,
35
49
  copyMarkdown,
36
50
  openDocs,
37
51
  openDocsProviders,
38
52
  pageActionsPosition,
53
+ pageActionsAlignment,
39
54
  githubUrl,
40
55
  githubBranch,
41
56
  githubDirectory,
42
57
  lastModifiedMap,
58
+ lastUpdatedEnabled,
59
+ lastUpdatedPosition,
60
+ llmsTxtEnabled,
61
+ descriptionMap,
62
+ description,
43
63
  children
44
64
  }: DocsPageClientProps): react_jsx_runtime0.JSX.Element;
45
65
  //#endregion
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { PageActions } from "./page-actions.mjs";
4
4
  import { useEffect, useState } from "react";
5
+ import { createPortal } from "react-dom";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  import { DocsBody, DocsPage, EditOnGitHub } from "fumadocs-ui/layouts/docs/page";
7
8
  import { usePathname, useRouter } from "next/navigation";
@@ -13,15 +14,13 @@ import { usePathname, useRouter } from "next/navigation";
13
14
  */
14
15
  function PathBreadcrumb({ pathname, entry }) {
15
16
  const router = useRouter();
16
- const allSegments = pathname.split("/").filter(Boolean);
17
- const segments = allSegments.filter((s) => s.toLowerCase() !== entry.toLowerCase());
17
+ const segments = pathname.split("/").filter(Boolean);
18
18
  if (segments.length < 2) return null;
19
19
  const parentSegment = segments[segments.length - 2];
20
20
  const currentSegment = segments[segments.length - 1];
21
21
  const parentLabel = parentSegment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
22
22
  const currentLabel = currentSegment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
23
- const parentIndex = allSegments.indexOf(parentSegment);
24
- const parentUrl = "/" + allSegments.slice(0, parentIndex + 1).join("/");
23
+ const parentUrl = "/" + segments.slice(0, segments.length - 1).join("/");
25
24
  return /* @__PURE__ */ jsxs("nav", {
26
25
  className: "fd-breadcrumb",
27
26
  "aria-label": "Breadcrumb",
@@ -54,19 +53,22 @@ function PathBreadcrumb({ pathname, entry }) {
54
53
  * (Copy Markdown, Open in LLM). Re-scans when the route changes.
55
54
  */
56
55
  /**
57
- * Build the GitHub URL for the current page's source file.
56
+ * Build the GitHub URL for the current page's source file (edit view).
58
57
  *
59
58
  * Examples:
60
- * No directory: https://github.com/user/repo/tree/main/app/docs/cli/page.mdx
61
- * With directory: https://github.com/farming-labs/docs/tree/main/website/app/docs/cli/page.mdx
59
+ * No directory: https://github.com/user/repo/edit/main/app/docs/cli/page.mdx
60
+ * With directory: https://github.com/farming-labs/docs/edit/main/website/app/docs/cli/page.mdx
62
61
  */
63
62
  function buildGithubFileUrl(githubUrl, branch, pathname, directory) {
64
63
  const segments = pathname.replace(/^\//, "").replace(/\/$/, "");
65
- return `${githubUrl}/tree/${branch}/${directory ? `${directory}/` : ""}app/${segments}/page.mdx`;
64
+ return `${githubUrl}/edit/${branch}/${`${directory ? `${directory}/` : ""}app/${segments}/page.mdx`}`;
66
65
  }
67
- function DocsPageClient({ tocEnabled, breadcrumbEnabled = true, entry = "docs", copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", githubUrl, githubBranch = "main", githubDirectory, lastModifiedMap, children }) {
66
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, githubBranch = "main", githubDirectory, lastModifiedMap, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, children }) {
67
+ const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
68
68
  const [toc, setToc] = useState([]);
69
69
  const pathname = usePathname();
70
+ const [actionsPortalTarget, setActionsPortalTarget] = useState(null);
71
+ const pageDescription = description ?? descriptionMap?.[pathname.replace(/\/$/, "") || "/"];
70
72
  useEffect(() => {
71
73
  if (!tocEnabled) return;
72
74
  const timer = requestAnimationFrame(() => {
@@ -81,29 +83,102 @@ function DocsPageClient({ tocEnabled, breadcrumbEnabled = true, entry = "docs",
81
83
  });
82
84
  return () => cancelAnimationFrame(timer);
83
85
  }, [tocEnabled, pathname]);
86
+ useEffect(() => {
87
+ if (!pageDescription) return;
88
+ const timer = requestAnimationFrame(() => {
89
+ const container = document.getElementById("nd-page");
90
+ if (!container) return;
91
+ const existingDesc = container.querySelector(".fd-page-description");
92
+ if (existingDesc) existingDesc.remove();
93
+ const h1 = container.querySelector("h1");
94
+ if (!h1) return;
95
+ const descEl = document.createElement("p");
96
+ descEl.className = "fd-page-description";
97
+ descEl.textContent = pageDescription;
98
+ h1.insertAdjacentElement("afterend", descEl);
99
+ });
100
+ return () => {
101
+ cancelAnimationFrame(timer);
102
+ const desc = document.querySelector("#nd-page .fd-page-description");
103
+ if (desc) desc.remove();
104
+ };
105
+ }, [pageDescription, pathname]);
84
106
  const showActions = copyMarkdown || openDocs;
85
107
  const githubFileUrl = githubUrl ? buildGithubFileUrl(githubUrl, githubBranch, pathname, githubDirectory) : void 0;
86
108
  const normalizedPath = pathname.replace(/\/$/, "") || "/";
87
- const lastModified = lastModifiedMap?.[normalizedPath];
88
- const showFooter = githubFileUrl || lastModified;
109
+ const lastModified = lastUpdatedEnabled ? lastModifiedMap?.[normalizedPath] : void 0;
110
+ const showLastUpdatedBelowTitle = !!lastModified && lastUpdatedPosition === "below-title";
111
+ const showLastUpdatedInFooter = !!lastModified && lastUpdatedPosition === "footer";
112
+ const showFooter = !!githubFileUrl || showLastUpdatedInFooter || llmsTxtEnabled;
113
+ const needsBelowTitleBlock = showLastUpdatedBelowTitle || showActions;
114
+ useEffect(() => {
115
+ if (!needsBelowTitleBlock) return;
116
+ const timer = requestAnimationFrame(() => {
117
+ const container = document.getElementById("nd-page");
118
+ if (!container) return;
119
+ container.querySelectorAll(".fd-below-title-block").forEach((el) => el.remove());
120
+ const h1 = container.querySelector("h1");
121
+ if (!h1) return;
122
+ let insertAfter = h1;
123
+ const desc = container.querySelector(".fd-page-description");
124
+ if (desc) insertAfter = desc;
125
+ const wrapper = document.createElement("div");
126
+ wrapper.className = "fd-below-title-block not-prose";
127
+ if (showLastUpdatedBelowTitle) {
128
+ const lastUpdatedEl = document.createElement("p");
129
+ lastUpdatedEl.className = "fd-last-updated-inline";
130
+ lastUpdatedEl.textContent = `Last updated ${lastModified}`;
131
+ wrapper.appendChild(lastUpdatedEl);
132
+ }
133
+ if (showLastUpdatedBelowTitle || showActions) {
134
+ const hr = document.createElement("hr");
135
+ hr.className = "fd-title-separator";
136
+ wrapper.appendChild(hr);
137
+ }
138
+ if (showActions) {
139
+ const portalEl = document.createElement("div");
140
+ portalEl.className = "fd-actions-portal";
141
+ portalEl.setAttribute("data-actions-alignment", pageActionsAlignment);
142
+ wrapper.appendChild(portalEl);
143
+ setActionsPortalTarget(portalEl);
144
+ }
145
+ insertAfter.insertAdjacentElement("afterend", wrapper);
146
+ });
147
+ return () => {
148
+ cancelAnimationFrame(timer);
149
+ setActionsPortalTarget(null);
150
+ document.querySelectorAll("#nd-page .fd-below-title-block").forEach((el) => el.remove());
151
+ };
152
+ }, [
153
+ lastModified,
154
+ needsBelowTitleBlock,
155
+ showLastUpdatedBelowTitle,
156
+ showActions,
157
+ pageActionsAlignment,
158
+ pathname
159
+ ]);
89
160
  return /* @__PURE__ */ jsxs(DocsPage, {
90
161
  toc,
91
- tableOfContent: { enabled: tocEnabled },
92
- tableOfContentPopover: { enabled: tocEnabled },
162
+ tableOfContent: {
163
+ enabled: tocEnabled,
164
+ style: fdTocStyle
165
+ },
166
+ tableOfContentPopover: {
167
+ enabled: tocEnabled,
168
+ style: fdTocStyle
169
+ },
93
170
  breadcrumb: { enabled: false },
94
171
  children: [
95
172
  breadcrumbEnabled && /* @__PURE__ */ jsx(PathBreadcrumb, {
96
173
  pathname,
97
174
  entry
98
175
  }),
99
- showActions && /* @__PURE__ */ jsx("div", {
100
- "data-actions-position": pageActionsPosition,
101
- children: /* @__PURE__ */ jsx(PageActions, {
102
- copyMarkdown,
103
- openDocs,
104
- providers: openDocsProviders
105
- })
106
- }),
176
+ showActions && actionsPortalTarget && createPortal(/* @__PURE__ */ jsx(PageActions, {
177
+ copyMarkdown,
178
+ openDocs,
179
+ providers: openDocsProviders,
180
+ githubFileUrl
181
+ }), actionsPortalTarget),
107
182
  /* @__PURE__ */ jsxs(DocsBody, {
108
183
  style: {
109
184
  display: "flex",
@@ -114,10 +189,29 @@ function DocsPageClient({ tocEnabled, breadcrumbEnabled = true, entry = "docs",
114
189
  children
115
190
  }), showFooter && /* @__PURE__ */ jsxs("div", {
116
191
  className: "not-prose fd-page-footer",
117
- children: [githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }), lastModified && /* @__PURE__ */ jsxs("span", {
118
- className: "fd-last-updated",
119
- children: ["Last updated: ", lastModified]
120
- })]
192
+ children: [
193
+ githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }),
194
+ llmsTxtEnabled && /* @__PURE__ */ jsxs("span", {
195
+ className: "fd-llms-txt-links",
196
+ children: [/* @__PURE__ */ jsx("a", {
197
+ href: "/api/docs?format=llms",
198
+ target: "_blank",
199
+ rel: "noopener noreferrer",
200
+ className: "fd-llms-txt-link",
201
+ children: "llms.txt"
202
+ }), /* @__PURE__ */ jsx("a", {
203
+ href: "/api/docs?format=llms-full",
204
+ target: "_blank",
205
+ rel: "noopener noreferrer",
206
+ className: "fd-llms-txt-link",
207
+ children: "llms-full.txt"
208
+ })]
209
+ }),
210
+ showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
211
+ className: "fd-last-updated-footer",
212
+ children: ["Last updated ", lastModified]
213
+ })
214
+ ]
121
215
  })]
122
216
  })
123
217
  ]