@farming-labs/theme 0.0.2-beta.17 → 0.0.2-beta.19

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.
@@ -33,5 +33,20 @@ declare function FloatingAIChat({
33
33
  aiLabel?: string;
34
34
  loadingComponentHtml?: string;
35
35
  }): any;
36
+ declare function AIModalDialog({
37
+ open,
38
+ onOpenChange,
39
+ api,
40
+ suggestedQuestions,
41
+ aiLabel,
42
+ loadingComponentHtml
43
+ }: {
44
+ open: boolean;
45
+ onOpenChange: (open: boolean) => void;
46
+ api?: string;
47
+ suggestedQuestions?: string[];
48
+ aiLabel?: string;
49
+ loadingComponentHtml?: string;
50
+ }): any;
36
51
  //#endregion
37
- export { DocsSearchDialog, FloatingAIChat };
52
+ export { AIModalDialog, DocsSearchDialog, FloatingAIChat };
@@ -932,6 +932,95 @@ function TrashIcon() {
932
932
  ]
933
933
  });
934
934
  }
935
+ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loadingComponentHtml }) {
936
+ const [messages, setMessages] = useState([]);
937
+ const [aiInput, setAiInput] = useState("");
938
+ const [isStreaming, setIsStreaming] = useState(false);
939
+ useEffect(() => {
940
+ if (!open) return;
941
+ const handler = (e) => {
942
+ if (e.key === "Escape") onOpenChange(false);
943
+ };
944
+ document.addEventListener("keydown", handler);
945
+ return () => document.removeEventListener("keydown", handler);
946
+ }, [open, onOpenChange]);
947
+ useEffect(() => {
948
+ if (open) document.body.style.overflow = "hidden";
949
+ else document.body.style.overflow = "";
950
+ return () => {
951
+ document.body.style.overflow = "";
952
+ };
953
+ }, [open]);
954
+ if (!open) return null;
955
+ const aiName = aiLabel || "AI";
956
+ return createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
957
+ onClick: () => onOpenChange(false),
958
+ className: "fd-ai-overlay"
959
+ }), /* @__PURE__ */ jsxs("div", {
960
+ role: "dialog",
961
+ "aria-modal": "true",
962
+ onClick: (e) => e.stopPropagation(),
963
+ className: "fd-ai-dialog fd-ai-modal-pure",
964
+ style: {
965
+ left: "50%",
966
+ top: "50%",
967
+ transform: "translate(-50%, -50%)",
968
+ width: "min(680px, calc(100vw - 32px))",
969
+ height: "min(560px, calc(100vh - 64px))",
970
+ animation: "fd-ai-float-center-in 200ms ease-out"
971
+ },
972
+ children: [
973
+ /* @__PURE__ */ jsxs("div", {
974
+ className: "fd-ai-header",
975
+ children: [
976
+ /* @__PURE__ */ jsx(SparklesIcon, { size: 16 }),
977
+ /* @__PURE__ */ jsxs("span", {
978
+ className: "fd-ai-header-title",
979
+ children: ["Ask ", aiName]
980
+ }),
981
+ /* @__PURE__ */ jsx("kbd", {
982
+ className: "fd-ai-esc",
983
+ children: "ESC"
984
+ }),
985
+ /* @__PURE__ */ jsx("button", {
986
+ onClick: () => onOpenChange(false),
987
+ className: "fd-ai-close-btn",
988
+ children: /* @__PURE__ */ jsx(XIcon, {})
989
+ })
990
+ ]
991
+ }),
992
+ /* @__PURE__ */ jsx(AIChat, {
993
+ api,
994
+ messages,
995
+ setMessages,
996
+ aiInput,
997
+ setAiInput,
998
+ isStreaming,
999
+ setIsStreaming,
1000
+ suggestedQuestions,
1001
+ aiLabel,
1002
+ loadingComponentHtml
1003
+ }),
1004
+ /* @__PURE__ */ jsx("div", {
1005
+ className: "fd-ai-modal-footer",
1006
+ children: messages.length > 0 ? /* @__PURE__ */ jsxs("button", {
1007
+ className: "fd-ai-fm-clear-btn",
1008
+ onClick: () => {
1009
+ if (!isStreaming) {
1010
+ setMessages([]);
1011
+ setAiInput("");
1012
+ }
1013
+ },
1014
+ "aria-disabled": isStreaming,
1015
+ children: [/* @__PURE__ */ jsx(TrashIcon, {}), /* @__PURE__ */ jsx("span", { children: "Clear chat" })]
1016
+ }) : /* @__PURE__ */ jsx("div", {
1017
+ className: "fd-ai-modal-footer-hint",
1018
+ children: "AI can be inaccurate, please verify the information."
1019
+ })
1020
+ })
1021
+ ]
1022
+ })] }), document.body);
1023
+ }
935
1024
 
