diff --git a/docs/user/src/css/custom.css b/docs/user/src/css/custom.css index 908ea87..18f72d9 100644 --- a/docs/user/src/css/custom.css +++ b/docs/user/src/css/custom.css @@ -229,43 +229,81 @@ h1, h2, h3, h4, h5, h6 { /* ── Mermaid diagram zoom / lightbox ──────────────────────────── */ -/* Wrapper that holds the diagram + the expand button */ +/* Wrapper — entire diagram is the click target */ .mermaid-zoom-wrapper { position: relative; display: block; + cursor: zoom-in; + border-radius: var(--ifm-card-border-radius, 0.5rem); + /* Clip the hover overlay to the same rounded corners */ + overflow: hidden; } -/* Expand button — hidden until the diagram is hovered */ -.mermaid-zoom-btn { +/* Semi-transparent tint that fades in on hover */ +.mermaid-zoom-hint { position: absolute; - top: 0.5rem; - right: 0.5rem; + inset: 0; 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; + pointer-events: none; opacity: 0; - transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease, - border-color 0.15s ease; + background: rgba(0, 0, 0, 0); + transition: opacity 0.2s ease, background 0.2s ease; z-index: 1; } -.mermaid-zoom-wrapper:hover .mermaid-zoom-btn, -.mermaid-zoom-btn:focus-visible { +.mermaid-zoom-wrapper:hover .mermaid-zoom-hint, +.mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint { opacity: 1; + background: rgba(0, 0, 0, 0.06); } -.mermaid-zoom-btn:hover { - background: var(--ifm-color-primary); - border-color: var(--ifm-color-primary); - color: #fff; +[data-theme='dark'] .mermaid-zoom-wrapper:hover .mermaid-zoom-hint, +[data-theme='dark'] .mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint { + background: rgba(255, 255, 255, 0.06); +} + +/* Centered pill badge inside the tint */ +.mermaid-zoom-hint-badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.8rem; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 2rem; + color: var(--ifm-color-emphasis-800); + font-size: 0.78rem; + font-weight: 600; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18); + transform: translateY(4px); + transition: transform 0.2s ease; +} + +.mermaid-zoom-wrapper:hover .mermaid-zoom-hint-badge, +.mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint-badge { + transform: translateY(0); +} + +/* Focus ring for keyboard users */ +.mermaid-zoom-wrapper:focus-visible { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 2px; +} + +/* Touch devices: no hover state — show badge permanently so users know it's tappable */ +@media (hover: none) { + .mermaid-zoom-hint { + opacity: 1; + background: transparent; + } + + .mermaid-zoom-hint-badge { + transform: translateY(0); + /* Slightly more opaque so it reads well without hover context */ + opacity: 0.85; + } } /* Full-screen backdrop */ diff --git a/docs/user/src/theme/Mermaid/index.tsx b/docs/user/src/theme/Mermaid/index.tsx index e36b45f..4ea1037 100644 --- a/docs/user/src/theme/Mermaid/index.tsx +++ b/docs/user/src/theme/Mermaid/index.tsx @@ -8,6 +8,7 @@ type Props = WrapperProps; export default function MermaidWrapper(props: Props): React.JSX.Element { const containerRef = useRef(null); + const lightboxRef = useRef(null); const [overlayOpen, setOverlayOpen] = useState(false); const [svgMarkup, setSvgMarkup] = useState(''); const [mounted, setMounted] = useState(false); @@ -33,14 +34,43 @@ export default function MermaidWrapper(props: Props): React.JSX.Element { setOverlayOpen(false); }, []); - // Keyboard + body-scroll lock while overlay is open + // Keyboard + body-scroll lock + focus trap while overlay is open useEffect(() => { if (!overlayOpen) return; + const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') handleClose(); + if (e.key === 'Escape') { + handleClose(); + return; + } + // Focus trap: keep Tab/Shift+Tab inside the lightbox + if (e.key === 'Tab' && lightboxRef.current) { + const focusable = lightboxRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault(); + last?.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first?.focus(); + } + } + } }; + document.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; + + // Move focus into the lightbox on open + const closeBtn = lightboxRef.current?.querySelector('button'); + closeBtn?.focus(); + return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; @@ -48,37 +78,49 @@ export default function MermaidWrapper(props: Props): React.JSX.Element { }, [overlayOpen, handleClose]); return ( -
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleOpen(); + } + }} + aria-label="View diagram enlarged" + > - + {/* Hover hint — pointer-events:none so it never swallows clicks */} + {mounted && overlayOpen && createPortal(