@farming-labs/theme 0.0.2-beta.18 → 0.0.2-beta.20

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 };
@@ -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,32 @@ 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 desktopRootVars = [];
313
+ const desktopGridVars = [];
314
+ if (layout.sidebarWidth) {
315
+ const v = `--fd-sidebar-width: ${layout.sidebarWidth}px`;
316
+ desktopRootVars.push(`${v};`);
317
+ desktopGridVars.push(`${v} !important;`);
318
+ }
319
+ if (layout.contentWidth) rootVars.push(`--fd-content-width: ${layout.contentWidth}px;`);
320
+ if (layout.tocWidth) {
321
+ const v = `--fd-toc-width: ${layout.tocWidth}px`;
322
+ desktopRootVars.push(`${v};`);
323
+ desktopGridVars.push(`${v} !important;`);
324
+ }
325
+ if (rootVars.length === 0 && desktopRootVars.length === 0) return null;
326
+ const parts = [];
327
+ if (rootVars.length > 0) parts.push(`:root {\n ${rootVars.join("\n ")}\n}`);
328
+ if (desktopRootVars.length > 0) {
329
+ const inner = [`:root {\n ${desktopRootVars.join("\n ")}\n }`];
330
+ if (desktopGridVars.length > 0) inner.push(`[style*="fd-sidebar-col"] {\n ${desktopGridVars.join("\n ")}\n }`);
331
+ parts.push(`@media (min-width: 1024px) {\n ${inner.join("\n ")}\n}`);
332
+ }
333
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: parts.join("\n") } });
334
+ }
303
335
  function createDocsLayout(config) {
304
336
  const tocConfig = config.theme?.ui?.layout?.toc;
305
337
  const tocEnabled = tocConfig?.enabled !== false;
@@ -309,15 +341,22 @@ function createDocsLayout(config) {
309
341
  const themeSwitch = resolveThemeSwitch(config.themeToggle);
310
342
  const toggleConfig = typeof config.themeToggle === "object" ? config.themeToggle : void 0;
311
343
  const forcedTheme = themeSwitch.enabled === false && toggleConfig?.default && toggleConfig.default !== "system" ? toggleConfig.default : void 0;
312
- const sidebarProps = resolveSidebar(config.sidebar);
344
+ const resolvedSidebar = resolveSidebar(config.sidebar);
345
+ const sidebarFlat = resolvedSidebar.flat;
346
+ const { flat: _sidebarFlat, ...sidebarProps } = resolvedSidebar;
313
347
  const breadcrumbConfig = config.breadcrumb;
314
348
  const breadcrumbEnabled = breadcrumbConfig === void 0 || breadcrumbConfig === true || typeof breadcrumbConfig === "object" && breadcrumbConfig.enabled !== false;
315
349
  const colors = config.theme?._userColorOverrides;
316
350
  const typography = config.theme?.ui?.typography;
351
+ const layoutDimensions = config.theme?.ui?.layout;
317
352
  const pageActions = config.pageActions;
318
353
  const copyMarkdownEnabled = resolveBool(pageActions?.copyMarkdown);
319
354
  const openDocsEnabled = resolveBool(pageActions?.openDocs);
320
355
  const pageActionsPosition = pageActions?.position ?? "below-title";
356
+ const pageActionsAlignment = pageActions?.alignment ?? "left";
357
+ const lastUpdatedRaw = config.lastUpdated;
358
+ const lastUpdatedEnabled = lastUpdatedRaw !== false && (typeof lastUpdatedRaw !== "object" || lastUpdatedRaw.enabled !== false);
359
+ const lastUpdatedPosition = typeof lastUpdatedRaw === "object" ? lastUpdatedRaw.position ?? "footer" : "footer";
321
360
  const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
322
361
  name: p.name,
323
362
  urlTemplate: p.urlTemplate,
@@ -340,16 +379,18 @@ function createDocsLayout(config) {
340
379
  const descriptionMap = buildDescriptionMap(config.entry);
341
380
  return function DocsLayoutWrapper({ children }) {
342
381
  return /* @__PURE__ */ jsxs(DocsLayout, {
343
- tree: buildTree(config),
382
+ tree: buildTree(config, !!sidebarFlat),
344
383
  nav: {
345
384
  title: navTitle,
346
385
  url: navUrl
347
386
  },
348
387
  themeSwitch,
349
388
  sidebar: sidebarProps,
389
+ ...aiMode === "sidebar-icon" && aiEnabled ? { searchToggle: { components: { lg: /* @__PURE__ */ jsx(SidebarSearchWithAI, {}) } } } : {},
350
390
  children: [
351
391
  /* @__PURE__ */ jsx(ColorStyle, { colors }),
352
392
  /* @__PURE__ */ jsx(TypographyStyle, { typography }),
393
+ /* @__PURE__ */ jsx(LayoutStyle, { layout: layoutDimensions }),
353
394
  forcedTheme && /* @__PURE__ */ jsx(ForcedThemeScript, { theme: forcedTheme }),
354
395
  /* @__PURE__ */ jsx(DocsCommandSearch, {}),
355
396
  aiEnabled && /* @__PURE__ */ jsx(DocsAIFeatures, {
@@ -370,10 +411,13 @@ function createDocsLayout(config) {
370
411
  openDocs: openDocsEnabled,
371
412
  openDocsProviders,
372
413
  pageActionsPosition,
414
+ pageActionsAlignment,
373
415
  githubUrl,
374
416
  githubBranch,
375
417
  githubDirectory,
376
418
  lastModifiedMap,
419
+ lastUpdatedEnabled,
420
+ lastUpdatedPosition,
377
421
  descriptionMap,
378
422
  children
379
423
  })
@@ -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
  })