936
1025
  //#endregion
937
- export { DocsSearchDialog, FloatingAIChat };
1026
+ export { AIModalDialog, DocsSearchDialog, FloatingAIChat };
@@ -0,0 +1,80 @@
1
+ import * as _farming_labs_docs0 from "@farming-labs/docs";
2
+
3
+ //#region src/darkbold/index.d.ts
4
+ declare const DarkBoldUIDefaults: {
5
+ colors: {
6
+ primary: string;
7
+ background: string;
8
+ muted: string;
9
+ border: string;
10
+ };
11
+ typography: {
12
+ font: {
13
+ style: {
14
+ sans: string;
15
+ mono: string;
16
+ };
17
+ h1: {
18
+ size: string;
19
+ weight: number;
20
+ lineHeight: string;
21
+ letterSpacing: string;
22
+ };
23
+ h2: {
24
+ size: string;
25
+ weight: number;
26
+ lineHeight: string;
27
+ letterSpacing: string;
28
+ };
29
+ h3: {
30
+ size: string;
31
+ weight: number;
32
+ lineHeight: string;
33
+ letterSpacing: string;
34
+ };
35
+ h4: {
36
+ size: string;
37
+ weight: number;
38
+ lineHeight: string;
39
+ };
40
+ body: {
41
+ size: string;
42
+ weight: number;
43
+ lineHeight: string;
44
+ };
45
+ small: {
46
+ size: string;
47
+ weight: number;
48
+ lineHeight: string;
49
+ };
50
+ };
51
+ };
52
+ layout: {
53
+ contentWidth: number;
54
+ sidebarWidth: number;
55
+ toc: {
56
+ enabled: boolean;
57
+ depth: number;
58
+ style: "default";
59
+ };
60
+ header: {
61
+ height: number;
62
+ sticky: boolean;
63
+ };
64
+ };
65
+ components: {
66
+ Callout: {
67
+ variant: string;
68
+ icon: boolean;
69
+ };
70
+ CodeBlock: {
71
+ showCopyButton: boolean;
72
+ };
73
+ Tabs: {
74
+ style: string;
75
+ };
76
+ };
77
+ };
78
+ declare const darkbold: (overrides?: Partial<_farming_labs_docs0.DocsTheme>) => _farming_labs_docs0.DocsTheme;
79
+ //#endregion
80
+ export { DarkBoldUIDefaults, darkbold };
@@ -0,0 +1,84 @@
1
+ import { createTheme } from "@farming-labs/docs";
2
+
3
+ //#region src/darkbold/index.ts
4
+ /**
5
+ * DarkBold theme preset.
6
+ * Pure monochrome design, Geist typography, clean minimalism.
7
+ *
8
+ * CSS: `@import "@farming-labs/theme/darkbold/css";`
9
+ */
10
+ const DarkBoldUIDefaults = {
11
+ colors: {
12
+ primary: "#000",
13
+ background: "#fff",
14
+ muted: "#666",
15
+ border: "#eaeaea"
16
+ },
17
+ typography: { font: {
18
+ style: {
19
+ sans: "Geist, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
20
+ mono: "Geist Mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace"
21
+ },
22
+ h1: {
23
+ size: "2.5rem",
24
+ weight: 600,
25
+ lineHeight: "1.2",
26
+ letterSpacing: "-0.06em"
27
+ },
28
+ h2: {
29
+ size: "2rem",
30
+ weight: 600,
31
+ lineHeight: "1.25",
32
+ letterSpacing: "-0.04em"
33
+ },
34
+ h3: {
35
+ size: "1.5rem",
36
+ weight: 600,
37
+ lineHeight: "1.3",
38
+ letterSpacing: "-0.02em"
39
+ },
40
+ h4: {
41
+ size: "1.25rem",
42
+ weight: 600,
43
+ lineHeight: "1.4"
44
+ },
45
+ body: {
46
+ size: "1rem",
47
+ weight: 400,
48
+ lineHeight: "1.6"
49
+ },
50
+ small: {
51
+ size: "0.875rem",
52
+ weight: 400,
53
+ lineHeight: "1.5"
54
+ }
55
+ } },
56
+ layout: {
57
+ contentWidth: 768,
58
+ sidebarWidth: 260,
59
+ toc: {
60
+ enabled: true,
61
+ depth: 3,
62
+ style: "default"
63
+ },
64
+ header: {
65
+ height: 64,
66
+ sticky: true
67
+ }
68
+ },
69
+ components: {
70
+ Callout: {
71
+ variant: "soft",
72
+ icon: true
73
+ },
74
+ CodeBlock: { showCopyButton: true },
75
+ Tabs: { style: "default" }
76
+ }
77
+ };
78
+ const darkbold = createTheme({
79
+ name: "darkbold",
80
+ ui: DarkBoldUIDefaults
81
+ });
82
+
83
+ //#endregion
84
+ export { DarkBoldUIDefaults, darkbold };
@@ -2,7 +2,7 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
2
 
