diff --git a/website/package-lock.json b/website/package-lock.json index ac68185..7b711de 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -12,6 +12,7 @@ "@docusaurus/preset-classic": "3.1.1", "@docusaurus/theme-mermaid": "^3.1.1", "@mdx-js/react": "^3.0.0", + "@panzoom/panzoom": "^4.6.2", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", @@ -2958,6 +2959,12 @@ "node": ">= 8" } }, + "node_modules/@panzoom/panzoom": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@panzoom/panzoom/-/panzoom-4.6.2.tgz", + "integrity": "sha512-Zn3B5/hwa6eYIPRSKX0xf2clv8nviTX8AnAU5kU/EugiTDhG41ya2wlBqYrZJYCWQROr/5XkWObZhIkepi89qw==", + "license": "MIT" + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", diff --git a/website/package.json b/website/package.json index 2cdb0b1..b28824d 100644 --- a/website/package.json +++ b/website/package.json @@ -18,6 +18,7 @@ "@docusaurus/preset-classic": "3.1.1", "@docusaurus/theme-mermaid": "^3.1.1", "@mdx-js/react": "^3.0.0", + "@panzoom/panzoom": "^4.6.2", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", diff --git a/website/src/theme/Mermaid/index.js b/website/src/theme/Mermaid/index.js new file mode 100644 index 0000000..6402672 --- /dev/null +++ b/website/src/theme/Mermaid/index.js @@ -0,0 +1,182 @@ +import React, { useEffect, useRef, useState, useCallback } from 'react'; +import ErrorBoundary from '@docusaurus/ErrorBoundary'; +import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common'; +import { + MermaidContainerClassName, + useMermaidRenderResult, +} from '@docusaurus/theme-mermaid/client'; +import panzoom from '@panzoom/panzoom'; + +import styles from './styles.module.css'; + +function MermaidRenderResult({ renderResult }) { + const ref = useRef(null); + const [modalOpen, setModalOpen] = useState(false); + const modalRef = useRef(null); + const zoomRef = useRef(null); + const instanceRef = useRef(null); + + useEffect(() => { + const div = ref.current; + renderResult.bindFunctions?.(div); + }, [renderResult]); + + // Handle click on the mermaid diagram to open modal + const handleClick = useCallback(() => { + setModalOpen(true); + }, []); + + // Initialize panzoom when modal opens + useEffect(() => { + if (!modalOpen || !modalRef.current) return; + + const modalEl = modalRef.current; + const svgEl = modalEl.querySelector('svg'); + if (!svgEl) return; + + // Clean up any previous instance + if (instanceRef.current) { + instanceRef.current.destroy(); + } + + const instance = panzoom(svgEl, { + maxScale: 10, + minScale: 0.3, + step: 0.3, + contain: 'outside', + pinchAndPan: true, + }); + instanceRef.current = instance; + + // Reset zoom on open + // Use a small delay to let the modal render finish + const resetTimer = setTimeout(() => { + instance.reset({ animate: false }); + }, 50); + + // Keyboard handlers + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + setModalOpen(false); + } + // Zoom with +/- + if (e.key === '+' || e.key === '=') { + e.preventDefault(); + instance.zoomIn(); + } + if (e.key === '-') { + e.preventDefault(); + instance.zoomOut(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => { + clearTimeout(resetTimer); + window.removeEventListener('keydown', handleKeyDown); + if (instanceRef.current) { + instanceRef.current.destroy(); + instanceRef.current = null; + } + }; + }, [modalOpen]); + + return ( + <> +