diff --git a/src/components/AiActionsDropdown/index.tsx b/src/components/AiActionsDropdown/index.tsx new file mode 100644 index 000000000..b8942a088 --- /dev/null +++ b/src/components/AiActionsDropdown/index.tsx @@ -0,0 +1,156 @@ +import React, { useState, useRef, useEffect } from 'react' +import styles from './styles.module.css' + +// Simple inline SVG icons +const ChatGPTIcon = () => ( + + + +) + +const ClaudeIcon = () => ( + + + +) + +const CopyIcon = () => ( + + + + +) + +const MarkdownIcon = () => ( + + + +) + +const ExternalIcon = () => ( + + + +) + +interface Props { + editUrl?: string +} + +export default function AiActionsDropdown({ editUrl }: Props) { + const [open, setOpen] = useState(false) + const [copied, setCopied] = useState(false) + const ref = useRef(null) + + useEffect(() => { + const onClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', onClickOutside) + return () => document.removeEventListener('mousedown', onClickOutside) + }, []) + + const pageUrl = typeof window !== 'undefined' ? window.location.href : '' + + // Convert GitHub editUrl to raw content URL + // e.g. https://github.com/cowprotocol/docs/tree/main/docs/foo.md + // -> https://raw.githubusercontent.com/cowprotocol/docs/main/docs/foo.md + const rawUrl = editUrl + ?.replace('github.com', 'raw.githubusercontent.com') + .replace('/tree/', '/') + + const openInChatGPT = () => { + const q = encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it`) + window.open(`https://chatgpt.com/?q=${q}`, '_blank') + setOpen(false) + } + + const openInClaude = () => { + const q = encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it`) + window.open(`https://claude.ai/new?q=${q}`, '_blank') + setOpen(false) + } + + const copyPage = async () => { + if (rawUrl) { + try { + const res = await fetch(rawUrl) + if (!res.ok) throw new Error() + const md = await res.text() + await navigator.clipboard.writeText(md) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + // Fallback: copy article text if fetch fails + const article = document.querySelector('article') + if (article) { + await navigator.clipboard.writeText(article.innerText) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + } + setOpen(false) + } + + const viewMarkdown = () => { + if (rawUrl) { + window.open(rawUrl, '_blank') + } + setOpen(false) + } + + return ( +
+ + + {open && ( +
+ + + + + + + {editUrl && ( + + )} +
+ )} +
+ ) +} diff --git a/src/components/AiActionsDropdown/styles.module.css b/src/components/AiActionsDropdown/styles.module.css new file mode 100644 index 000000000..3d4d6d489 --- /dev/null +++ b/src/components/AiActionsDropdown/styles.module.css @@ -0,0 +1,108 @@ +.wrapper { + position: relative; + display: inline-block; +} + +.trigger { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border: none; + border-radius: 8px; + background: #052B65; + color: #fff; + font-size: 0.85rem; + cursor: pointer; + transition: opacity 0.15s, box-shadow 0.15s; +} + +:global(html[data-theme='dark']) .trigger { + background: #CAE9FF; + color: #052B65; +} + +.trigger:hover { + opacity: 0.9; + box-shadow: var(--ifm-global-shadow-lw); +} + +.triggerIcon { + width: 18px; + height: 18px; + opacity: 1; +} + +.chevron { + width: 14px; + height: 14px; + opacity: 0.8; + transition: transform 0.15s; +} + +.chevronOpen { + transform: rotate(180deg); +} + +.dropdown { + position: absolute; + top: calc(100% + 4px); + right: 0; + z-index: 100; + min-width: 260px; + padding: 6px; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 10px; + background: var(--ifm-background-color); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +.item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + border: none; + border-radius: 8px; + background: none; + color: var(--ifm-color-emphasis-800); + font-size: 0.85rem; + text-decoration: none; + cursor: pointer; + transition: background 0.12s; +} + +.item:hover { + background: var(--ifm-background-surface-color); + color: var(--ifm-color-emphasis-900); + text-decoration: none; +} + +.itemIcon { + width: 20px; + height: 20px; + flex-shrink: 0; + opacity: 0.75; +} + +.itemText { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.itemLabel { + font-weight: 500; + line-height: 1.3; +} + +.itemDesc { + font-size: 0.75rem; + color: var(--ifm-color-emphasis-700); + line-height: 1.3; +} + +.copied .itemLabel { + color: #16a34a; +} diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx new file mode 100644 index 000000000..d0f3ffed4 --- /dev/null +++ b/src/theme/DocItem/Content/index.tsx @@ -0,0 +1,39 @@ +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 AiActionsDropdown from '@site/src/components/AiActionsDropdown' + +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}: {children: React.ReactNode}) { + const syntheticTitle = useSyntheticTitle() + const {metadata} = useDoc() + + return ( +
+ {syntheticTitle && ( +
+ {syntheticTitle} + +
+ )} + {!syntheticTitle && ( +
+ +
+ )} + {children} +
+ ) +}