3
3
  //#region src/docs-ai-features.d.ts
4
4
  interface DocsAIFeaturesProps {
5
- mode: "search" | "floating";
5
+ mode: "search" | "floating" | "sidebar-icon";
6
6
  position?: "bottom-right" | "bottom-left" | "bottom-center";
7
7
  floatingStyle?: "panel" | "modal" | "popover" | "full-modal";
8
8
  triggerComponentHtml?: string;
@@ -1,18 +1,20 @@
1
1
  "use client";
2
2
 
3
- import { DocsSearchDialog, FloatingAIChat } from "./ai-search-dialog.mjs";
3
+ import { AIModalDialog, DocsSearchDialog, FloatingAIChat } from "./ai-search-dialog.mjs";
4
4
  import { useEffect, useState } from "react";
5
- import { jsx } from "react/jsx-runtime";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
 
7
7
  //#region src/docs-ai-features.tsx
8
8
  /**
9
9
  * Client component injected by `createDocsLayout` when `ai` is configured.
10
10
  *
11
- * Handles both modes:
11
+ * Handles multiple modes:
12
12
  * - "search": Intercepts Cmd+K / Ctrl+K and opens the custom search dialog
13
13
  * with Search + Ask AI tabs (prevents fumadocs' default dialog from opening).
14
14
  * - "floating": Renders the floating chat widget with configurable position,
15
15
  * style, and trigger component.
16
+ * - "sidebar-icon": Injects an AI trigger icon button next to the search bar
17
+ * in the sidebar header area (Mintlify-style).
16
18
  *
17
19
  * This component is rendered inside the docs layout so the user's root layout
18
20
  * never needs to be modified — AI features work purely from `docs.config.tsx`.
@@ -23,6 +25,11 @@ function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "pane
23
25
  aiLabel,
24
26
  loadingComponentHtml
25
27
  });
28
+ if (mode === "sidebar-icon") return /* @__PURE__ */ jsx(SidebarIconModeAI, {
29
+ suggestedQuestions,
30
+ aiLabel,
31
+ loadingComponentHtml
32
+ });
26
33
  return /* @__PURE__ */ jsx(FloatingAIChat, {
27
34
  api: "/api/docs",
28
35
  position,
@@ -76,6 +83,56 @@ function SearchModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }) {
76
83
  loadingComponentHtml
77
84
  });
78
85
  }
