feat: add interactive zoom, pan, and fullscreen support for Mermaid diagrams using svg-pan-zoom
This commit is contained in:
@@ -174,13 +174,11 @@
|
||||
.mermaid-container {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow-x: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.dark .mermaid-container {
|
||||
background: #0f172a;
|
||||
@@ -188,6 +186,131 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Mermaid zoom toolbar */
|
||||
.mermaid-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
.dark .mermaid-toolbar {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.mermaid-toolbar-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
margin-right: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
.mermaid-zoom-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dark .mermaid-zoom-btn {
|
||||
background: #1e293b;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: #94a3b8;
|
||||
}
|
||||
.mermaid-zoom-btn:hover {
|
||||
background: var(--accent-color, #4f46e5);
|
||||
border-color: var(--accent-color, #4f46e5);
|
||||
color: #fff;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
.mermaid-zoom-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.mermaid-zoom-pct {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
min-width: 38px;
|
||||
text-align: center;
|
||||
font-family: 'Inter', monospace;
|
||||
}
|
||||
.dark .mermaid-zoom-pct { color: #64748b; }
|
||||
|
||||
/* Mermaid svg canvas area */
|
||||
.mermaid-canvas {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
cursor: grab;
|
||||
padding: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mermaid-canvas:active { cursor: grabbing; }
|
||||
.mermaid-canvas svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Fullscreen overlay */
|
||||
.mermaid-fullscreen-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(2, 6, 23, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mermaid-fullscreen-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
.mermaid-fullscreen-inner {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: 92vw;
|
||||
max-width: 1200px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 40px 80px rgba(0,0,0,0.5);
|
||||
}
|
||||
.dark .mermaid-fullscreen-inner {
|
||||
background: #0f172a;
|
||||
}
|
||||
.mermaid-fullscreen-canvas {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
cursor: grab;
|
||||
}
|
||||
.mermaid-fullscreen-canvas:active { cursor: grabbing; }
|
||||
.mermaid-fullscreen-canvas svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Bullet lists */
|
||||
.proposal-content ul, .proposal-content ol {
|
||||
padding-left: 1.5rem;
|
||||
@@ -682,6 +805,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Mermaid.js for drawing diagrams -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.8.0/dist/mermaid.min.js"></script>
|
||||
<!-- SVG Pan Zoom for interactive Mermaid diagrams -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
||||
<!-- Confetti for approval celebration -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
|
||||
<!-- Lucide Premium Icons -->
|
||||
@@ -819,6 +944,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Mermaid zoom state store
|
||||
const mermaidZoomInstances = {};
|
||||
|
||||
// Scan code blocks for mermaid and convert them
|
||||
const codeElements = contentDiv.querySelectorAll('pre code.language-mermaid');
|
||||
codeElements.forEach((codeEl, index) => {
|
||||
@@ -830,25 +958,196 @@
|
||||
wrapper.className = 'mermaid-container no-print transition-all duration-300';
|
||||
wrapper.id = 'mermaid-wrapper-' + index;
|
||||
|
||||
// Zoom Toolbar
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'mermaid-toolbar';
|
||||
toolbar.innerHTML = `
|
||||
<span class="mermaid-toolbar-label">Diyagram Görünümü</span>
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır (Zoom In)" onclick="mermaidZoomIn(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-pct-${index}">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Uzaklaştır (Zoom Out)" onclick="mermaidZoomOut(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Tam Boyut (1:1)" onclick="mermaidZoomReset(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Tam Ekran" onclick="mermaidFullscreen(${index})">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Canvas wrapper
|
||||
const canvas = document.createElement('div');
|
||||
canvas.className = 'mermaid-canvas';
|
||||
canvas.id = 'mermaid-canvas-' + index;
|
||||
|
||||
const graphDiv = document.createElement('div');
|
||||
graphDiv.className = 'mermaid w-full';
|
||||
graphDiv.id = 'mermaid-graph-' + index;
|
||||
graphDiv.textContent = rawMermaid;
|
||||
|
||||
wrapper.appendChild(graphDiv);
|
||||
canvas.appendChild(graphDiv);
|
||||
wrapper.appendChild(toolbar);
|
||||
wrapper.appendChild(canvas);
|
||||
preContainer.parentNode.replaceChild(wrapper, preContainer);
|
||||
});
|
||||
|
||||
// Fullscreen overlay (singleton)
|
||||
const fsOverlay = document.createElement('div');
|
||||
fsOverlay.className = 'mermaid-fullscreen-overlay';
|
||||
fsOverlay.id = 'mermaid-fs-overlay';
|
||||
fsOverlay.innerHTML = `
|
||||
<div class="mermaid-fullscreen-inner" id="mermaid-fs-inner">
|
||||
<div class="mermaid-toolbar" style="border-radius:20px 20px 0 0;">
|
||||
<span class="mermaid-toolbar-label">Tam Ekran Görünüm</span>
|
||||
<button class="mermaid-zoom-btn" title="Yakınlaştır" onclick="mermaidFsZoom('in')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span class="mermaid-zoom-pct" id="mermaid-fs-pct">100%</span>
|
||||
<button class="mermaid-zoom-btn" title="Uzaklaştır" onclick="mermaidFsZoom('out')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="1:1 Sıfırla" onclick="mermaidFsZoom('reset')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</button>
|
||||
<button class="mermaid-zoom-btn" title="Kapat" onclick="closeMermaidFullscreen()" style="background:#ef4444;border-color:#ef4444;color:#fff;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mermaid-fullscreen-canvas" id="mermaid-fs-canvas"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(fsOverlay);
|
||||
fsOverlay.addEventListener('click', function(e) { if (e.target === fsOverlay) closeMermaidFullscreen(); });
|
||||
|
||||
// Run mermaid parser
|
||||
setTimeout(() => {
|
||||
mermaid.run().then(() => {
|
||||
// Re-init lucide icons inside alerts after marked rendering completes
|
||||
lucide.createIcons();
|
||||
|
||||
// Initialize svg-pan-zoom for each mermaid diagram
|
||||
codeElements.forEach((_, index) => {
|
||||
initMermaidZoom(index);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error("Mermaid drawing error:", err);
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Init svg-pan-zoom for a given mermaid index
|
||||
function initMermaidZoom(index) {
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
if (!canvas) return;
|
||||
const svg = canvas.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
// Ensure SVG has an ID for svg-pan-zoom
|
||||
if (!svg.id) svg.id = 'mermaid-svg-' + index;
|
||||
|
||||
// Make SVG fill canvas
|
||||
svg.style.width = '100%';
|
||||
svg.style.height = 'auto';
|
||||
svg.removeAttribute('height');
|
||||
|
||||
try {
|
||||
const panZoom = svgPanZoom('#' + svg.id, {
|
||||
zoomEnabled: true,
|
||||
controlIconsEnabled: false,
|
||||
fit: true,
|
||||
center: true,
|
||||
minZoom: 0.3,
|
||||
maxZoom: 8,
|
||||
zoomScaleSensitivity: 0.3,
|
||||
onZoom: (zoom) => {
|
||||
const pctEl = document.getElementById('mermaid-pct-' + index);
|
||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
||||
}
|
||||
});
|
||||
mermaidZoomInstances[index] = panZoom;
|
||||
} catch(e) {
|
||||
console.warn('svg-pan-zoom init failed for mermaid-' + index, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom controls
|
||||
function mermaidZoomIn(index) {
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (pz) { pz.zoomIn(); updatePct(index, pz.getZoom()); }
|
||||
}
|
||||
function mermaidZoomOut(index) {
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (pz) { pz.zoomOut(); updatePct(index, pz.getZoom()); }
|
||||
}
|
||||
function mermaidZoomReset(index) {
|
||||
const pz = mermaidZoomInstances[index];
|
||||
if (pz) { pz.resetZoom(); pz.center(); updatePct(index, pz.getZoom()); }
|
||||
}
|
||||
function updatePct(index, zoom) {
|
||||
const pctEl = document.getElementById('mermaid-pct-' + index);
|
||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
||||
}
|
||||
|
||||
// Fullscreen
|
||||
let fsActivePanZoom = null;
|
||||
function mermaidFullscreen(index) {
|
||||
const canvas = document.getElementById('mermaid-canvas-' + index);
|
||||
if (!canvas) return;
|
||||
const svg = canvas.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
const fsCanvas = document.getElementById('mermaid-fs-canvas');
|
||||
const svgClone = svg.cloneNode(true);
|
||||
svgClone.id = 'mermaid-fs-svg-' + index;
|
||||
svgClone.style.width = '100%';
|
||||
svgClone.style.height = 'auto';
|
||||
svgClone.removeAttribute('height');
|
||||
fsCanvas.innerHTML = '';
|
||||
fsCanvas.appendChild(svgClone);
|
||||
|
||||
document.getElementById('mermaid-fs-overlay').classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Init pan-zoom on cloned SVG
|
||||
setTimeout(() => {
|
||||
try {
|
||||
fsActivePanZoom = svgPanZoom('#mermaid-fs-svg-' + index, {
|
||||
zoomEnabled: true,
|
||||
controlIconsEnabled: false,
|
||||
fit: true,
|
||||
center: true,
|
||||
minZoom: 0.2,
|
||||
maxZoom: 10,
|
||||
zoomScaleSensitivity: 0.3,
|
||||
onZoom: (zoom) => {
|
||||
const pctEl = document.getElementById('mermaid-fs-pct');
|
||||
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
|
||||
}
|
||||
});
|
||||
document.getElementById('mermaid-fs-pct').textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
|
||||
} catch(e) {}
|
||||
}, 100);
|
||||
}
|
||||
function closeMermaidFullscreen() {
|
||||
document.getElementById('mermaid-fs-overlay').classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
if (fsActivePanZoom) { try { fsActivePanZoom.destroy(); } catch(e) {} fsActivePanZoom = null; }
|
||||
}
|
||||
function mermaidFsZoom(action) {
|
||||
if (!fsActivePanZoom) return;
|
||||
if (action === 'in') fsActivePanZoom.zoomIn();
|
||||
else if (action === 'out') fsActivePanZoom.zoomOut();
|
||||
else if (action === 'reset') { fsActivePanZoom.resetZoom(); fsActivePanZoom.center(); }
|
||||
const pctEl = document.getElementById('mermaid-fs-pct');
|
||||
if (pctEl) pctEl.textContent = Math.round(fsActivePanZoom.getZoom() * 100) + '%';
|
||||
}
|
||||
// Keyboard ESC closes fullscreen
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeMermaidFullscreen();
|
||||
});
|
||||
|
||||
// 3. Dynamic Sidebar TOC Generator
|
||||
const tocNav = document.getElementById('toc-nav');
|
||||
const headings = contentDiv.querySelectorAll('h2, h3');
|
||||
|
||||
Reference in New Issue
Block a user