From 36fa6fdf76f09687544dbd2a5a2d06741524d6c4 Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Thu, 23 Jul 2026 10:23:39 +0100 Subject: [PATCH 1/2] Fix stacked modals on Mermaid diagrams and squished figure captions Mermaid diagrams are wrapped in an imageblock div, so the generic expand-images lightbox bound to them alongside the dedicated Mermaid zoom modal. One click opened both overlays stacked, and closing the Mermaid modal left the plain lightbox behind. Skip imageblocks that contain a Mermaid diagram when binding the generic lightbox. Mermaid SVGs have no internal bottom padding, so the 0.5rem figure caption margin looked squished against the diagram. Give Mermaid captions 1.25rem. Also remove a dead, duplicate #modal-overlay div from the feedback form partial. Nothing references it, and the duplicate id breaks getElementById-based checks against the real overlay. --- src/css/mermaid.css | 6 ++++++ src/js/07-expand-images.js | 3 +++ src/partials/feedback-forms.hbs | 2 -- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/css/mermaid.css b/src/css/mermaid.css index 42b304e4..8a01222b 100644 --- a/src/css/mermaid.css +++ b/src/css/mermaid.css @@ -4,6 +4,12 @@ text-align: center; } +/* Mermaid SVGs have no internal bottom padding, so the figure caption + needs more separation than a regular image caption. */ +.doc .imageblock .mermaid + .title { + margin-top: 1.25rem; +} + .mermaid > svg { background-color: var(--body-background) !important; max-width: 100%; diff --git a/src/js/07-expand-images.js b/src/js/07-expand-images.js index 98c87948..04b6db4d 100644 --- a/src/js/07-expand-images.js +++ b/src/js/07-expand-images.js @@ -18,6 +18,9 @@ if (!blocks.length || !modalOverlay) return blocks.forEach((block) => { + // Mermaid diagrams have their own zoom modal (20-mermaid-zoom.js). + // Binding both opens two stacked modals for one click. + if (block.querySelector('.mermaid')) return block.addEventListener('click', function (e) { const media = block.querySelector('img, svg') if (!media) return diff --git a/src/partials/feedback-forms.hbs b/src/partials/feedback-forms.hbs index 8d70f270..b58d4a25 100644 --- a/src/partials/feedback-forms.hbs +++ b/src/partials/feedback-forms.hbs @@ -43,6 +43,4 @@ - - From ad9fde538c4e8b975adca49abdc8004e62426e76 Mon Sep 17 00:00:00 2001 From: JakeSCahill Date: Thu, 23 Jul 2026 11:33:03 +0100 Subject: [PATCH 2/2] Add vertical space between Mermaid subgraph labels and nested subgraphs Mermaid v10 and v11 flowcharts never reserve vertical space for a cluster label above nested subgraphs: the label overlaps the inner boxes' top borders, and neither flowchart.subGraphTitleMargin nor flowchart.padding changes the label-to-content distance (top shifts the label down into the content, bottom only grows the box downward). Post-process the rendered SVG instead: for each cluster whose label sits within 12px of a fully-contained child cluster, shift the label up, grow the cluster rect, and expand the viewBox for top-level clusters. --- src/js/08-mermaid-label-spacing.js | 77 ++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/js/08-mermaid-label-spacing.js diff --git a/src/js/08-mermaid-label-spacing.js b/src/js/08-mermaid-label-spacing.js new file mode 100644 index 00000000..c45bedce --- /dev/null +++ b/src/js/08-mermaid-label-spacing.js @@ -0,0 +1,77 @@ +;(function () { + 'use strict' + + // Mermaid (v10 and v11) never reserves vertical space between a subgraph + // label and nested subgraphs: the label overlaps the inner boxes' top + // borders, and neither flowchart.subGraphTitleMargin nor flowchart.padding + // moves the children. Fix the rendered SVG instead: shift the label up and + // grow the cluster rect (and the viewBox for top-level clusters) to match. + + const MIN_GAP = 12 + + if (!document.querySelector('.mermaid')) return + + function fixSvg (svg) { + const clusters = Array.from(svg.querySelectorAll('g.cluster')) + let viewBoxGrow = 0 + clusters.forEach(function (cluster) { + const rect = cluster.querySelector('rect') + const label = cluster.querySelector('.cluster-label') + if (!rect || !label) return + const rx = parseFloat(rect.getAttribute('x')) + const ry = parseFloat(rect.getAttribute('y')) + const rw = parseFloat(rect.getAttribute('width')) + const rh = parseFloat(rect.getAttribute('height')) + const m = (label.getAttribute('transform') || '').match(/translate\(\s*([-\d.]+)[ ,]\s*([-\d.]+)\s*\)/) + if (!m) return + const lx = parseFloat(m[1]) + const ly = parseFloat(m[2]) + let lh + try { + lh = label.getBBox().height + } catch (e) { + return // not rendered (display: none); leave untouched + } + // Find the highest cluster rect fully contained in this one + let minChildTop = Infinity + clusters.forEach(function (other) { + if (other === cluster) return + const or = other.querySelector('rect') + if (!or) return + const ox = parseFloat(or.getAttribute('x')) + const oy = parseFloat(or.getAttribute('y')) + const ow = parseFloat(or.getAttribute('width')) + const oh = parseFloat(or.getAttribute('height')) + const contained = ox >= rx - 1 && ox + ow <= rx + rw + 1 && oy > ry && oy + oh <= ry + rh + 1 + if (contained && oy < minChildTop) minChildTop = oy + }) + if (minChildTop === Infinity) return + const gap = minChildTop - (ly + lh) + if (gap >= MIN_GAP) return + const delta = Math.ceil(MIN_GAP - gap) + label.setAttribute('transform', 'translate(' + lx + ', ' + (ly - delta) + ')') + rect.setAttribute('y', ry - delta) + rect.setAttribute('height', rh + delta) + viewBoxGrow = Math.max(viewBoxGrow, delta) + }) + if (viewBoxGrow > 0) { + const vb = (svg.getAttribute('viewBox') || '').split(/[ ,]+/).map(Number) + if (vb.length === 4 && vb.every(function (n) { return !isNaN(n) })) { + vb[1] -= viewBoxGrow + vb[3] += viewBoxGrow + svg.setAttribute('viewBox', vb.join(' ')) + } + } + svg.setAttribute('data-label-spacing', 'fixed') + } + + // Mermaid renders asynchronously after its module loads; poll briefly. + let attempts = 0 + const timer = setInterval(function () { + attempts++ + const pending = document.querySelectorAll('.mermaid svg:not([data-label-spacing])') + pending.forEach(fixSvg) + const remaining = document.querySelectorAll('.mermaid:not([data-processed])').length + if (remaining === 0 || attempts > 100) clearInterval(timer) + }, 200) +})()