86
+ /**
87
+ * Sidebar-icon mode: injects a sparkle icon button next to the search bar
88
+ * in the sidebar header. The search button opens the Cmd+K search dialog,
89
+ * and the AI sparkle button opens a pure AI modal (no search tabs).
90
+ */
91
+ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }) {
92
+ const [searchOpen, setSearchOpen] = useState(false);
93
+ const [aiOpen, setAiOpen] = useState(false);
94
+ useEffect(() => {
95
+ function handler(e) {
96
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
97
+ e.preventDefault();
98
+ e.stopPropagation();
99
+ e.stopImmediatePropagation();
100
+ setSearchOpen(true);
101
+ }
102
+ }
103
+ document.addEventListener("keydown", handler, true);
104
+ return () => document.removeEventListener("keydown", handler, true);
105
+ }, []);
106
+ useEffect(() => {
107
+ function onSearch() {
108
+ setSearchOpen(true);
109
+ }
110
+ function onAI() {
111
+ setAiOpen(true);
112
+ }
113
+ window.addEventListener("fd-open-search", onSearch);
114
+ window.addEventListener("fd-open-ai", onAI);
115
+ return () => {
116
+ window.removeEventListener("fd-open-search", onSearch);
117
+ window.removeEventListener("fd-open-ai", onAI);
118
+ };
119
+ }, []);
120
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DocsSearchDialog, {
121
+ open: searchOpen,
122
+ onOpenChange: setSearchOpen,
123
+ api: "/api/docs",
124
+ suggestedQuestions,
125
+ aiLabel,
126
+ loadingComponentHtml
127
+ }), /* @__PURE__ */ jsx(AIModalDialog, {
128
+ open: aiOpen,
129
+ onOpenChange: setAiOpen,
130
+ api: "/api/docs",
131
+ suggestedQuestions,
132
+ aiLabel,
133
+ loadingComponentHtml
134
+ })] });
135
+ }
79
136
 
80
137
  //#endregion
81
138
  export { DocsAIFeatures };
@@ -336,7 +336,8 @@ function DocsCommandSearch() {
336
336
  const button = e.target.closest("button");
337
337
  if (!button) return;
338
338
  const text = button.textContent || "";
339
- if (text.includes("Search") && (text.includes("") || text.includes("K"))) {
339
+ const ariaLabel = (button.getAttribute("aria-label") || "").toLowerCase();
340
+ if (text.includes("Search") && (text.includes("⌘") || text.includes("K")) || ariaLabel.includes("search") || text === "Open Search") {
340
341
  e.preventDefault();
341
342
  e.stopPropagation();
342
343
  e.stopImmediatePropagation();
@@ -2,6 +2,7 @@ import { DocsAIFeatures } from "./docs-ai-features.mjs";
2
2
  import { DocsCommandSearch } from "./docs-command-search.mjs";
3
3
  import { serializeIcon } from "./serialize-icon.mjs";
4
4
  import { DocsPageClient } from "./docs-page-client.mjs";
5
+ import { SidebarSearchWithAI } from "./sidebar-search-ai.mjs";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  import fs from "node:fs";
7
8
  import path from "node:path";
@@ -32,7 +33,7 @@ function hasChildPages(dir) {
32
33
  }
33
34
  return false;
34
35
  }
35
- function buildTree(config) {
36
+ function buildTree(config, flat = false) {
36
37
  const docsDir = path.join(process.cwd(), "app", config.entry);
37
38
  const icons = config.icons;
38
39
  const ordering = config.ordering;
@@ -68,7 +69,11 @@ function buildTree(config) {
68
69
  url,
69
70
  icon
70
71
  },
71
- children: folderChildren
72
+ children: folderChildren,
73
+ ...flat ? {
74
+ collapsible: false,
75
+ defaultOpen: true
76
+ } : {}
72
77
  };
73
78
  }
74
79
  return {
@@ -227,7 +232,8 @@ function resolveSidebar(sidebar) {
227
232
  component: sidebar.component,
228
233
  footer: sidebar.footer,
229
234
  banner: sidebar.banner,
230
- collapsible: sidebar.collapsible
235
+ collapsible: sidebar.collapsible,
236
+ flat: sidebar.flat
231
237
  };
232
238
  }
233
239
  const COLOR_MAP = {
@@ -300,6 +306,26 @@ function TypographyStyle({ typography }) {
300
306
  if (!css) return null;
301
307
  return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } });
302
308
  }
