Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 0 additions & 54 deletions website/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -636,60 +636,6 @@
color: var(--ifm-color-primary);
}

/* ════════════════════════════════════════════════════════════════
MERMAID DIAGRAMS — horizontal scroll + zoom on hover
════════════════════════════════════════════════════════════════ */

/* Wrapper around each mermaid SVG: enable horizontal scroll */
.docusaurus-mermaid-container {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
margin: 1.5rem 0;
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: var(--gu-radius);
background: #fff;
padding: 0.5rem;
}

[data-theme='dark'] .docusaurus-mermaid-container {
background: #1a1a2e;
border-color: rgba(93, 173, 226, 0.15);
}

/* Allow the SVG to keep its natural width (don't shrink to container) */
.docusaurus-mermaid-container svg {
width: auto !important;
max-width: none !important;
height: auto !important;
min-width: 600px; /* Ensures diagrams don't get squashed too small */
display: block;
}

/* Zoom-on-hover: scale up slightly for readability */
.docusaurus-mermaid-container:hover {
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
}
[data-theme='dark'] .docusaurus-mermaid-container:hover {
box-shadow: 0 2px 12px rgba(0,0,0,0.35);
}

/* Make the scrollbar visible and styled */
.docusaurus-mermaid-container::-webkit-scrollbar {
height: 8px;
}
.docusaurus-mermaid-container::-webkit-scrollbar-track {
background: var(--ifm-color-emphasis-100);
border-radius: 4px;
}
.docusaurus-mermaid-container::-webkit-scrollbar-thumb {
background: var(--ifm-color-emphasis-400);
border-radius: 4px;
}
.docusaurus-mermaid-container::-webkit-scrollbar-thumb:hover {
background: var(--ifm-color-emphasis-500);
}

/* ════════════════════════════════════════════════════════════════
PRINT-FRIENDLY
════════════════════════════════════════════════════════════════ */
Expand Down
99 changes: 74 additions & 25 deletions website/src/theme/Mermaid/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,57 +9,84 @@ import panzoom from '@panzoom/panzoom';

import styles from './styles.module.css';

const PADDING = 80; // pixels of padding inside modal

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
// Initialize panzoom and auto-fit SVG 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
// Clean up previous
if (instanceRef.current) {
instanceRef.current.destroy();
}

const instance = panzoom(svgEl, {
maxScale: 10,
minScale: 0.3,
step: 0.3,
minScale: 0.1,
step: 0.15,
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);
// Auto-fit: scale the SVG to fill the modal while keeping aspect ratio
const fitToScreen = () => {
const parent = modalRef.current;
if (!parent) return;
const svg = parent.querySelector('svg');
if (!svg) return;

const containerW = parent.clientWidth;
const containerH = parent.clientHeight;
const svgW = svg.viewBox?.baseVal?.width || svg.width?.baseVal?.value || svg.getBoundingClientRect().width;
const svgH = svg.viewBox?.baseVal?.height || svg.height?.baseVal?.value || svg.getBoundingClientRect().height;

if (!svgW || !svgH) return;

const availableW = containerW - PADDING;
const availableH = containerH - PADDING;
const scale = Math.min(availableW / svgW, availableH / svgH, 3); // cap at 3x

if (scale > 0) {
instance.zoom(scale, { animate: false });
// Center the SVG
const scaledW = svgW * scale;
const scaledH = svgH * scale;
const offsetX = (containerW - scaledW) / 2;
const offsetY = (containerH - scaledH) / 2;
instance.pan(offsetX, offsetY, { animate: false });
}
};

// Wait for layout, then fit
const fitTimer = setTimeout(fitToScreen, 80);

// Re-fit on resize
window.addEventListener('resize', fitToScreen);

// Keyboard handlers
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
setModalOpen(false);
}
// Zoom with +/-
if (e.key === '+' || e.key === '=') {
e.preventDefault();
instance.zoomIn();
Expand All @@ -68,12 +95,17 @@ function MermaidRenderResult({ renderResult }) {
e.preventDefault();
instance.zoomOut();
}
if (e.key === '0') {
e.preventDefault();
fitToScreen(); // reset to fit
}
};

window.addEventListener('keydown', handleKeyDown);

