Skip to content

Commit e180bfa

Browse files
feat(docs): add theme.page-actions config to control page actions display style (#4840)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent e00333b commit e180bfa

File tree

18 files changed

+257
-40
lines changed

18 files changed

+257
-40
lines changed

fern/apis/fdr/definition/docs/v1/commons/commons.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ types:
214214
sidebar: optional<DocsSidebarConfig>
215215
body: optional<DocsBodyConfig>
216216
tabs: optional<DocsTabsConfig>
217+
page-actions: optional<DocsPageActionsConfig>
217218

218219
DocsSidebarConfig:
219220
enum:
@@ -230,6 +231,11 @@ types:
230231
- default
231232
- bubble
232233

234+
DocsPageActionsConfig:
235+
enum:
236+
- default
237+
- toolbar
238+
233239
DocsSettingsConfig:
234240
properties:
235241
searchText: optional<string>

packages/commons/i18n/src/locales/en/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
"connectToCursor": "Connect to Cursor",
5757
"resetTokenToDefault": "Reset token to default",
5858
"close": "Close",
59-
"cancel": "Cancel"
59+
"cancel": "Cancel",
60+
"moreActions": "More actions"
6061
},
6162
"search": {
6263
"search": "Search",

packages/commons/i18n/src/locales/es/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
"connectToCursor": "Conectar a Cursor",
5757
"resetTokenToDefault": "Restablecer token a predeterminado",
5858
"close": "Cerrar",
59-
"cancel": "Cancelar"
59+
"cancel": "Cancelar",
60+
"moreActions": "Más acciones"
6061
},
6162
"search": {
6263
"search": "Buscar",

packages/fdr-sdk/src/client/generated/api/resources/docs/resources/v1/resources/commons/resources/commons/types/DocsPageActionsConfig.ts

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/fdr-sdk/src/client/generated/api/resources/docs/resources/v1/resources/commons/resources/commons/types/DocsThemeConfig.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/fdr-sdk/src/client/generated/api/resources/docs/resources/v1/resources/commons/resources/commons/types/index.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/fern-docs/bundle/src/components/PageActionsDropdown.tsx

Lines changed: 189 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useSetAtom } from "jotai";
77
import { Check, ChevronDown, Copy } from "lucide-react";
88
import type { ParamValue } from "next/dist/server/request/params";
99
import { useParams } from "next/navigation";
10-
import { useState } from "react";
10+
import { Fragment, useState } from "react";
1111
import { capturePosthogEventInternal } from "@/components/analytics/posthog";
1212
import { useIsAskAiEnabled } from "@/state/search";
1313
import { searchPanelOpenAtom, useSetPageContext } from "@/state/search-panel";
@@ -17,11 +17,13 @@ import { OpenAISearchOption, Separator } from "./PageActionsDropdownOptions";
1717
export function PageActionsDropdown({
1818
markdownPromise,
1919
pageActionOptions,
20-
lang
20+
lang,
21+
style = "default"
2122
}: {
2223
markdownPromise?: Promise<{ content: string; contentType: "markdown" | "mdx" } | undefined>;
2324
pageActionOptions: FernDropdown.PageActionOption[];
2425
lang: string;
26+
style?: "default" | "toolbar";
2527
}) {
2628
const [showCopied, setShowCopied] = useState<boolean>(false);
2729
const { domain, slug } = useParams();
@@ -40,51 +42,207 @@ export function PageActionsDropdown({
4042
return null;
4143
}
4244

43-
const handleValueChange = async (value: string) => {
44-
if (value === "copy-page") {
45-
window.focus();
45+
const handleCopyPage = async () => {
46+
window.focus();
4647

47-
if (markdownPromise) {
48-
try {
49-
const markdownResult = await markdownPromise;
50-
const markdown = markdownResult?.content;
48+
if (markdownPromise) {
49+
try {
50+
const markdownResult = await markdownPromise;
51+
const markdown = markdownResult?.content;
5152

52-
if (markdown) {
53-
await navigator.clipboard.writeText(markdown);
54-
capturePosthogEventInternal("page_actions_dropdown", {
55-
type: "copy-option",
56-
page_location: window.location.pathname
57-
});
53+
if (markdown) {
54+
await navigator.clipboard.writeText(markdown);
55+
capturePosthogEventInternal("page_actions_dropdown", {
56+
type: "copy-option",
57+
page_location: window.location.pathname
58+
});
5859

59-
setShowCopied(true);
60+
setShowCopied(true);
6061

61-
setTimeout(() => {
62-
setShowCopied(false);
63-
}, 2000);
64-
}
65-
} catch (error) {
66-
console.error("Failed to copy to clipboard:", error);
62+
setTimeout(() => {
63+
setShowCopied(false);
64+
}, 2000);
6765
}
66+
} catch (error) {
67+
console.error("Failed to copy to clipboard:", error);
6868
}
69+
}
70+
};
71+
72+
const handleAskAI = () => {
73+
const pageContext = {
74+
title: document.title,
75+
url: constructPageUrl(domain, slug)
76+
};
77+
setPageContext(pageContext);
78+
setSearchPanelState(true);
79+
capturePosthogEventInternal("page_actions_dropdown", {
80+
type: "ai-search",
81+
page_location: window.location.pathname
82+
});
83+
};
84+
85+
const handleValueChange = async (value: string) => {
86+
if (value === "copy-page") {
87+
await handleCopyPage();
6988
} else if (value === "open-ai-search") {
70-
const pageContext = {
71-
title: document.title,
72-
url: constructPageUrl(domain, slug)
73-
};
74-
setPageContext(pageContext);
75-
setSearchPanelState(true);
89+
handleAskAI();
90+
} else if (value === "view-as-markdown") {
7691
capturePosthogEventInternal("page_actions_dropdown", {
77-
type: "ai-search",
92+
type: "markdown",
7893
page_location: window.location.pathname
7994
});
80-
} else if (value === "view-as-markdown") {
95+
} else {
8196
capturePosthogEventInternal("page_actions_dropdown", {
82-
type: "markdown",
97+
type: value,
8398
page_location: window.location.pathname
8499
});
85100
}
86101
};
87102

103+
if (style === "toolbar") {
104+
const renderInlineLink = (option: FernDropdown.PageActionOption) => {
105+
if (option.type === "separator") {
106+
return null;
107+
}
108+
109+
const { value, label, href } = option;
110+
111+
if (value === "copy-page") {
112+
return (
113+
<button
114+
key={value}
115+
onClick={() => {
116+
capturePosthogEventInternal("page_actions_dropdown", {
117+
type: "copy-button",
118+
page_location: window.location.pathname
119+
});
120+
void handleCopyPage();
121+
}}
122+
className="hover:underline text-(color:--grayscale-a11) whitespace-nowrap"
123+
>
124+
{showCopied ? t(lang).buttons.copied : t(lang).buttons.copyPage}
125+
</button>
126+
);
127+
}
128+
129+
if (value === "open-ai-search") {
130+
return (
131+
<button
132+
key={value}
133+
onClick={handleAskAI}
134+
className="hover:underline text-(color:--grayscale-a11) whitespace-nowrap"
135+
>
136+
{label}
137+
</button>
138+
);
139+
}
140+
141+
if (value === "view-as-markdown" && href) {
142+
return (
143+
<a
144+
key={value}
145+
href={href}
146+
target="_blank"
147+
rel="noopener noreferrer"
148+
onClick={() => {
149+
capturePosthogEventInternal("page_actions_dropdown", {
150+
type: "markdown",
151+
page_location: window.location.pathname
152+
});
153+
}}
154+
className="hover:underline text-(color:--grayscale-a11) whitespace-nowrap"
155+
>
156+
{label}
157+
</a>
158+
);
159+
}
160+
161+
if (href) {
162+
return (
163+
<a
164+
key={value}
165+
href={href}
166+
target="_blank"
167+
rel="noopener noreferrer"
168+
onClick={() => {
169+
capturePosthogEventInternal("page_actions_dropdown", {
170+
type: value,
171+
page_location: window.location.pathname
172+
});
173+
}}
174+
className="hover:underline text-(color:--grayscale-a11) whitespace-nowrap"
175+
>
176+
{label}
177+
</a>
178+
);
179+
}
180+
181+
return (
182+
<button
183+
key={value}
184+
onClick={() => {
185+
capturePosthogEventInternal("page_actions_dropdown", {
186+
type: value,
187+
page_location: window.location.pathname
188+
});
189+
}}
190+
className="hover:underline text-(color:--grayscale-a11) whitespace-nowrap"
191+
>
192+
{label}
193+
</button>
194+
);
195+
};
196+
197+
const allOptions = isAskAiEnabled ? [OpenAISearchOption({ lang }), ...pageActionOptions] : pageActionOptions;
198+
const items = allOptions.filter((option) => option.type !== "separator");
199+
200+
if (items.length === 0) {
201+
return null;
202+
}
203+
204+
const MAX_VISIBLE = 3;
205+
const visibleItems = items.slice(0, MAX_VISIBLE);
206+
const overflowItems = items.slice(MAX_VISIBLE);
207+
208+
return (
209+
<div className="fern-page-actions flex flex-wrap items-center text-sm">
210+
{visibleItems.map((item, i) => (
211+
<Fragment key={item.value}>
212+
{i > 0 && (
213+
<span aria-hidden="true" className="px-1 text-(color:--grayscale-a8)">
214+
|
215+
</span>
216+
)}
217+
{renderInlineLink(item)}
218+
</Fragment>
219+
))}
220+
{overflowItems.length > 0 && (
221+
<>
222+
{visibleItems.length > 0 && (
223+
<span aria-hidden="true" className="px-1 text-(color:--grayscale-a8)">
224+
|
225+
</span>
226+
)}
227+
<FernDropdown
228+
options={overflowItems}
229+
onValueChange={(value) => void handleValueChange(value)}
230+
dropdownMenuElement={<a target="_blank" rel="noopener noreferrer" />}
231+
lang={lang}
232+
>
233+
<button
234+
aria-label={t(lang).buttons.moreActions}
235+
className="px-1 text-(color:--grayscale-a11)"
236+
>
237+
238+
</button>
239+
</FernDropdown>
240+
</>
241+
)}
242+
</div>
243+
);
244+
}
245+
88246
return (
89247
<div className="fern-page-actions">
90248
<FernButton

packages/fern-docs/bundle/src/components/PageHeader.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export function PageHeader({
2828
showRssFeedButton,
2929
filters,
3030
showBackIcon,
31-
lang
31+
lang,
32+
pageActionsStyle = "default"
3233
}: {
3334
slug: string;
3435
serialize: MdxSerializer;
@@ -42,10 +43,10 @@ export function PageHeader({
4243
markdownPromise?: Promise<{ content: string; contentType: "markdown" | "mdx" } | undefined>;
4344
pageActionOptions?: FernDropdown.PageActionOption[];
4445
showRssFeedButton?: boolean;
45-
// tags for the changelog section
4646
filters?: string[];
4747
showBackIcon?: boolean;
4848
lang: string;
49+
pageActionsStyle?: "default" | "toolbar";
4950
}) {
5051
return (
5152
<header className="my-8 space-y-2">
@@ -93,6 +94,7 @@ export function PageHeader({
9394
markdownPromise={markdownPromise}
9495
pageActionOptions={pageActionOptions}
9596
lang={lang}
97+
style={pageActionsStyle}
9698
/>
9799
</div>
98100
)}

0 commit comments

Comments
 (0)