From 2a09a4dc630f14d342a5532cb8b9bef5c70f12aa Mon Sep 17 00:00:00 2001 From: Guillermo Alejandro Gallardo Diez Date: Thu, 29 Jan 2026 16:11:22 +0100 Subject: [PATCH 1/3] feat: added button to copy markdown and install mcp --- .../components/CopyMarkdownButton/index.js | 253 ++++++++++++++++++ .../CopyMarkdownButton/styles.module.css | 123 +++++++++ website/src/theme/DocItem/Content/index.js | 48 ++++ .../theme/DocItem/Content/styles.module.css | 35 +++ 4 files changed, 459 insertions(+) create mode 100644 website/src/components/CopyMarkdownButton/index.js create mode 100644 website/src/components/CopyMarkdownButton/styles.module.css create mode 100644 website/src/theme/DocItem/Content/index.js create mode 100644 website/src/theme/DocItem/Content/styles.module.css diff --git a/website/src/components/CopyMarkdownButton/index.js b/website/src/components/CopyMarkdownButton/index.js new file mode 100644 index 00000000000..8b0242f5204 --- /dev/null +++ b/website/src/components/CopyMarkdownButton/index.js @@ -0,0 +1,253 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useLocation } from '@docusaurus/router'; +import styles from './styles.module.css'; + +const CHATGPT_PROMPT = `Read from {{document}} so I can ask you questions about it` +const CLAUDE_PROMPT = `Read from {{document}} so I can answer questions about it` + +export default function CopyMarkdownButton({docId}) { + const [copied, setCopied] = useState(false); + const [loading, setLoading] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [content, setContent] = useState(null); + const location = useLocation(); + const dropdownRef = useRef(null); + + const browserUrl = `${window.location.origin}${location.pathname}`; + const mdUrl = `${window.location.origin}${docId}.md`; + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const fetchMarkdown = async () => { + if (content) return content; + try { + const response = await fetch(mdUrl); + if (!response.ok) { + throw new Error('Failed to fetch markdown content'); + } + const text = await response.text(); + setContent(text); + return text; + } catch (error) { + console.error('Failed to fetch markdown:', error); + throw error; + } + }; + + const handleCopy = async () => { + try { + setLoading(true); + const text = await fetchMarkdown(); + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + alert('Failed to copy markdown content. The .md file may not be available.'); + } finally { + setLoading(false); + } + }; + + const handleOpenInChatGPT = async () => { + const prompt = CHATGPT_PROMPT.replace('{{document}}', browserUrl); + window.open(`https://chatgpt.com/?prompt=${prompt}`, '_blank'); + }; + + const handleOpenInClaude = async () => { + const prompt = CLAUDE_PROMPT.replace('{{document}}', browserUrl); + window.open(`https://claude.ai/new?q=${prompt}`, '_blank'); + }; + + const handleConnectMCPCursor = () => { + window.open(`cursor:mcp/install?%7B%22name%22%3A%22NEAR%20Docs%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fdocs.near.org%2Fmcp%22%7D`, '_blank'); + setIsOpen(false); + }; + + const handleConnectMCPVSCode = () => { + window.open(`vscode:mcp/install?%7B%22name%22%3A%22NEAR%20Docs%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fdocs.near.org%2Fmcp%22%7D`, '_blank'); + setIsOpen(false); + }; + + return ( +
+
+ + +
+ + {isOpen && ( +
+ + + + +
+ )} +
+ ); +} + +function MenuIcon() { + return ( + + + + + + ); +} + +function CopyIcon() { + return ( + + + + + ); +} + +function CheckIcon() { + return ( + + + + ); +} + +function ChevronIcon({ isOpen }) { + return ( + + + + ); +} + +function ExternalLinkIcon() { + return ( + + + + + + ); +} + +function LinkIcon() { + return ( + + + + + ); +} diff --git a/website/src/components/CopyMarkdownButton/styles.module.css b/website/src/components/CopyMarkdownButton/styles.module.css new file mode 100644 index 00000000000..2f66c562bc7 --- /dev/null +++ b/website/src/components/CopyMarkdownButton/styles.module.css @@ -0,0 +1,123 @@ +.container { + position: relative; + display: inline-block; +} + +.buttonGroup { + display: inline-flex; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + overflow: hidden; + margin: 8px 0; +} + +.copyButton { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-size: 13px; + font-weight: 500; + color: var(--ifm-color-emphasis-700); + background-color: var(--ifm-color-emphasis-100); + border: none; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 0; +} + +.copyButton:hover:not(:disabled) { + background-color: var(--ifm-color-emphasis-200); +} + +.copyButton:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +.dropdownButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + color: var(--ifm-color-emphasis-700); + background-color: var(--ifm-color-emphasis-100); + border: none; + border-left: 1px solid var(--ifm-color-emphasis-300); + cursor: pointer; + transition: all 0.2s ease; + border-radius: 0; +} + +.dropdownButton:hover:not(:disabled) { + background-color: var(--ifm-color-emphasis-200); +} + +.dropdownButton:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +.dropdown { + position: absolute; + right: 0; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + min-width: 200px; + overflow: hidden; +} + +.menuItem { + width: 100%; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + font-size: 13px; + color: var(--ifm-color-emphasis-700); + border: none; + cursor: pointer; + text-align: left; + transition: all 0.2s ease; + background-color: var(--ifm-color-emphasis-100); +} + +.menuItem:hover:not(:disabled) { + background-color: var(--ifm-color-emphasis-200); +} + +.menuItem:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.chevron { + transition: transform 0.2s ease; +} + +.chevron.open { + transform: rotate(180deg); +} + +.spinner { + width: 14px; + height: 14px; + border: 2px solid var(--ifm-color-emphasis-300); + border-top-color: var(--ifm-color-emphasis-700); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/website/src/theme/DocItem/Content/index.js b/website/src/theme/DocItem/Content/index.js new file mode 100644 index 00000000000..43dcbdc77ca --- /dev/null +++ b/website/src/theme/DocItem/Content/index.js @@ -0,0 +1,48 @@ +import React from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; +import Heading from '@theme/Heading'; +import MDXContent from '@theme/MDXContent'; +import CopyMarkdownButton from '@site/src/components/CopyMarkdownButton'; +import styles from './styles.module.css'; +/** + Title can be declared inside md content or declared through + front matter and added manually. To make both cases consistent, + the added title is added under the same div.markdown block + See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120 + + We render a "synthetic title" if: + - user doesn't ask to hide it with front matter + - the markdown content does not already contain a top-level h1 heading +*/ +function useSyntheticTitle() { + const {metadata, frontMatter, contentTitle} = useDoc(); + const shouldRender = + !frontMatter.hide_title && typeof contentTitle === 'undefined'; + if (!shouldRender) { + return null; + } + return metadata.title; +} +export default function DocItemContent({children}) { + const syntheticTitle = useSyntheticTitle(); + const {metadata} = useDoc(); + return ( +
+ {syntheticTitle && ( +
+
+
+ {syntheticTitle} +
+
+ +
+
+
+ )} + {children} +
+ ); +} diff --git a/website/src/theme/DocItem/Content/styles.module.css b/website/src/theme/DocItem/Content/styles.module.css new file mode 100644 index 00000000000..26085cd47da --- /dev/null +++ b/website/src/theme/DocItem/Content/styles.module.css @@ -0,0 +1,35 @@ +.headerRow { + display: flex; + align-items: center; + gap: 12px; + width: 100%; +} + +.titleCol { + flex: 1 1 auto; + min-width: 0; +} + +.buttonCol { + flex: 0 0 auto; + display: flex; + justify-content: flex-end; + margin-bottom: calc( var(--ifm-h1-vertical-rhythm-bottom) * var(--ifm-leading) ); +} + +@media (max-width: 640px) { + .headerRow { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .buttonCol { + width: 100%; + justify-content: flex-start; + } + + .titleCol h1 { + margin-bottom: 0rem !important; + } +} \ No newline at end of file From fac4511b5347d49595e955265b3e5e274ba0acc2 Mon Sep 17 00:00:00 2001 From: Guillermo Alejandro Gallardo Diez Date: Thu, 29 Jan 2026 16:20:50 +0100 Subject: [PATCH 2/3] fix: build --- website/src/components/CopyMarkdownButton/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/website/src/components/CopyMarkdownButton/index.js b/website/src/components/CopyMarkdownButton/index.js index 8b0242f5204..90f522c2f08 100644 --- a/website/src/components/CopyMarkdownButton/index.js +++ b/website/src/components/CopyMarkdownButton/index.js @@ -13,9 +13,6 @@ export default function CopyMarkdownButton({docId}) { const location = useLocation(); const dropdownRef = useRef(null); - const browserUrl = `${window.location.origin}${location.pathname}`; - const mdUrl = `${window.location.origin}${docId}.md`; - useEffect(() => { const handleClickOutside = (event) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { @@ -28,6 +25,8 @@ export default function CopyMarkdownButton({docId}) { }, []); const fetchMarkdown = async () => { + const mdUrl = `${window.location.origin}${docId}.md`; + if (content) return content; try { const response = await fetch(mdUrl); @@ -58,11 +57,13 @@ export default function CopyMarkdownButton({docId}) { }; const handleOpenInChatGPT = async () => { + const browserUrl = `${window.location.origin}${location.pathname}`; const prompt = CHATGPT_PROMPT.replace('{{document}}', browserUrl); window.open(`https://chatgpt.com/?prompt=${prompt}`, '_blank'); }; const handleOpenInClaude = async () => { + const browserUrl = `${window.location.origin}${location.pathname}`; const prompt = CLAUDE_PROMPT.replace('{{document}}', browserUrl); window.open(`https://claude.ai/new?q=${prompt}`, '_blank'); }; From 17f3178ff34668c4b0d4d2186dcda2721d3ece7e Mon Sep 17 00:00:00 2001 From: Guillermo Alejandro Gallardo Diez Date: Thu, 29 Jan 2026 16:44:19 +0100 Subject: [PATCH 3/3] fix: correct link on md copy --- website/src/components/CopyMarkdownButton/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/CopyMarkdownButton/index.js b/website/src/components/CopyMarkdownButton/index.js index 90f522c2f08..39464a5903a 100644 --- a/website/src/components/CopyMarkdownButton/index.js +++ b/website/src/components/CopyMarkdownButton/index.js @@ -25,7 +25,7 @@ export default function CopyMarkdownButton({docId}) { }, []); const fetchMarkdown = async () => { - const mdUrl = `${window.location.origin}${docId}.md`; + const mdUrl = `${window.location.origin}/${docId}.md`; if (content) return content; try {