309
+ function LayoutStyle({ layout }) {
310
+ if (!layout) return null;
311
+ const rootVars = [];
312
+ const gridVars = [];
313
+ if (layout.sidebarWidth) {
314
+ const v = `--fd-sidebar-width: ${layout.sidebarWidth}px`;
315
+ rootVars.push(`${v};`);
316
+ gridVars.push(`${v} !important;`);
317
+ }
318
+ if (layout.contentWidth) rootVars.push(`--fd-content-width: ${layout.contentWidth}px;`);
319
+ if (layout.tocWidth) {
320
+ const v = `--fd-toc-width: ${layout.tocWidth}px`;
321
+ rootVars.push(`${v};`);
322
+ gridVars.push(`${v} !important;`);
323
+ }
324
+ if (rootVars.length === 0) return null;
325
+ const parts = [`:root {\n ${rootVars.join("\n ")}\n}`];
326
+ if (gridVars.length > 0) parts.push(`[style*="fd-sidebar-col"] {\n ${gridVars.join("\n ")}\n}`);
327
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: parts.join("\n") } });
328
+ }
303
329
  function createDocsLayout(config) {
304
330
  const tocConfig = config.theme?.ui?.layout?.toc;
305
331
  const tocEnabled = tocConfig?.enabled !== false;
@@ -309,15 +335,22 @@ function createDocsLayout(config) {
309
335
  const themeSwitch = resolveThemeSwitch(config.themeToggle);
310
336
  const toggleConfig = typeof config.themeToggle === "object" ? config.themeToggle : void 0;
311
337
  const forcedTheme = themeSwitch.enabled === false && toggleConfig?.default && toggleConfig.default !== "system" ? toggleConfig.default : void 0;
312
- const sidebarProps = resolveSidebar(config.sidebar);
338
+ const resolvedSidebar = resolveSidebar(config.sidebar);
339
+ const sidebarFlat = resolvedSidebar.flat;
340
+ const { flat: _sidebarFlat, ...sidebarProps } = resolvedSidebar;
313
341
  const breadcrumbConfig = config.breadcrumb;
314
342
  const breadcrumbEnabled = breadcrumbConfig === void 0 || breadcrumbConfig === true || typeof breadcrumbConfig === "object" && breadcrumbConfig.enabled !== false;
315
343
  const colors = config.theme?._userColorOverrides;
316
344
  const typography = config.theme?.ui?.typography;
345
+ const layoutDimensions = config.theme?.ui?.layout;
317
346
  const pageActions = config.pageActions;
318
347
  const copyMarkdownEnabled = resolveBool(pageActions?.copyMarkdown);
319
348
  const openDocsEnabled = resolveBool(pageActions?.openDocs);
320
349
  const pageActionsPosition = pageActions?.position ?? "below-title";
350
+ const pageActionsAlignment = pageActions?.alignment ?? "left";
351
+ const lastUpdatedRaw = config.lastUpdated;
352
+ const lastUpdatedEnabled = lastUpdatedRaw !== false && (typeof lastUpdatedRaw !== "object" || lastUpdatedRaw.enabled !== false);
353
+ const lastUpdatedPosition = typeof lastUpdatedRaw === "object" ? lastUpdatedRaw.position ?? "footer" : "footer";
321
354
  const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
322
355
  name: p.name,
323
356
  urlTemplate: p.urlTemplate,
@@ -340,16 +373,18 @@ function createDocsLayout(config) {
340
373
  const descriptionMap = buildDescriptionMap(config.entry);
341
374
  return function DocsLayoutWrapper({ children }) {
342
375
  return /* @__PURE__ */ jsxs(DocsLayout, {
343
- tree: buildTree(config),
376
+ tree: buildTree(config, !!sidebarFlat),
344
377
  nav: {
345
378
  title: navTitle,
346
379
  url: navUrl
347
380
  },
348
381
  themeSwitch,
349
382
  sidebar: sidebarProps,
383
+ ...aiMode === "sidebar-icon" && aiEnabled ? { searchToggle: { components: { lg: /* @__PURE__ */ jsx(SidebarSearchWithAI, {}) } } } : {},
350
384
  children: [
351
385
  /* @__PURE__ */ jsx(ColorStyle, { colors }),
352
386
  /* @__PURE__ */ jsx(TypographyStyle, { typography }),
387
+ /* @__PURE__ */ jsx(LayoutStyle, { layout: layoutDimensions }),
353
388
  forcedTheme && /* @__PURE__ */ jsx(ForcedThemeScript, { theme: forcedTheme }),
354
389
  /* @__PURE__ */ jsx(DocsCommandSearch, {}),
355
390
  aiEnabled && /* @__PURE__ */ jsx(DocsAIFeatures, {
@@ -370,10 +405,13 @@ function createDocsLayout(config) {
370
405
  openDocs: openDocsEnabled,
371
406
  openDocsProviders,
372
407
  pageActionsPosition,
408
+ pageActionsAlignment,
373
409
  githubUrl,
374
410
  githubBranch,
375
411
  githubDirectory,
376
412
  lastModifiedMap,
413
+ lastUpdatedEnabled,
414
+ lastUpdatedPosition,
377
415
  descriptionMap,
378
416
  children
379
417
  })
@@ -19,6 +19,8 @@ interface DocsPageClientProps {
19
19
  openDocsProviders?: SerializedProvider[];
20
20
  /** Where to render page actions relative to the title */
21
21
  pageActionsPosition?: "above-title" | "below-title";
22
+ /** Horizontal alignment of page action buttons */
23
+ pageActionsAlignment?: "left" | "right";
22
24
  /** GitHub repository URL (e.g. "https://github.com/user/repo") */
23
25
  githubUrl?: string;
24
26
  /** GitHub branch name @default "main" */
@@ -27,6 +29,10 @@ interface DocsPageClientProps {
27
29
  githubDirectory?: string;
28
30
  /** Map of pathname → formatted last-modified date string */
29
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";
30
36
  /** Map of pathname → frontmatter description */
31
37
  descriptionMap?: Record<string, string>;
32
38
  /** Frontmatter description to display below the page title (overrides descriptionMap) */
@@ -42,10 +48,13 @@ declare function DocsPageClient({
42
48
  openDocs,
43
49
  openDocsProviders,
44
50
  pageActionsPosition,
51
+ pageActionsAlignment,
45
52
  githubUrl,
46
53
  githubBranch,
47
54
  githubDirectory,
48
55
  lastModifiedMap,
56
+ lastUpdatedEnabled,
57
+ lastUpdatedPosition,
49
58
  descriptionMap,
50
59
  description,
51
60
  children
@@ -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";
@@ -62,10 +63,11 @@ function buildGithubFileUrl(githubUrl, branch, pathname, directory) {
62
63
  const segments = pathname.replace(/^\//, "").replace(/\/$/, "");
63
64
  return `${githubUrl}/tree/${branch}/${directory ? `${directory}/` : ""}app/${segments}/page.mdx`;
64
65
  }
65
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", githubUrl, githubBranch = "main", githubDirectory, lastModifiedMap, descriptionMap, description, 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", descriptionMap, description, children }) {
66
67
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
67
68
  const [toc, setToc] = useState([]);
68
69
  const pathname = usePathname();
70
+ const [actionsPortalTarget, setActionsPortalTarget] = useState(null);
69
71
  const pageDescription = description ?? descriptionMap?.[pathname.replace(/\/$/, "") || "/"];
70
72
  useEffect(() => {
71
73
  if (!tocEnabled) return;
@@ -104,8 +106,57 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
104
106
  const showActions = copyMarkdown || openDocs;
105
107
  const githubFileUrl = githubUrl ? buildGithubFileUrl(githubUrl, githubBranch, pathname, githubDirectory) : void 0;
106
108
  const normalizedPath = pathname.replace(/\/$/, "") || "/";
107
- const lastModified = lastModifiedMap?.[normalizedPath];
108
- 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;
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
+ ]);
109
160
  return /* @__PURE__ */ jsxs(DocsPage, {
110
161
  toc,
111
162
  tableOfContent: {
@@ -122,14 +173,11 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
122
173
  pathname,
123
174
  entry
124
175
  }),
125
- showActions && /* @__PURE__ */ jsx("div", {
126
- "data-actions-position": pageActionsPosition,
127
- children: /* @__PURE__ */ jsx(PageActions, {
128
- copyMarkdown,
129
- openDocs,
130
- providers: openDocsProviders
131
- })
132
- }),
176
+ showActions && actionsPortalTarget && createPortal(/* @__PURE__ */ jsx(PageActions, {
177
+ copyMarkdown,
178
+ openDocs,
179
+ providers: openDocsProviders
180
+ }), actionsPortalTarget),
133
181
  /* @__PURE__ */ jsxs(DocsBody, {
134
182
  style: {
135
183
  display: "flex",
@@ -140,9 +188,9 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
140
188
  children
141
189
  }), showFooter && /* @__PURE__ */ jsxs("div", {
142
190
  className: "not-prose fd-page-footer",
143
- children: [githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }), lastModified && /* @__PURE__ */ jsxs("span", {
144
- className: "fd-last-updated",
145
- children: ["Last updated: ", lastModified]
191
+ children: [githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }), showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
192
+ className: "fd-last-updated-footer",
193
+ children: ["Last updated ", lastModified]
146
194
  })]
147
195
  })]
148
196
  })