From 917b550916faf929926b538602e296ccf1a9e9a0 Mon Sep 17 00:00:00 2001 From: Bryce Date: Thu, 19 Mar 2026 20:05:29 -0700 Subject: [PATCH] Add enlargable/zoomable mermaid lightbox for KQVI diagram - Add mermaid-lightbox.js with click-to-expand modal functionality - Hover shows 'View Full Size' button on mermaid diagrams - Modal displays full diagram with dark overlay backdrop - Escape key or click outside closes modal - CSS fades in expand button on hover for discoverability - Works with mdbook's mermaid preprocessor --- book.toml | 2 +- mermaid-lightbox.js | 196 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 mermaid-lightbox.js diff --git a/book.toml b/book.toml index 1c5468c..c2aa765 100644 --- a/book.toml +++ b/book.toml @@ -7,7 +7,7 @@ description = "A taxonomy of puzzle design patterns from classic point-and-click [output.html] git-repository-url = "https://github.com/noti/puzzle-design-kb" edit-url-template = "https://github.com/noti/puzzle-design-kb/edit/main/{path}" -additional-js = ["mermaid.min.js", "mermaid-init.js"] +additional-js = ["mermaid.min.js", "mermaid-init.js", "mermaid-lightbox.js"] [output.html.playground] editable = true diff --git a/mermaid-lightbox.js b/mermaid-lightbox.js new file mode 100644 index 0000000..2c113d0 --- /dev/null +++ b/mermaid-lightbox.js @@ -0,0 +1,196 @@ +// Mermaid Lightbox - Click to expand diagrams in full-size modal +(() => { + const lightboxHTML = ` + + `; + + const styleHTML = ` + + `; + + // Inject styles + document.head.insertAdjacentHTML('beforeend', styleHTML); + + // Inject lightbox HTML + document.body.insertAdjacentHTML('beforeend', lightboxHTML); + + const lightbox = document.getElementById('mermaid-lightbox'); + const lightboxDiagram = lightbox.querySelector('.mermaid-lightbox-diagram'); + const closeBtn = lightbox.querySelector('.mermaid-lightbox-close'); + + // Wait for mermaid to initialize, then wrap each mermaid diagram + function initLightbox() { + const mermaidPres = document.querySelectorAll('pre.mermaid'); + + mermaidPres.forEach((pre) => { + // Only add container and button if not already wrapped + if (pre.parentElement.classList.contains('mermaid-container')) return; + + // Wrap pre in container + const container = document.createElement('div'); + container.className = 'mermaid-container'; + pre.parentNode.insertBefore(container, pre); + container.appendChild(pre); + + // Add expand button + const btn = document.createElement('button'); + btn.className = 'mermaid-expand-btn'; + btn.textContent = 'View Full Size'; + btn.setAttribute('aria-label', 'Expand diagram to full size'); + container.appendChild(btn); + + // Click handler for expanding + btn.addEventListener('click', (e) => { + e.stopPropagation(); + openLightbox(pre); + }); + + // Also make the pre element clickable + pre.style.cursor = 'pointer'; + pre.addEventListener('click', (e) => { + e.stopPropagation(); + openLightbox(pre); + }); + }); + } + + function openLightbox(pre) { + // Clone the mermaid content for the lightbox + lightboxDiagram.innerHTML = ''; + const clone = pre.cloneNode(true); + clone.className = 'mermaid'; + lightboxDiagram.appendChild(clone); + + lightbox.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + + // Re-render mermaid in lightbox if needed + if (typeof mermaid !== 'undefined') { + mermaid.init({ node: clone.parentElement }, clone); + } + } + + function closeLightbox() { + lightbox.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + lightboxDiagram.innerHTML = ''; + } + + // Event listeners + closeBtn.addEventListener('click', closeLightbox); + + lightbox.addEventListener('click', (e) => { + if (e.target === lightbox) { + closeLightbox(); + } + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && lightbox.classList.contains('active')) { + closeLightbox(); + } + }); + + // Initialize after DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initLightbox); + } else { + // Small delay to ensure mermaid.init() has run + setTimeout(initLightbox, 100); + } +})();