diff --git a/CHANGELOG.md b/CHANGELOG.md index f76554e..a0011af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0] - Unreleased + +### Changed +- Exposed `MarkdownActionsDropdown` as a first-class Docusaurus theme component at `theme/MarkdownActionsDropdown`, so consumers can swizzle it directly with `npm run swizzle docusaurus-markdown-source-plugin MarkdownActionsDropdown -- --eject`. +- Updated `Root` to render `@theme/MarkdownActionsDropdown`, allowing swizzled dropdown overrides to be picked up automatically while keeping the existing injection behavior internal. + +### Fixed +- Made `theme/MarkdownActionsDropdown` self-contained so swizzled copies no longer fail to build by trying to resolve plugin-local imports like `../../lib/markdown-path` inside the consumer site's `src/theme/` directory. + ## [2.2.5] - 2026-05-03 ### Fixed diff --git a/README.md b/README.md index 15b370b..852db37 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ article .markdown header h1 { /* Add hover effect for dropdown items */ .dropdown__link:hover { - background-color: var(--ifm-hover-overlay); + background-color: rgba(0, 0, 0, 0.05); } /* Responsive adjustments for mobile */ @@ -296,7 +296,9 @@ location ~* ^/docs/.*/img/.* { | `X-Robots-Tag: noindex, nofollow` | Prevents search engines from indexing (avoids duplicate content SEO issues) while allowing AI assistants to access | | `Cache-Control` | Balances performance (caching) with content freshness | -## CSS Customization +## Customization + +### CSS Customization You can customize the dropdown appearance by overriding these CSS classes in your `custom.css`: @@ -317,6 +319,18 @@ You can customize the dropdown appearance by overriding these CSS classes in you } ``` +### Component Customization with Swizzling + +For deeper changes such as button labels, icons, menu items, analytics, or custom copy/open behavior, swizzle the dropdown component itself: + +```bash +npm run swizzle docusaurus-markdown-source-plugin MarkdownActionsDropdown -- --eject +``` + +This creates a local theme override at `src/theme/MarkdownActionsDropdown/index.js` in your Docusaurus site. The plugin's internal `Root` component will keep handling page injection, but it renders `@theme/MarkdownActionsDropdown`, so your swizzled component is used automatically. + +If you only need to wrap the default dropdown, import the original component from `@theme-original/MarkdownActionsDropdown` inside the swizzled file and compose around it. + ## Troubleshooting ### Dropdown Not Appearing diff --git a/components/MarkdownActionsDropdown/index.js b/components/MarkdownActionsDropdown/index.js index c0622ad..8f9538b 100644 --- a/components/MarkdownActionsDropdown/index.js +++ b/components/MarkdownActionsDropdown/index.js @@ -1,141 +1 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { getMarkdownUrl } from '../../lib/markdown-path'; - -export default function MarkdownActionsDropdown() { - const [copied, setCopied] = useState(false); - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); - const copyResetTimerRef = useRef(null); - - // Clear any pending copy-reset timer on unmount so it cannot fire setState - // on a torn-down component or flip UI state after the user has navigated. - useEffect(() => () => { - if (copyResetTimerRef.current) { - clearTimeout(copyResetTimerRef.current); - copyResetTimerRef.current = null; - } - }, []); - - // Get pathname from window.location for URL construction - const currentPath = typeof window !== 'undefined' ? window.location.pathname : ''; - - // Handle click outside to close dropdown - useEffect(() => { - // Only add listener if dropdown is open - if (!isOpen) return; - - const handleClickOutside = (event) => { - // If the click is outside the dropdown, close it - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { - setIsOpen(false); - } - }; - - // Add event listener to document - // Use mousedown instead of click for better UX (fires before click) - document.addEventListener('mousedown', handleClickOutside); - - // Cleanup function to remove event listener - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - const markdownUrl = getMarkdownUrl(currentPath); - - // Handle opening markdown in new tab - const handleOpenMarkdown = () => { - window.open(markdownUrl, '_blank'); - setIsOpen(false); - }; - - // Handle copying markdown to clipboard - const handleCopyMarkdown = async () => { - // Cancel any in-flight reset timer up front so a stale timer can't flip - // state during a slow fetch or after a rapid second click. - if (copyResetTimerRef.current) { - clearTimeout(copyResetTimerRef.current); - copyResetTimerRef.current = null; - } - - try { - const response = await fetch(markdownUrl); - if (!response.ok) { - throw new Error('Failed to fetch markdown'); - } - const markdown = await response.text(); - await navigator.clipboard.writeText(markdown); - - setCopied(true); - copyResetTimerRef.current = setTimeout(() => { - setCopied(false); - copyResetTimerRef.current = null; - }, 2000); - } catch (error) { - console.error('Failed to copy markdown:', error); - alert('Failed to copy markdown. Please try again.'); - } - }; - - return ( -
- - - -
- ); -} +export { default } from '../../theme/MarkdownActionsDropdown'; diff --git a/package.json b/package.json index e3ef420..83a7858 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docusaurus-markdown-source-plugin", - "version": "2.2.5", + "version": "2.3.0", "description": "A lightweight Docusaurus plugin that exposes your markdown files as raw .md URLs, perfect for LLMs and documentation tools", "main": "index.js", "scripts": { diff --git a/theme/MarkdownActionsDropdown/index.js b/theme/MarkdownActionsDropdown/index.js new file mode 100644 index 0000000..f19b8b1 --- /dev/null +++ b/theme/MarkdownActionsDropdown/index.js @@ -0,0 +1,147 @@ +import React, { useState, useRef, useEffect } from 'react'; + +function getMarkdownUrl(routePath) { + return routePath.endsWith('/') + ? routePath + 'index.md' + : routePath + '.md'; +} + +export default function MarkdownActionsDropdown() { + const [copied, setCopied] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const copyResetTimerRef = useRef(null); + + // Clear any pending copy-reset timer on unmount so it cannot fire setState + // on a torn-down component or flip UI state after the user has navigated. + useEffect(() => () => { + if (copyResetTimerRef.current) { + clearTimeout(copyResetTimerRef.current); + copyResetTimerRef.current = null; + } + }, []); + + // Get pathname from window.location for URL construction + const currentPath = typeof window !== 'undefined' ? window.location.pathname : ''; + + // Handle click outside to close dropdown + useEffect(() => { + // Only add listener if dropdown is open + if (!isOpen) return; + + const handleClickOutside = (event) => { + // If the click is outside the dropdown, close it + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + // Add event listener to document + // Use mousedown instead of click for better UX (fires before click) + document.addEventListener('mousedown', handleClickOutside); + + // Cleanup function to remove event listener + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + const markdownUrl = getMarkdownUrl(currentPath); + + // Handle opening markdown in new tab + const handleOpenMarkdown = () => { + window.open(markdownUrl, '_blank'); + setIsOpen(false); + }; + + // Handle copying markdown to clipboard + const handleCopyMarkdown = async () => { + // Cancel any in-flight reset timer up front so a stale timer can't flip + // state during a slow fetch or after a rapid second click. + if (copyResetTimerRef.current) { + clearTimeout(copyResetTimerRef.current); + copyResetTimerRef.current = null; + } + + try { + const response = await fetch(markdownUrl); + if (!response.ok) { + throw new Error('Failed to fetch markdown'); + } + const markdown = await response.text(); + await navigator.clipboard.writeText(markdown); + + setCopied(true); + copyResetTimerRef.current = setTimeout(() => { + setCopied(false); + copyResetTimerRef.current = null; + }, 2000); + } catch (error) { + console.error('Failed to copy markdown:', error); + alert('Failed to copy markdown. Please try again.'); + } + }; + + return ( +
+ + + +
+ ); +} + diff --git a/theme/Root.js b/theme/Root.js index 0520489..4e80b89 100644 --- a/theme/Root.js +++ b/theme/Root.js @@ -3,7 +3,7 @@ import React, { useEffect, useRef } from 'react'; import { useLocation } from '@docusaurus/router'; import { createRoot } from 'react-dom/client'; import { usePluginData } from '@docusaurus/useGlobalData'; -import MarkdownActionsDropdown from '../components/MarkdownActionsDropdown'; +import MarkdownActionsDropdown from '@theme/MarkdownActionsDropdown'; import { decodeHashSafely } from '../lib/decode-hash'; export default function Root({ children }) {