return () => {
clearTimeout(resetTimer);
clearTimeout(fitTimer);
window.removeEventListener('resize', fitToScreen);
window.removeEventListener('keydown', handleKeyDown);
if (instanceRef.current) {
instanceRef.current.destroy();
Expand Down Expand Up @@ -126,11 +158,32 @@ function MermaidRenderResult({ renderResult }) {
className={styles.toolBtn}
onClick={(e) => {
e.stopPropagation();
instanceRef.current?.reset({ animate: true });
// Re-fit to screen
const parent = modalRef.current;
const svg = parent?.querySelector('svg');
if (!svg || !instanceRef.current) return;
const containerW = parent.clientWidth;
const containerH = parent.clientHeight;
const svgW = svg.viewBox?.baseVal?.width || svg.width?.baseVal?.value || svg.getBoundingClientRect().width;
const svgH = svg.viewBox?.baseVal?.height || svg.height?.baseVal?.value || svg.getBoundingClientRect().height;
if (!svgW || !svgH) return;
const availableW = containerW - PADDING;
const availableH = containerH - PADDING;
const scale = Math.min(availableW / svgW, availableH / svgH, 3);
if (scale > 0) {
instanceRef.current.zoom(scale, { animate: true });
const scaledW = svgW * scale;
const scaledH = svgH * scale;
instanceRef.current.pan(
(containerW - scaledW) / 2,
(containerH - scaledH) / 2,
{ animate: true }
);
}
}}
title="Reset"
title="Fit to screen (0)"
>
</button>
<button
className={styles.toolBtn}
Expand All @@ -143,20 +196,16 @@ function MermaidRenderResult({ renderResult }) {
</button>
<span className={styles.toolHint}>
Scroll to zoom · Drag to pan
Scroll zoom · Drag pan · 0 fit
</span>
</div>
<div
ref={modalRef}
className={styles.modalBody}
onClick={(e) => e.stopPropagation()}
role="presentation"
>
<div
ref={modalRef}
className={styles.zoomContainer}
dangerouslySetInnerHTML={{ __html: renderResult.svg }}
/>
</div>
dangerouslySetInnerHTML={{ __html: renderResult.svg }}
/>
</div>
)}
</>
Expand Down
86 changes: 45 additions & 41 deletions website/src/theme/Mermaid/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,95 +22,99 @@
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
background: rgba(0, 0, 0, 0.78);
backdrop-filter: blur(6px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
animation: overlayIn 0.2s ease;
/* Prevent body scroll when modal is open */
overflow: hidden;
touch-action: none;
}

@keyframes overlayIn {
from { opacity: 0; }
to { opacity: 1; }
}

/* ── Toolbar at top of overlay ─────────────────────────── */
/* ── Toolbar at top-right ──────────────────────────────── */
.toolbar {
position: absolute;
top: 16px;
right: 16px;
position: fixed;
top: 12px;
right: 12px;
display: flex;
align-items: center;
gap: 6px;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(6px);
border-radius: 8px;
gap: 4px;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(8px);
border-radius: 10px;
padding: 6px 10px;
z-index: 10000;
z-index: 10001;
user-select: none;
}

.toolBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
width: 34px;
height: 34px;
border: none;
border-radius: 6px;
border-radius: 8px;
background: transparent;
color: #eee;
font-size: 16px;
color: #ddd;
font-size: 18px;
cursor: pointer;
transition: background 0.15s;
}

.toolBtn:hover {
background: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.18);
}

.toolBtn:active {
background: rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.3);
}

.toolHint {
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
margin-left: 6px;
padding-left: 8px;
color: rgba(255, 255, 255, 0.45);
font-size: 11px;
margin-left: 8px;
padding-left: 10px;
border-left: 1px solid rgba(255, 255, 255, 0.15);
white-space: nowrap;
}

/* ── Scrollable body area (for large diagrams) ─────────── */
/* ── Modal body = the fullscreen SVG container ──────────── */
.modalBody {
width: 90vw;
height: 85vh;
overflow: auto;
border-radius: 12px;
background: #fff;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.4);
position: absolute;
/* Leave room for toolbar */
inset: 0;
overflow: hidden;
cursor: grab;
touch-action: none;
}

.modalBody:active {
cursor: grabbing;
}

[data-theme='dark'] .modalBody {
background: #1a1a2e;
/* The SVG gets transform-origin 0 0 by panzoom internally */
.modalBody svg {
position: absolute;
top: 0;
left: 0;
display: block;
transform-origin: 0 0;
/* Panzoom handles the transform */
}

/* ── Panzoom container inside modal body ───────────────── */
.zoomContainer {
display: inline-block;
padding: 32px;
transform-origin: 0 0;
touch-action: none;
/* Light background for the SVG canvas itself */
.modalBody {
background: #f8f9fa;
}

.zoomContainer svg {
display: block;
max-width: none;
[data-theme='dark'] .modalBody {
background: #121220;
}
Loading