refactor: standardize mermaid diagram styling and improve interactive zoom toolbar UI

This commit is contained in:
Ümit Tunç
2026-07-30 17:00:17 +03:00
parent aa003df568
commit 4576739e6e
2 changed files with 146 additions and 58 deletions
@@ -138,7 +138,7 @@ class ProposalForm
'coral' => 'Coral Corporate (Mercan Kırmızı)', 'coral' => 'Coral Corporate (Mercan Kırmızı)',
'amber' => 'Amber Executive (Kehribar/Altın)', 'amber' => 'Amber Executive (Kehribar/Altın)',
]) ])
->default('indigo') ->default('coral')
->live() ->live()
->columnSpan(4), ->columnSpan(4),
+137 -49
View File
@@ -296,7 +296,68 @@
display: block; display: block;
} }
.mermaid-container.mermaid-active { .mermaid-container.mermaid-active {
box-shadow: 0 0 0 2px var(--accent-color, #4f46e5); box-shadow: 0 0 0 2px var(--accent-color, #ea580c);
}
/* Mermaid Orange/Red Brand Theme Styling Overrides */
.mermaid svg .task {
fill: #ea580c !important;
stroke: #c2410c !important;
rx: 6px !important;
ry: 6px !important;
}
.mermaid svg .task0, .mermaid svg .task2 {
fill: #ea580c !important;
stroke: #c2410c !important;
}
.mermaid svg .task1, .mermaid svg .task3 {
fill: #ef4444 !important;
stroke: #b91c1c !important;
}
.mermaid svg .taskText,
.mermaid svg .taskTextOutsideRight,
.mermaid svg .taskTextOutsideLeft {
font-family: 'Inter', sans-serif !important;
font-weight: 600 !important;
}
.mermaid svg .taskText {
fill: #ffffff !important;
}
.mermaid svg .taskTextOutsideRight,
.mermaid svg .taskTextOutsideLeft {
fill: #1e293b !important;
}
.dark .mermaid svg .taskTextOutsideRight,
.dark .mermaid svg .taskTextOutsideLeft {
fill: #f1f5f9 !important;
}
.mermaid svg .section0,
.mermaid svg .section2 {
fill: rgba(234, 88, 12, 0.08) !important;
}
.dark .mermaid svg .section0,
.dark .mermaid svg .section2 {
fill: rgba(234, 88, 12, 0.18) !important;
}
.mermaid svg .section1,
.mermaid svg .section3 {
fill: rgba(239, 68, 68, 0.08) !important;
}
.dark .mermaid svg .section1,
.dark .mermaid svg .section3 {
fill: rgba(239, 68, 68, 0.18) !important;
}
.mermaid svg .today {
stroke: #ef4444 !important;
stroke-width: 2px !important;
}
.mermaid svg .titleText {
fill: #0f172a !important;
font-weight: 700 !important;
font-family: 'Outfit', sans-serif !important;
}
.dark .mermaid svg .titleText {
fill: #f8fafc !important;
} }
/* Fullscreen overlay */ /* Fullscreen overlay */
@@ -411,27 +472,27 @@
</style> </style>
@php @php
$accent = $proposal->meta['accent_color'] ?? 'indigo'; $accent = $proposal->meta['accent_color'] ?? 'coral';
$accentColor = match($accent) { $accentColor = match($accent) {
'emerald' => '#059669', 'emerald' => '#059669',
'cyberpunk' => '#ec4899', 'cyberpunk' => '#ec4899',
'coral' => '#ea580c', 'coral' => '#ea580c',
'amber' => '#d97706', 'amber' => '#d97706',
default => '#4f46e5', default => '#ea580c',
}; };
$accentColorGlow = match($accent) { $accentColorGlow = match($accent) {
'emerald' => 'rgba(5, 150, 105, 0.2)', 'emerald' => 'rgba(5, 150, 105, 0.2)',
'cyberpunk' => 'rgba(236, 72, 153, 0.2)', 'cyberpunk' => 'rgba(236, 72, 153, 0.2)',
'coral' => 'rgba(234, 88, 12, 0.2)', 'coral' => 'rgba(234, 88, 12, 0.2)',
'amber' => 'rgba(217, 119, 6, 0.2)', 'amber' => 'rgba(217, 119, 6, 0.2)',
default => 'rgba(79, 70, 229, 0.2)', default => 'rgba(234, 88, 12, 0.2)',
}; };
$themeClass = match($accent) { $themeClass = match($accent) {
'emerald' => 'theme-emerald bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500 text-emerald-600 dark:text-emerald-400 border-emerald-500/20', 'emerald' => 'theme-emerald bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500 text-emerald-600 dark:text-emerald-400 border-emerald-500/20',
'cyberpunk' => 'theme-rose bg-rose-500 hover:bg-rose-600 focus:ring-rose-400 text-rose-500 dark:text-rose-400 border-rose-500/20', 'cyberpunk' => 'theme-rose bg-rose-500 hover:bg-rose-600 focus:ring-rose-400 text-rose-500 dark:text-rose-400 border-rose-500/20',
'coral' => 'theme-orange bg-orange-600 hover:bg-orange-700 focus:ring-orange-500 text-orange-600 dark:text-orange-400 border-orange-500/20', 'coral' => 'theme-orange bg-orange-600 hover:bg-orange-700 focus:ring-orange-500 text-orange-600 dark:text-orange-400 border-orange-500/20',
'amber' => 'theme-amber bg-amber-600 hover:bg-amber-700 focus:ring-amber-500 text-amber-600 dark:text-amber-400 border-amber-500/20', 'amber' => 'theme-amber bg-amber-600 hover:bg-amber-700 focus:ring-amber-500 text-amber-600 dark:text-amber-400 border-amber-500/20',
default => 'theme-indigo bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500 text-indigo-600 dark:text-indigo-400 border-indigo-500/20', default => 'theme-orange bg-orange-600 hover:bg-orange-700 focus:ring-orange-500 text-orange-600 dark:text-orange-400 border-orange-500/20',
}; };
$gradient = match($accent) { $gradient = match($accent) {
'emerald' => 'from-emerald-600 to-teal-600 dark:from-emerald-500 dark:to-teal-500', 'emerald' => 'from-emerald-600 to-teal-600 dark:from-emerald-500 dark:to-teal-500',
@@ -966,10 +1027,39 @@
const isDark = document.documentElement.classList.contains('dark'); const isDark = document.documentElement.classList.contains('dark');
mermaid.initialize({ mermaid.initialize({
startOnLoad: false, startOnLoad: false,
theme: isDark ? 'dark' : 'default', theme: 'base',
securityLevel: 'loose', securityLevel: 'loose',
themeVariables: { themeVariables: {
fontFamily: 'Inter, sans-serif' fontFamily: 'Inter, sans-serif',
darkMode: isDark,
background: isDark ? '#0f172a' : '#ffffff',
primaryColor: '#ea580c',
primaryTextColor: '#ffffff',
primaryBorderColor: '#c2410c',
lineColor: '#ea580c',
secondaryColor: '#ef4444',
secondaryTextColor: '#ffffff',
tertiaryColor: isDark ? '#1e293b' : '#fff7ed',
tertiaryTextColor: isDark ? '#f8fafc' : '#9a3412',
activeTaskBkgColor: '#ea580c',
activeTaskBorderColor: '#c2410c',
doneTaskBkgColor: '#f97316',
doneTaskBorderColor: '#ea580c',
critBkgColor: '#ef4444',
critBorderColor: '#b91c1c',
taskTextLightColor: '#ffffff',
taskTextColor: '#ffffff',
taskTextDarkColor: '#ffffff',
taskTextOutsideColor: isDark ? '#e2e8f0' : '#1e293b',
sectionBkgColor: isDark ? 'rgba(234, 88, 12, 0.15)' : 'rgba(234, 88, 12, 0.07)',
sectionBkgColor2: isDark ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.07)',
altSectionBkgColor: isDark ? 'rgba(234, 88, 12, 0.10)' : 'rgba(234, 88, 12, 0.04)',
gridColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.08)',
todayLineColor: '#ef4444',
nodeBorder: '#ea580c',
clusterBkg: isDark ? '#1e293b' : '#fff7ed',
clusterBorder: '#f97316',
titleColor: isDark ? '#f8fafc' : '#0f172a'
} }
}); });
@@ -986,20 +1076,18 @@
const wrapper = document.getElementById('mermaid-wrapper-' + index); const wrapper = document.getElementById('mermaid-wrapper-' + index);
const canvas = document.getElementById('mermaid-canvas-' + index); const canvas = document.getElementById('mermaid-canvas-' + index);
const pz = mermaidZoomInstances[index]; const pz = mermaidZoomInstances[index];
if (!wrapper || !canvas || !pz) return; if (!wrapper || !canvas) return;
wrapper.classList.add('mermaid-active'); wrapper.classList.add('mermaid-active');
canvas.classList.add('mermaid-active'); canvas.classList.add('mermaid-active');
pz.enablePan(); if (pz) pz.enablePan();
} }
function deactivateMermaid(index) { function deactivateMermaid(index) {
const wrapper = document.getElementById('mermaid-wrapper-' + index); const wrapper = document.getElementById('mermaid-wrapper-' + index);
const canvas = document.getElementById('mermaid-canvas-' + index); const canvas = document.getElementById('mermaid-canvas-' + index);
const pz = mermaidZoomInstances[index];
if (wrapper) wrapper.classList.remove('mermaid-active'); if (wrapper) wrapper.classList.remove('mermaid-active');
if (canvas) canvas.classList.remove('mermaid-active'); if (canvas) canvas.classList.remove('mermaid-active');
if (pz) pz.disablePan();
if (mermaidActiveIndex === index) mermaidActiveIndex = null; if (mermaidActiveIndex === index) mermaidActiveIndex = null;
} }
@@ -1041,25 +1129,19 @@
wrapper.id = 'mermaid-wrapper-' + index; wrapper.id = 'mermaid-wrapper-' + index;
wrapper.dataset.mermaidSource = rawMermaid; wrapper.dataset.mermaidSource = rawMermaid;
// Zoom Toolbar // Zoom Toolbar (Uzaklaştır [-] | 100% | Yakınlaştır [+] | Sıfırla/Sığdır | Tam Ekran)
const toolbar = document.createElement('div'); const toolbar = document.createElement('div');
toolbar.className = 'mermaid-toolbar'; toolbar.className = 'mermaid-toolbar';
toolbar.innerHTML = ` toolbar.innerHTML = `
<span class="mermaid-toolbar-label">Diyagram Görünümü</span> <span class="mermaid-toolbar-label">DİYAGRAM GÖRÜNÜMÜ</span>
<span class="mermaid-toolbar-hint">Zoom ve kaydırma için tıklayın</span> <span class="mermaid-toolbar-hint">Zoom ve sürükleme için tıklayın</span>
<div class="mermaid-toolbar-controls"> <div class="mermaid-toolbar-controls">
<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})"> <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> <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>
<button class="mermaid-zoom-btn" title="Tam Boyut (1:1)" onclick="mermaidZoomReset(${index})"> <span class="mermaid-zoom-pct" id="mermaid-pct-${index}">100%</span>
<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 class="mermaid-zoom-btn" title="Yakınlaştır (Zoom In)" onclick="mermaidZoomIn(${index})">
</button> <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 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> </button>
</div> </div>
`; `;
@@ -1070,7 +1152,7 @@
canvas.id = 'mermaid-canvas-' + index; canvas.id = 'mermaid-canvas-' + index;
const graphDiv = document.createElement('div'); const graphDiv = document.createElement('div');
graphDiv.className = 'mermaid w-full'; graphDiv.className = 'mermaid w-full h-full';
graphDiv.id = 'mermaid-graph-' + index; graphDiv.id = 'mermaid-graph-' + index;
graphDiv.textContent = rawMermaid; graphDiv.textContent = rawMermaid;
@@ -1088,20 +1170,22 @@
<div class="mermaid-fullscreen-inner" id="mermaid-fs-inner"> <div class="mermaid-fullscreen-inner" id="mermaid-fs-inner">
<div class="mermaid-toolbar" style="border-radius:20px 20px 0 0;"> <div class="mermaid-toolbar" style="border-radius:20px 20px 0 0;">
<span class="mermaid-toolbar-label">Tam Ekran Görünüm</span> <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')"> <div class="mermaid-toolbar-controls">
<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')"> <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> <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>
<button class="mermaid-zoom-btn" title="1:1 Sıfırla" onclick="mermaidFsZoom('reset')"> <span class="mermaid-zoom-pct" id="mermaid-fs-pct">100%</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>
<button class="mermaid-zoom-btn" title="Sığdır / 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> <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>
<button class="mermaid-zoom-btn" title="Kapat" onclick="closeMermaidFullscreen()" style="background:#ef4444;border-color:#ef4444;color:#fff;"> <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> <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> </button>
</div> </div>
</div>
<div class="mermaid-fullscreen-canvas" id="mermaid-fs-canvas"></div> <div class="mermaid-fullscreen-canvas" id="mermaid-fs-canvas"></div>
</div> </div>
`; `;
@@ -1117,10 +1201,7 @@
// Run mermaid parser // Run mermaid parser
setTimeout(() => { setTimeout(() => {
mermaid.run().then(() => { mermaid.run().then(() => {
// Re-init lucide icons inside alerts after marked rendering completes
lucide.createIcons(); lucide.createIcons();
// Initialize svg-pan-zoom for each mermaid diagram
codeElements.forEach((_, index) => { codeElements.forEach((_, index) => {
initMermaidZoom(index); initMermaidZoom(index);
}); });
@@ -1136,48 +1217,50 @@
const svg = canvas.querySelector('svg'); const svg = canvas.querySelector('svg');
if (!svg) return; if (!svg) return;
// Ensure SVG has an ID for svg-pan-zoom
if (!svg.id) svg.id = 'mermaid-svg-' + index; if (!svg.id) svg.id = 'mermaid-svg-' + index;
// Compute a good canvas height from SVG viewBox aspect ratio svg.setAttribute('width', '100%');
const canvasW = canvas.clientWidth || canvas.offsetWidth || 800; svg.setAttribute('height', '100%');
let canvasH = 520; svg.style.width = '100%';
svg.style.height = '100%';
// Compute canvas height from SVG viewBox aspect ratio
let canvasH = 500;
const vb = svg.getAttribute('viewBox'); const vb = svg.getAttribute('viewBox');
if (vb) { if (vb) {
const parts = vb.split(/[\s,]+/); const parts = vb.split(/[\s,]+/);
if (parts.length >= 4) { if (parts.length >= 4) {
const svgW = parseFloat(parts[2]); const svgW = parseFloat(parts[2]);
const svgH = parseFloat(parts[3]); const svgH = parseFloat(parts[3]);
const canvasW = canvas.clientWidth || 800;
if (svgW > 0 && svgH > 0) { if (svgW > 0 && svgH > 0) {
canvasH = Math.max(400, Math.min(Math.round(svgH * (canvasW / svgW)), 900)); canvasH = Math.max(380, Math.min(Math.round(svgH * (canvasW / svgW)), 800));
} }
} }
} }
canvas.style.height = canvasH + 'px'; canvas.style.height = canvasH + 'px';
// svg-pan-zoom needs explicit width/height on the SVG element
svg.setAttribute('width', canvasW);
svg.setAttribute('height', canvasH);
svg.style.width = canvasW + 'px';
svg.style.height = canvasH + 'px';
try { try {
if (mermaidZoomInstances[index]) {
try { mermaidZoomInstances[index].destroy(); } catch(e) {}
}
const panZoom = svgPanZoom('#' + svg.id, { const panZoom = svgPanZoom('#' + svg.id, {
zoomEnabled: true, zoomEnabled: true,
panEnabled: false, panEnabled: true,
controlIconsEnabled: false, controlIconsEnabled: false,
fit: true, fit: true,
center: true, center: true,
minZoom: 0.3, minZoom: 0.3,
maxZoom: 8, maxZoom: 10,
zoomScaleSensitivity: 0.3, zoomScaleSensitivity: 0.25,
mouseWheelZoomEnabled: false, mouseWheelZoomEnabled: false,
onZoom: (zoom) => { onZoom: (zoom) => {
const pctEl = document.getElementById('mermaid-pct-' + index); updatePct(index, zoom);
if (pctEl) pctEl.textContent = Math.round(zoom * 100) + '%';
} }
}); });
mermaidZoomInstances[index] = panZoom; mermaidZoomInstances[index] = panZoom;
updatePct(index, panZoom.getZoom());
setupMermaidActivation(index); setupMermaidActivation(index);
} catch(e) { } catch(e) {
console.warn('svg-pan-zoom init failed for mermaid-' + index, e); console.warn('svg-pan-zoom init failed for mermaid-' + index, e);
@@ -1198,7 +1281,12 @@
function mermaidZoomReset(index) { function mermaidZoomReset(index) {
setMermaidActive(index); setMermaidActive(index);
const pz = mermaidZoomInstances[index]; const pz = mermaidZoomInstances[index];
if (pz) { pz.resetZoom(); pz.center(); updatePct(index, pz.getZoom()); } if (pz) {
pz.resetZoom();
pz.fit();
pz.center();
updatePct(index, pz.getZoom());
}
} }
function updatePct(index, zoom) { function updatePct(index, zoom) {
const pctEl = document.getElementById('mermaid-pct-' + index); const pctEl = document.getElementById('mermaid-pct-' + index);