Skip to content

Commit 35fb647

Browse files
authored
[Docs Site] Add Copy Page dropdown for Markdown (#21404)
* [Docs Site] Add Copy Page dropdown for Markdown * formatting * add visual feedback * tweaks
1 parent b7f44ea commit 35fb647

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

src/components/CopyPageButton.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
useFloating,
3+
useInteractions,
4+
useClick,
5+
useDismiss,
6+
shift,
7+
offset,
8+
autoUpdate,
9+
FloatingPortal,
10+
} from "@floating-ui/react";
11+
import { useState } from "react";
12+
import {
13+
PiMarkdownLogo,
14+
PiClipboardTextLight,
15+
PiArrowSquareOutLight,
16+
PiCheckCircleLight,
17+
PiXCircleLight,
18+
} from "react-icons/pi";
19+
20+
type CopyState = "idle" | "success" | "error";
21+
22+
export default function CopyPageButton() {
23+
const [isOpen, setIsOpen] = useState(false);
24+
const [copyState, setCopyState] = useState<CopyState>("idle");
25+
26+
const { refs, floatingStyles, context } = useFloating({
27+
open: isOpen,
28+
onOpenChange: setIsOpen,
29+
middleware: [shift(), offset(5)],
30+
whileElementsMounted: autoUpdate,
31+
});
32+
33+
const click = useClick(context);
34+
const dismiss = useDismiss(context);
35+
36+
const { getReferenceProps, getFloatingProps } = useInteractions([
37+
click,
38+
dismiss,
39+
]);
40+
41+
const handleViewMarkdown = () => {
42+
const markdownUrl = new URL("index.md", window.location.href).toString();
43+
window.open(markdownUrl, "_blank");
44+
};
45+
46+
const handleCopyMarkdown = async () => {
47+
const markdownUrl = new URL("index.md", window.location.href).toString();
48+
try {
49+
const response = await fetch(markdownUrl);
50+
51+
if (!response.ok) {
52+
throw new Error(`Received ${response.status} on ${response.url}`);
53+
}
54+
55+
const markdown = await response.text();
56+
await navigator.clipboard.writeText(markdown);
57+
58+
setCopyState("success");
59+
setTimeout(() => {
60+
setCopyState("idle");
61+
}, 1500);
62+
} catch (error) {
63+
console.error("Failed to copy Markdown:", error);
64+
65+
setCopyState("error");
66+
setTimeout(() => {
67+
setCopyState("idle");
68+
}, 1500);
69+
}
70+
};
71+
72+
const options = [
73+
{
74+
label: "Copy Page as Markdown",
75+
description: "Copy the raw Markdown content to clipboard",
76+
icon: PiClipboardTextLight,
77+
onClick: handleCopyMarkdown,
78+
},
79+
{
80+
label: "View Page as Markdown",
81+
description: "Open the Markdown file in a new tab",
82+
icon: PiArrowSquareOutLight,
83+
onClick: handleViewMarkdown,
84+
},
85+
];
86+
87+
const getButtonContent = () => {
88+
if (copyState === "success") {
89+
return (
90+
<>
91+
<span>Copied!</span>
92+
<PiCheckCircleLight className="text-green-600" />
93+
</>
94+
);
95+
}
96+
97+
if (copyState === "error") {
98+
return (
99+
<>
100+
<span>Failed</span>
101+
<PiXCircleLight className="text-red-600" />
102+
</>
103+
);
104+
}
105+
106+
return (
107+
<>
108+
<span>Copy Page</span>
109+
<PiMarkdownLogo />
110+
</>
111+
);
112+
};
113+
114+
return (
115+
<>
116+
<button
117+
ref={refs.setReference}
118+
{...getReferenceProps()}
119+
className="inline-flex h-8 min-w-32 cursor-pointer items-center justify-center gap-2 rounded border border-[--sl-color-hairline] bg-transparent px-3 text-sm text-black hover:bg-[--sl-color-bg-nav]"
120+
>
121+
{getButtonContent()}
122+
</button>
123+
{isOpen && (
124+
<FloatingPortal>
125+
<ul
126+
ref={refs.setFloating}
127+
style={floatingStyles}
128+
{...getFloatingProps()}
129+
className="list-none rounded border border-[--sl-color-hairline] bg-[--sl-color-bg] pl-0 shadow-md"
130+
>
131+
{options.map(({ label, description, icon: Icon, onClick }) => (
132+
<li key={label}>
133+
<button
134+
onClick={onClick}
135+
className="relative block w-full cursor-pointer bg-transparent px-3 py-2 text-left text-black no-underline hover:bg-[--sl-color-bg-nav]"
136+
>
137+
<div className="flex items-center gap-2 text-sm">
138+
<Icon />
139+
{label}
140+
</div>
141+
<div className="ml-6 mt-0.5 text-xs text-[--sl-color-gray-3]">
142+
{description}
143+
</div>
144+
</button>
145+
</li>
146+
))}
147+
</ul>
148+
</FloatingPortal>
149+
)}
150+
</>
151+
);
152+
}

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { default as AvailableNotifications } from "./AvailableNotifications.astr
1111
export { default as CompatibilityFlag } from "./CompatibilityFlag.astro";
1212
export { default as CompatibilityFlags } from "./CompatibilityFlags.astro";
1313
export { default as ComponentsUsage } from "./ComponentsUsage.astro";
14+
export { default as CopyPageButton } from "./CopyPageButton.tsx";
1415
export { default as CURL } from "./CURL.astro";
1516
export { default as Description } from "./Description.astro";
1617
export { default as Details } from "./Details.astro";

src/components/overrides/PageTitle.astro

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Aside } from "@astrojs/starlight/components";
99
import Description from "~/components/Description.astro";
1010
import SpotlightAuthorDetails from "~/components/SpotlightAuthorDetails.astro";
1111
import LastReviewed from "~/components/LastReviewed.astro";
12+
import CopyPageButton from "~/components/CopyPageButton.tsx";
1213
1314
import { getEntry } from "astro:content";
1415
@@ -113,6 +114,12 @@ const hideBreadcrumbs = Astro.locals.starlightRoute.hideBreadcrumbs;
113114

114115
{component && <Aside>To see a list of pages this component is used on, please visit the <a href={`/style-guide/components/usage/#${component.toLowerCase()}`}>usage page</a>.</Aside>}
115116

117+
{frontmatter.template !== "splash" && (
118+
<div class="flex justify-end items-center">
119+
<CopyPageButton client:idle />
120+
</div>
121+
)}
122+
116123
<style>
117124
:root {
118125
--color-link-breadcrumbs: var(--sl-color-text-accent);

0 commit comments

Comments
 (0)