From 552db6ef7d04ee78216848747b2f875c041c8d7d Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Tue, 7 Apr 2026 14:09:19 +0000 Subject: [PATCH] feat(docs): add click-to-zoom lightbox for Mermaid diagrams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swizzled @docusaurus/theme-mermaid's Mermaid component to wrap every diagram with a portal-based lightbox overlay. An expand icon appears on diagram hover. Clicking opens the SVG in a full-screen overlay (90vw × 85vh, scrollable). Closes via backdrop click, Escape key, or close button. SSR-safe, no external dependencies. Matches Tibber electric color theme. Impact: Users can inspect complex flowcharts (e.g. the Options Flow wizard) without squinting at small embedded diagrams. --- docs/user/src/css/custom.css | 110 ++++++++++++++++++++++++ docs/user/src/theme/Mermaid/index.tsx | 118 ++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 docs/user/src/theme/Mermaid/index.tsx diff --git a/docs/user/src/css/custom.css b/docs/user/src/css/custom.css index 4332890..a0234c0 100644 --- a/docs/user/src/css/custom.css +++ b/docs/user/src/css/custom.css @@ -226,3 +226,113 @@ h1, h2, h3, h4, h5, h6 { [data-theme='dark'] .bmc-logo-dark { display: block; } + +/* ── Mermaid diagram zoom / lightbox ──────────────────────────── */ + +/* Wrapper that holds the diagram + the expand button */ +.mermaid-zoom-wrapper { + position: relative; + display: block; +} + +/* Expand button — hidden until the diagram is hovered */ +.mermaid-zoom-btn { + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + padding: 0; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 0.4rem; + color: var(--ifm-color-emphasis-600); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease, + border-color 0.15s ease; + z-index: 1; +} + +.mermaid-zoom-wrapper:hover .mermaid-zoom-btn, +.mermaid-zoom-btn:focus-visible { + opacity: 1; +} + +.mermaid-zoom-btn:hover { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: #fff; +} + +/* Full-screen backdrop */ +.mermaid-lightbox { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); + cursor: zoom-out; + animation: mermaid-fadein 0.15s ease; +} + +@keyframes mermaid-fadein { + from { opacity: 0; } + to { opacity: 1; } +} + +/* The white/dark card containing the diagram */ +.mermaid-lightbox-inner { + position: relative; + max-width: min(90vw, 1200px); + max-height: 85vh; + overflow: auto; + padding: 1.5rem; + background: var(--ifm-background-color); + border-radius: var(--ifm-card-border-radius); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4); + cursor: default; + animation: mermaid-scalein 0.15s ease; +} + +@keyframes mermaid-scalein { + from { transform: scale(0.95); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.mermaid-lightbox-inner svg { + display: block; + width: 100%; + height: auto; +} + +/* Close button (top-right corner of the card) */ +.mermaid-lightbox-close { + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + padding: 0; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 0.4rem; + color: var(--ifm-color-emphasis-700); + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.mermaid-lightbox-close:hover { + background: var(--ifm-color-danger); + border-color: var(--ifm-color-danger); + color: #fff; +} diff --git a/docs/user/src/theme/Mermaid/index.tsx b/docs/user/src/theme/Mermaid/index.tsx new file mode 100644 index 0000000..e36b45f --- /dev/null +++ b/docs/user/src/theme/Mermaid/index.tsx @@ -0,0 +1,118 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import OriginalMermaid from '@theme-original/Mermaid'; +import type MermaidType from '@theme/Mermaid'; +import type { WrapperProps } from '@docusaurus/types'; + +type Props = WrapperProps; + +export default function MermaidWrapper(props: Props): React.JSX.Element { + const containerRef = useRef(null); + const [overlayOpen, setOverlayOpen] = useState(false); + const [svgMarkup, setSvgMarkup] = useState(''); + const [mounted, setMounted] = useState(false); + + // Only run portals client-side (SSR safety) + useEffect(() => { + setMounted(true); + }, []); + + const handleOpen = useCallback(() => { + const svg = containerRef.current?.querySelector('svg'); + if (!svg) return; + // Clone the SVG and strip fixed dimensions so it scales freely + const clone = svg.cloneNode(true) as SVGElement; + clone.removeAttribute('width'); + clone.removeAttribute('height'); + clone.style.cssText = 'width:100%;height:auto;display:block;'; + setSvgMarkup(clone.outerHTML); + setOverlayOpen(true); + }, []); + + const handleClose = useCallback(() => { + setOverlayOpen(false); + }, []); + + // Keyboard + body-scroll lock while overlay is open + useEffect(() => { + if (!overlayOpen) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') handleClose(); + }; + document.addEventListener('keydown', onKey); + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', onKey); + document.body.style.overflow = ''; + }; + }, [overlayOpen, handleClose]); + + return ( +
+ + + + {mounted && + overlayOpen && + createPortal( +
+
e.stopPropagation()} + // Safe: SVG content is cloned from our own rendered DOM node + dangerouslySetInnerHTML={{ __html: svgMarkup }} + /> + +
, + document.body, + )} +
+ ); +}