diff --git a/frontend/js/mermaid-viewer.js b/frontend/js/mermaid-viewer.js
index 942079d..a705257 100644
--- a/frontend/js/mermaid-viewer.js
+++ b/frontend/js/mermaid-viewer.js
@@ -83,8 +83,8 @@ export async function renderMermaidBlocks(container) {
for (var i = 0; i < blocks.length; i++) {
var codeBlock = blocks[i];
- var code = codeBlock.textContent || codeBlock.innerText || '';
- if (!code.trim()) continue;
+ var originalCode = codeBlock.textContent || codeBlock.innerText || '';
+ if (!originalCode.trim()) continue;
var wrapper = codeBlock.closest('pre') || codeBlock.parentElement;
if (!wrapper) continue;
@@ -94,30 +94,53 @@ export async function renderMermaidBlocks(container) {
try {
// Preprocess Obsidian syntax before Mermaid parsing (#24)
- code = preprocessObsidian(code);
+ var code = preprocessObsidian(originalCode);
await mermaid.parse(code);
var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now();
var result = await mermaid.render(id, code);
- var container_1 = document.createElement('div');
- container_1.className = 'mermaid-container';
- container_1.innerHTML = '
' + result.svg + '
';
+ // Extract diagram type from first non-comment, non-empty line
+ var diagramType = extractDiagramType(code);
- // Add zoom/pan controls (#27)
- var controls = document.createElement('div');
- controls.className = 'mermaid-controls';
- controls.innerHTML =
- '' +
- '' +
- '' +
- '';
- container_1.appendChild(controls);
+ // Build container with header + body
+ var containerDiv = document.createElement('div');
+ containerDiv.className = 'mermaid-container';
- wrapper.parentNode.replaceChild(container_1, wrapper);
- setupZoomPan(container_1);
+ // Header bar
+ var header = document.createElement('div');
+ header.className = 'mermaid-header';
+ header.innerHTML =
+ '' + escapeHtml(diagramType) + '' +
+ '';
+
+ // Body
+ var body = document.createElement('div');
+ body.className = 'mermaid-body';
+ body.innerHTML =
+ '' + result.svg + '
' +
+ '' + escapeHtml(originalCode) + '
';
+
+ containerDiv.appendChild(header);
+ containerDiv.appendChild(body);
+
+ wrapper.parentNode.replaceChild(containerDiv, wrapper);
+
+ // Wire up header buttons
+ wireHeaderButtons(containerDiv, originalCode);
+ setupZoomPan(containerDiv);
+ setupCodePreviewToggle(containerDiv);
safeCreateIcons();
} catch (err) {
- // Render error block
var errorDiv = document.createElement('div');
errorDiv.className = 'mermaid-error';
errorDiv.innerHTML = 'Erreur Mermaid
' + escapeHtml(err.message || String(err));
@@ -126,6 +149,86 @@ export async function renderMermaidBlocks(container) {
}
}
+// ---------------------------------------------------------------------------
+// Header button wiring
+// ---------------------------------------------------------------------------
+
+function wireHeaderButtons(container, originalCode) {
+ // Copy SVG
+ var copyBtn = container.querySelector('.mermaid-copy');
+ if (copyBtn) {
+ copyBtn.addEventListener('click', function() {
+ copySvgToClipboard(container);
+ });
+ }
+
+ // Download PNG
+ var downloadBtn = container.querySelector('.mermaid-download');
+ if (downloadBtn) {
+ downloadBtn.addEventListener('click', function() {
+ exportMermaidPNG(container);
+ });
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Code / Preview toggle
+// ---------------------------------------------------------------------------
+
+function setupCodePreviewToggle(container) {
+ var toggleBtns = container.querySelectorAll('.mermaid-toggle-btn');
+ var rendered = container.querySelector('.mermaid-rendered');
+ var source = container.querySelector('.mermaid-source');
+ var zoomBtns = container.querySelectorAll('.mermaid-zoom-in, .mermaid-zoom-out');
+ var fullscreenBtn = container.querySelector('.mermaid-fullscreen');
+
+ for (var i = 0; i < toggleBtns.length; i++) {
+ toggleBtns[i].addEventListener('click', function() {
+ var mode = this.getAttribute('data-mode');
+
+ // Update active state
+ for (var j = 0; j < toggleBtns.length; j++) {
+ toggleBtns[j].classList.remove('active');
+ }
+ this.classList.add('active');
+
+ if (mode === 'preview') {
+ rendered.style.display = '';
+ source.style.display = 'none';
+ // Show zoom/fullscreen buttons in preview mode
+ for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = '';
+ if (fullscreenBtn) fullscreenBtn.style.display = '';
+ } else {
+ rendered.style.display = 'none';
+ source.style.display = '';
+ // Hide zoom/fullscreen in code mode
+ for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = 'none';
+ if (fullscreenBtn) fullscreenBtn.style.display = 'none';
+ }
+ });
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Diagram type extraction
+// ---------------------------------------------------------------------------
+
+function extractDiagramType(code) {
+ var lines = code.trim().split('\n');
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i].trim();
+ // Skip comments and empty lines
+ if (!line || line.indexOf('%%') === 0) continue;
+ // Return the first meaningful word(s)
+ var words = line.split(/\s+/);
+ if (words.length >= 2) {
+ return words[0] + ' ' + words[1];
+ }
+ return words[0] || 'mermaid';
+ }
+ return 'mermaid';
+}
+
// ---------------------------------------------------------------------------
// Zoom & Pan (#27)
// ---------------------------------------------------------------------------
@@ -174,31 +277,44 @@ function setupZoomPan(container) {
update();
}, { passive: false });
- container.querySelector('.mermaid-zoom-in').addEventListener('click', function() {
- scale = Math.min(5, scale * 1.3); update();
- });
- container.querySelector('.mermaid-zoom-out').addEventListener('click', function() {
- scale = Math.max(0.2, scale * 0.7); update();
- });
- container.querySelector('.mermaid-reset').addEventListener('click', function() {
- scale = 1; panX = 0; panY = 0; update();
- });
+ var zoomIn = container.querySelector('.mermaid-zoom-in');
+ var zoomOut = container.querySelector('.mermaid-zoom-out');
+ if (zoomIn) zoomIn.addEventListener('click', function() { scale = Math.min(5, scale * 1.3); update(); });
+ if (zoomOut) zoomOut.addEventListener('click', function() { scale = Math.max(0.2, scale * 0.7); update(); });
// Fullscreen (#28)
- container.querySelector('.mermaid-fullscreen').addEventListener('click', function() {
- openFullscreen(svgWrapper);
- });
+ var fsBtn = container.querySelector('.mermaid-fullscreen');
+ if (fsBtn) {
+ fsBtn.addEventListener('click', function() {
+ openFullscreen(container);
+ });
+ }
}
// ---------------------------------------------------------------------------
-// Fullscreen mode (#28)
+// Fullscreen mode (#28) — with header bar + full-size diagram
// ---------------------------------------------------------------------------
-function openFullscreen(svgWrapper) {
+function openFullscreen(container) {
+ var svgWrapper = container.querySelector('.mermaid-rendered');
+ var diagramType = container.querySelector('.mermaid-diagram-type');
+ var typeLabel = diagramType ? diagramType.textContent : 'mermaid';
+
var overlay = document.createElement('div');
overlay.className = 'mermaid-fullscreen-overlay';
+
+ // Fullscreen header bar
overlay.innerHTML =
- '' +
+ '' +
'';
var content = overlay.querySelector('.mermaid-fullscreen-content');
@@ -209,6 +325,64 @@ function openFullscreen(svgWrapper) {
}
document.body.appendChild(overlay);
+
+ // Fullscreen zoom
+ var fsScale = 1, fsPanX = 0, fsPanY = 0, fsDragging = false, fsStartX, fsStartY;
+ if (svg) {
+ svg.style.transformOrigin = '0 0';
+ function fsUpdate() {
+ svg.style.transform = 'translate(' + fsPanX + 'px, ' + fsPanY + 'px) scale(' + fsScale + ')';
+ }
+
+ svg.addEventListener('mousedown', function(e) {
+ fsDragging = true;
+ fsStartX = e.clientX - fsPanX;
+ fsStartY = e.clientY - fsPanY;
+ svg.style.cursor = 'grabbing';
+ e.preventDefault();
+ });
+
+ document.addEventListener('mousemove', function(e) {
+ if (!fsDragging) return;
+ fsPanX = e.clientX - fsStartX;
+ fsPanY = e.clientY - fsStartY;
+ fsUpdate();
+ });
+
+ document.addEventListener('mouseup', function() {
+ fsDragging = false;
+ if (svg) svg.style.cursor = 'grab';
+ });
+
+ svg.addEventListener('wheel', function(e) {
+ e.preventDefault();
+ var delta = e.deltaY > 0 ? 0.9 : 1.1;
+ fsScale = Math.max(0.2, Math.min(5, fsScale * delta));
+ fsUpdate();
+ }, { passive: false });
+
+ var fsZoomIn = overlay.querySelector('.mermaid-fs-zoom-in');
+ var fsZoomOut = overlay.querySelector('.mermaid-fs-zoom-out');
+ if (fsZoomIn) fsZoomIn.addEventListener('click', function() { fsScale = Math.min(5, fsScale * 1.3); fsUpdate(); });
+ if (fsZoomOut) fsZoomOut.addEventListener('click', function() { fsScale = Math.max(0.2, fsScale * 0.7); fsUpdate(); });
+ }
+
+ // Copy SVG in fullscreen
+ var fsCopy = overlay.querySelector('.mermaid-fs-copy');
+ if (fsCopy) {
+ fsCopy.addEventListener('click', function() {
+ copySvgFromElement(content);
+ });
+ }
+
+ // Download PNG in fullscreen
+ var fsDownload = overlay.querySelector('.mermaid-fs-download');
+ if (fsDownload) {
+ fsDownload.addEventListener('click', function() {
+ exportMermaidPNGFromElement(svg);
+ });
+ }
+
safeCreateIcons();
function close() {
@@ -224,7 +398,89 @@ function openFullscreen(svgWrapper) {
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close();
});
- overlay.querySelector('.mermaid-fullscreen-close').addEventListener('click', close);
+ overlay.querySelector('.mermaid-fs-close').addEventListener('click', close);
+}
+
+// ---------------------------------------------------------------------------
+// Copy SVG to clipboard
+// ---------------------------------------------------------------------------
+
+function copySvgToClipboard(container) {
+ copySvgFromElement(container.querySelector('.mermaid-rendered'));
+}
+
+function copySvgFromElement(el) {
+ if (!el) return;
+ var svg = el.querySelector('svg');
+ if (!svg) return;
+ var data = new XMLSerializer().serializeToString(svg);
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(data).then(function() {
+ /* copied */
+ }).catch(function() {
+ fallbackCopy(data);
+ });
+ } else {
+ fallbackCopy(data);
+ }
+}
+
+function fallbackCopy(text) {
+ var textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.appendChild(textarea);
+ textarea.select();
+ try { document.execCommand('copy'); } catch (e) {}
+ document.body.removeChild(textarea);
+}
+
+// ---------------------------------------------------------------------------
+// Export SVG/PNG (#24)
+// ---------------------------------------------------------------------------
+
+export function exportMermaidSVG(container) {
+ var svg = container.querySelector('svg');
+ if (!svg) return;
+ var data = new XMLSerializer().serializeToString(svg);
+ var blob = new Blob([data], { type: 'image/svg+xml' });
+ downloadBlob(blob, 'diagram.svg');
+}
+
+export function exportMermaidPNG(container) {
+ var svg = container.querySelector('svg');
+ exportMermaidPNGFromElement(svg);
+}
+
+function exportMermaidPNGFromElement(svg) {
+ if (!svg) return;
+ var data = new XMLSerializer().serializeToString(svg);
+ var canvas = document.createElement('canvas');
+ var ctx = canvas.getContext('2d');
+ var img = new Image();
+ var blob = new Blob([data], { type: 'image/svg+xml' });
+ var url = URL.createObjectURL(blob);
+
+ img.onload = function() {
+ canvas.width = img.width * 2;
+ canvas.height = img.height * 2;
+ ctx.scale(2, 2);
+ ctx.drawImage(img, 0, 0);
+ URL.revokeObjectURL(url);
+ canvas.toBlob(function(pngBlob) {
+ downloadBlob(pngBlob, 'diagram.png');
+ }, 'image/png');
+ };
+ img.src = url;
+}
+
+function downloadBlob(blob, filename) {
+ var a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(a.href);
}
// ---------------------------------------------------------------------------
@@ -237,8 +493,8 @@ export function setupFocusMode() {
document.addEventListener('click', function(e) {
var container = e.target.closest('.mermaid-container');
if (!container) return;
- // Don't trigger focus if clicking controls
- if (e.target.closest('.mermaid-controls') || e.target.closest('button')) return;
+ // Don't trigger focus if clicking header or controls
+ if (e.target.closest('.mermaid-header') || e.target.closest('button')) return;
var svgWrapper = container.querySelector('.mermaid-rendered');
if (!svgWrapper) return;
@@ -285,49 +541,6 @@ function updateFocusPanel(svgWrapper) {
}
}
-// ---------------------------------------------------------------------------
-// Export SVG/PNG (#24)
-// ---------------------------------------------------------------------------
-
-export function exportMermaidSVG(container) {
- var svg = container.querySelector('svg');
- if (!svg) return;
- var data = new XMLSerializer().serializeToString(svg);
- var blob = new Blob([data], { type: 'image/svg+xml' });
- downloadBlob(blob, 'diagram.svg');
-}
-
-export function exportMermaidPNG(container) {
- var svg = container.querySelector('svg');
- if (!svg) return;
- var data = new XMLSerializer().serializeToString(svg);
- var canvas = document.createElement('canvas');
- var ctx = canvas.getContext('2d');
- var img = new Image();
- var blob = new Blob([data], { type: 'image/svg+xml' });
- var url = URL.createObjectURL(blob);
-
- img.onload = function() {
- canvas.width = img.width * 2;
- canvas.height = img.height * 2;
- ctx.scale(2, 2);
- ctx.drawImage(img, 0, 0);
- URL.revokeObjectURL(url);
- canvas.toBlob(function(pngBlob) {
- downloadBlob(pngBlob, 'diagram.png');
- }, 'image/png');
- };
- img.src = url;
-}
-
-function downloadBlob(blob, filename) {
- var a = document.createElement('a');
- a.href = URL.createObjectURL(blob);
- a.download = filename;
- a.click();
- URL.revokeObjectURL(a.href);
-}
-
// ---------------------------------------------------------------------------
// Obsidian compatibility pre-processor (#24)
// ---------------------------------------------------------------------------
diff --git a/frontend/style.css b/frontend/style.css
index 4c585c2..3e0517b 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -7621,7 +7621,6 @@ body.popup-mode .content-area {
========================================================================== */
.mermaid-container {
- position: relative;
margin: 12px 0;
border: 1px solid var(--border);
border-radius: 8px;
@@ -7629,6 +7628,55 @@ body.popup-mode .content-area {
background: var(--bg-secondary);
}
+/* ── Header bar ── */
+.mermaid-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 6px 10px;
+ background: var(--bg-tertiary, #f0f0f5);
+ border-bottom: 1px solid var(--border);
+ min-height: 34px;
+}
+.mermaid-diagram-type {
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
+ font-size: 0.75rem; font-weight: 600;
+ color: var(--text-secondary);
+}
+.mermaid-header-actions {
+ display: flex; align-items: center; gap: 2px;
+}
+
+/* ── Buttons ── */
+.mermaid-btn {
+ width: 28px; height: 28px;
+ border: 1px solid transparent; border-radius: 4px;
+ background: transparent; color: var(--text-secondary);
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
+ transition: background 0.12s, color 0.12s;
+}
+.mermaid-btn:hover { background: var(--bg-primary); color: var(--accent); border-color: var(--border); }
+
+/* ── Code / Preview toggle ── */
+.mermaid-toggle {
+ display: flex; margin-left: 8px;
+ border: 1px solid var(--border); border-radius: 5px;
+ overflow: hidden;
+}
+.mermaid-toggle-btn {
+ padding: 2px 10px; font-size: 0.7rem; font-weight: 500;
+ border: none; background: transparent;
+ color: var(--text-muted, #999); cursor: pointer;
+ transition: background 0.12s, color 0.12s;
+}
+.mermaid-toggle-btn.active {
+ background: var(--bg-primary); color: var(--text-primary);
+ box-shadow: 0 0 0 1px var(--border);
+}
+.mermaid-toggle-btn:first-child { border-right: 1px solid var(--border); }
+
+/* ── Body ── */
+.mermaid-body {
+ position: relative;
+}
.mermaid-rendered {
padding: 16px;
overflow: hidden;
@@ -7636,25 +7684,17 @@ body.popup-mode .content-area {
display: flex;
justify-content: center;
}
-
.mermaid-rendered svg { max-width: 100%; height: auto; }
-.mermaid-controls {
- position: absolute;
- top: 6px; right: 6px;
- display: flex; gap: 2px;
- opacity: 0; transition: opacity 0.15s;
+.mermaid-source { padding: 16px; overflow: auto; }
+.mermaid-source pre {
+ margin: 0; padding: 12px;
+ background: var(--bg-tertiary, #f5f5f5);
+ border-radius: 4px;
+ font-size: 0.78rem; line-height: 1.5;
}
-.mermaid-container:hover .mermaid-controls { opacity: 1; }
-
-.mermaid-btn {
- width: 28px; height: 28px;
- border: 1px solid var(--border); border-radius: 4px;
- background: var(--bg-primary); color: var(--text-secondary);
- cursor: pointer; display: flex; align-items: center; justify-content: center;
-}
-.mermaid-btn:hover { color: var(--accent); border-color: var(--accent); }
+/* ── Error ── */
.mermaid-error {
margin: 12px 0; padding: 12px 16px;
border: 1px solid rgba(239,68,68,0.3); border-radius: 6px;
@@ -7663,22 +7703,33 @@ body.popup-mode .content-area {
}
.mermaid-error strong { display: block; margin-bottom: 4px; }
+/* ── Fullscreen ── */
.mermaid-fullscreen-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
- z-index: 10000; background: rgba(0,0,0,0.85);
- display: flex; align-items: center; justify-content: center;
+ z-index: 10000; background: rgba(0,0,0,0.92);
+ display: flex; flex-direction: column;
}
-.mermaid-fullscreen-close {
- position: absolute; top: 16px; right: 16px;
- width: 40px; height: 40px; border: 1px solid rgba(255,255,255,0.2);
- border-radius: 8px; background: rgba(0,0,0,0.5); color: #fff;
- cursor: pointer; display: flex; align-items: center; justify-content: center;
- z-index: 1;
+.mermaid-fullscreen-header {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 10px 16px;
+ background: rgba(255,255,255,0.06);
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+ min-height: 46px;
+ flex-shrink: 0;
+}
+.mermaid-fullscreen-header .mermaid-diagram-type { color: rgba(255,255,255,0.7); }
+.mermaid-fullscreen-header .mermaid-btn {
+ color: rgba(255,255,255,0.6);
+}
+.mermaid-fullscreen-header .mermaid-btn:hover {
+ background: rgba(255,255,255,0.1);
+ color: #fff;
}
-.mermaid-fullscreen-close:hover { background: rgba(255,255,255,0.1); }
.mermaid-fullscreen-content {
- width: 95vw; height: 95vh;
+ flex: 1;
display: flex; align-items: center; justify-content: center;
+ padding: 16px;
+ overflow: hidden;
}
.mermaid-fullscreen-content svg {
width: 100%; height: 100%;