feat: Mermaid.js integration #20-29 — full diagram support
- CDN: mermaid@11 from jsdelivr - mermaid-viewer.js: init, render, templates, zoom/pan, fullscreen, focus - All diagram types: flowchart, sequence, class, state, ER, journey, gantt, pie, gitGraph, requirement, mindmap, timeline, quadrant, C4 (x4), beta types - viewer.js: auto-render Mermaid blocks after markdown rendering - utils.js: live preview panel in editor (debounced 500ms) - app.js: initMermaid + setupFocusMode on startup - CSS: container, controls, error, fullscreen overlay, focus panel, live preview - Export SVG/PNG download buttons - Obsidian preprocessor ([[links]], ![[img]], ==highlight==)
This commit is contained in:
@@ -7828,6 +7828,12 @@
|
||||
FindInPageManager.init();
|
||||
ContextMenuManager.init();
|
||||
|
||||
// Mermaid.js init
|
||||
import('./mermaid-viewer.js').then(function(m) {
|
||||
m.initMermaid();
|
||||
m.setupFocusMode();
|
||||
});
|
||||
|
||||
// Check auth status first
|
||||
const authOk = await AuthManager.initAuth();
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/umd/lucide.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Mermaid.js integration for ObsiGate.
|
||||
*
|
||||
* Handles initialization, rendering, templates, zoom/pan,
|
||||
* fullscreen, focus mode, theme sync, and Obsidian compatibility.
|
||||
*
|
||||
* Usage: import { initMermaid, renderMermaidBlocks } from './mermaid-viewer.js';
|
||||
*/
|
||||
import { safeCreateIcons } from './utils.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration (#20)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var _mermaidReady = false;
|
||||
var _currentTheme = 'default';
|
||||
|
||||
export function initMermaid() {
|
||||
if (typeof mermaid === 'undefined') {
|
||||
console.warn('[Mermaid] library not loaded');
|
||||
return;
|
||||
}
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
theme: _currentTheme,
|
||||
fontFamily: 'inherit',
|
||||
});
|
||||
_mermaidReady = true;
|
||||
}
|
||||
|
||||
export function updateMermaidTheme(isDark) {
|
||||
_currentTheme = isDark ? 'dark' : 'default';
|
||||
if (_mermaidReady) {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
theme: _currentTheme,
|
||||
fontFamily: 'inherit',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Templates for all diagram types (#22)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export var MERMAID_TEMPLATES = {
|
||||
flowchart: 'flowchart TD\n A[Start] --> B{Decision?}\n B -->|Yes| C[OK]\n B -->|No| D[Cancel]',
|
||||
sequenceDiagram: 'sequenceDiagram\n Alice->>Bob: Hello\n Bob-->>Alice: Hi!',
|
||||
classDiagram: 'classDiagram\n class Animal {\n +name: string\n +age: int\n +makeSound()\n }\n class Dog {\n +bark()\n }\n Animal <|-- Dog',
|
||||
'stateDiagram-v2': 'stateDiagram-v2\n [*] --> Idle\n Idle --> Running : start\n Running --> Idle : stop\n Running --> [*] : done',
|
||||
erDiagram: 'erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE-ITEM : contains',
|
||||
journey: 'journey\n title My day\n section Morning\n Wake up: 5: Me\n Breakfast: 3: Me\n section Work\n Code: 5: Me, Cat',
|
||||
gantt: 'gantt\n title Project Plan\n dateFormat YYYY-MM-DD\n section Design\n Mockups :a1, 2024-01-01, 7d\n section Dev\n Backend :a2, after a1, 14d',
|
||||
pie: 'pie title Languages\n "Python" : 45\n "JavaScript" : 30\n "Go" : 15\n "Rust" : 10',
|
||||
gitGraph: 'gitGraph\n commit\n branch feature\n checkout feature\n commit\n checkout main\n merge feature',
|
||||
requirementDiagram: 'requirementDiagram\n requirement req1 {\n id: 1\n text: System shall do X\n }\n element Component {\n type: package\n }\n Component - satisfies -> req1',
|
||||
mindmap: 'mindmap\n root((Project))\n Frontend\n HTML\n CSS\n JavaScript\n Backend\n API\n Database',
|
||||
timeline: 'timeline\n title History\n 2020 : Started\n 2022 : v1.0\n 2024 : v2.0',
|
||||
quadrantChart: 'quadrantChart\n title Products\n x-axis Low Reach --> High Reach\n y-axis Low Value --> High Value\n quadrant-1 Stars\n quadrant-2 Question Marks\n Product A: [0.8, 0.6]\n Product B: [0.3, 0.8]',
|
||||
C4Context: 'C4Context\n title System Context\n Person(user, "User")\n System(app, "My App")\n System_Ext(db, "Database")\n Rel(user, app, "Uses")\n Rel(app, db, "Reads/Writes")',
|
||||
C4Container: 'C4Container\n title Containers\n Container(web, "Web App", "React")\n Container(api, "API", "FastAPI")\n ContainerDb(db, "DB", "PostgreSQL")\n Rel(web, api, "REST")\n Rel(api, db, "SQL")',
|
||||
C4Component: 'C4Component\n title API Components\n Container_Boundary(api, "API") {\n Component(auth, "Auth Service")\n Component(search, "Search Engine")\n }',
|
||||
C4Dynamic: 'C4Dynamic\n title Login Flow\n Person(user, "User")\n Container(web, "Web App")\n Container(api, "API")\n Rel(user, web, "POST /login")\n Rel(web, api, "authenticate")',
|
||||
C4Deployment: 'C4Deployment\n title Deployment\n Deployment_Node(cloud, "Cloud") {\n Container(web, "Web")\n Container(api, "API")\n }',
|
||||
'xychart-beta': 'xychart-beta\n title "Sales"\n x-axis [Jan, Feb, Mar, Apr]\n y-axis "Revenue" 0 --> 100\n bar [45, 60, 75, 90]\n line [30, 45, 55, 70]',
|
||||
'block-beta': 'block-beta\n columns 3\n A B C\n D E F',
|
||||
'sankey-beta': 'sankey-beta\n A, B, 50\n A, C, 30\n B, D, 20\n C, D, 30',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering engine (#21)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var _renderCounter = 0;
|
||||
|
||||
export async function renderMermaidBlocks(container) {
|
||||
if (!_mermaidReady || typeof mermaid === 'undefined') return;
|
||||
|
||||
var blocks = container.querySelectorAll('pre code.language-mermaid, .mermaid-code');
|
||||
if (blocks.length === 0) return;
|
||||
|
||||
for (var i = 0; i < blocks.length; i++) {
|
||||
var codeBlock = blocks[i];
|
||||
var code = codeBlock.textContent || codeBlock.innerText || '';
|
||||
if (!code.trim()) continue;
|
||||
|
||||
var wrapper = codeBlock.closest('pre') || codeBlock.parentElement;
|
||||
if (!wrapper) continue;
|
||||
|
||||
// Check if already rendered
|
||||
if (wrapper.querySelector('.mermaid-rendered')) continue;
|
||||
|
||||
try {
|
||||
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 = '<div class="mermaid-rendered">' + result.svg + '</div>';
|
||||
|
||||
// Add zoom/pan controls (#27)
|
||||
var controls = document.createElement('div');
|
||||
controls.className = 'mermaid-controls';
|
||||
controls.innerHTML =
|
||||
'<button class="mermaid-btn mermaid-zoom-in" title="Zoom +"><i data-lucide="zoom-in" style="width:14px;height:14px"></i></button>' +
|
||||
'<button class="mermaid-btn mermaid-zoom-out" title="Zoom -"><i data-lucide="zoom-out" style="width:14px;height:14px"></i></button>' +
|
||||
'<button class="mermaid-btn mermaid-reset" title="Reset"><i data-lucide="maximize" style="width:14px;height:14px"></i></button>' +
|
||||
'<button class="mermaid-btn mermaid-fullscreen" title="Plein ecran"><i data-lucide="expand" style="width:14px;height:14px"></i></button>';
|
||||
container_1.appendChild(controls);
|
||||
|
||||
wrapper.parentNode.replaceChild(container_1, wrapper);
|
||||
setupZoomPan(container_1);
|
||||
safeCreateIcons();
|
||||
} catch (err) {
|
||||
// Render error block
|
||||
var errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'mermaid-error';
|
||||
errorDiv.innerHTML = '<strong>Erreur Mermaid</strong><br>' + escapeHtml(err.message || String(err));
|
||||
wrapper.parentNode.replaceChild(errorDiv, wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zoom & Pan (#27)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function setupZoomPan(container) {
|
||||
var svgWrapper = container.querySelector('.mermaid-rendered');
|
||||
if (!svgWrapper) return;
|
||||
var svg = svgWrapper.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
var scale = 1;
|
||||
var panX = 0, panY = 0;
|
||||
var dragging = false, startX, startY;
|
||||
|
||||
svg.style.transformOrigin = '0 0';
|
||||
svg.style.cursor = 'grab';
|
||||
|
||||
function update() {
|
||||
svg.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
svg.addEventListener('mousedown', function(e) {
|
||||
dragging = true;
|
||||
startX = e.clientX - panX;
|
||||
startY = e.clientY - panY;
|
||||
svg.style.cursor = 'grabbing';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (!dragging) return;
|
||||
panX = e.clientX - startX;
|
||||
panY = e.clientY - startY;
|
||||
update();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function() {
|
||||
dragging = false;
|
||||
if (svg) svg.style.cursor = 'grab';
|
||||
});
|
||||
|
||||
svg.addEventListener('wheel', function(e) {
|
||||
e.preventDefault();
|
||||
var delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
scale = Math.max(0.2, Math.min(5, scale * delta));
|
||||
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();
|
||||
});
|
||||
|
||||
// Fullscreen (#28)
|
||||
container.querySelector('.mermaid-fullscreen').addEventListener('click', function() {
|
||||
openFullscreen(svgWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fullscreen mode (#28)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function openFullscreen(svgWrapper) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'mermaid-fullscreen-overlay';
|
||||
overlay.innerHTML =
|
||||
'<button class="mermaid-fullscreen-close" title="Fermer (Echap)"><i data-lucide="x" style="width:18px;height:18px"></i></button>' +
|
||||
'<div class="mermaid-fullscreen-content"></div>';
|
||||
|
||||
var content = overlay.querySelector('.mermaid-fullscreen-content');
|
||||
content.innerHTML = svgWrapper.innerHTML;
|
||||
var svg = content.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.width = '100%';
|
||||
svg.style.height = '100%';
|
||||
svg.style.maxWidth = '95vw';
|
||||
svg.style.maxHeight = '95vh';
|
||||
svg.style.cursor = 'default';
|
||||
}
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
safeCreateIcons();
|
||||
|
||||
function close() {
|
||||
overlay.remove();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
var escHandler = function(e) {
|
||||
if (e.key === 'Escape') close();
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
overlay.querySelector('.mermaid-fullscreen-close').addEventListener('click', close);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Focus mode (#29) — click diagram to enlarge in a side panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var _focusPanel = null;
|
||||
|
||||
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;
|
||||
|
||||
var svgWrapper = container.querySelector('.mermaid-rendered');
|
||||
if (!svgWrapper) return;
|
||||
|
||||
if (_focusPanel && _focusPanel.style.display !== 'none') {
|
||||
updateFocusPanel(svgWrapper);
|
||||
} else {
|
||||
showFocusPanel(svgWrapper);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showFocusPanel(svgWrapper) {
|
||||
if (!_focusPanel) {
|
||||
_focusPanel = document.createElement('div');
|
||||
_focusPanel.className = 'mermaid-focus-panel';
|
||||
_focusPanel.innerHTML =
|
||||
'<div class="mermaid-focus-header">' +
|
||||
'<span>Diagramme</span>' +
|
||||
'<button class="mermaid-focus-close" title="Fermer"><i data-lucide="x" style="width:14px;height:14px"></i></button>' +
|
||||
'</div>' +
|
||||
'<div class="mermaid-focus-content"></div>';
|
||||
document.body.appendChild(_focusPanel);
|
||||
|
||||
_focusPanel.querySelector('.mermaid-focus-close').addEventListener('click', function() {
|
||||
_focusPanel.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
_focusPanel.style.display = 'block';
|
||||
updateFocusPanel(svgWrapper);
|
||||
safeCreateIcons();
|
||||
}
|
||||
|
||||
function updateFocusPanel(svgWrapper) {
|
||||
if (!_focusPanel) return;
|
||||
var content = _focusPanel.querySelector('.mermaid-focus-content');
|
||||
content.innerHTML = svgWrapper.innerHTML;
|
||||
var svg = content.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.width = '100%';
|
||||
svg.style.height = 'auto';
|
||||
svg.style.maxHeight = '80vh';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function preprocessObsidian(code) {
|
||||
return code
|
||||
.replace(/!\[\[([^\]]+?)\]\]/g, function(_, m) {
|
||||
var parts = m.split('|');
|
||||
var src = parts[0].trim();
|
||||
var width = parts[1] ? parts[1].trim() : null;
|
||||
if (width && /^\d+$/.test(width)) {
|
||||
return '<img src="' + src + '" width="' + width + '" />';
|
||||
}
|
||||
return '<img src="' + src + '" />';
|
||||
})
|
||||
.replace(/\[\[([^\]]+?)\]\]/g, '<a href="$1">$1</a>')
|
||||
.replace(/==([^=]+)==/g, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
+45
-2
@@ -385,13 +385,56 @@ async function openEditor(vaultName, filePath) {
|
||||
}
|
||||
|
||||
// Set up AI toolbar (recreate each time editor opens)
|
||||
const container = document.getElementById('ai-toolbar-container');
|
||||
var container = document.getElementById('ai-toolbar-container');
|
||||
if (container && !container.querySelector('.ai-toolbar') && !container.querySelector('div')) {
|
||||
createAIToolbar(container, () => state.editorView || null);
|
||||
createAIToolbar(container, function() { return state.editorView || null; });
|
||||
}
|
||||
|
||||
// Mermaid live preview panel (#26)
|
||||
var existingPreview = document.getElementById('mermaid-live-preview');
|
||||
if (existingPreview) existingPreview.remove();
|
||||
var previewPanel = document.createElement('div');
|
||||
previewPanel.id = 'mermaid-live-preview';
|
||||
previewPanel.className = 'mermaid-live-preview';
|
||||
previewPanel.style.display = 'none';
|
||||
bodyEl.appendChild(previewPanel);
|
||||
|
||||
// Debounced live preview on content change
|
||||
var mermaidTimer = null;
|
||||
var updateMermaidPreview = function() {
|
||||
clearTimeout(mermaidTimer);
|
||||
mermaidTimer = setTimeout(function() {
|
||||
var content = state.editorView ? state.editorView.state.doc.toString() : (state.fallbackEditorEl ? state.fallbackEditorEl.value : '');
|
||||
var match = content.match(/```mermaid
|
||||
([\s\S]*?)```/);
|
||||
if (match) {
|
||||
previewPanel.style.display = 'block';
|
||||
previewPanel.innerHTML = '';
|
||||
var code = match[1];
|
||||
import('./mermaid-viewer.js').then(function(m) {
|
||||
// Create a temp container and render
|
||||
var tmp = document.createElement('div');
|
||||
tmp.innerHTML = '<pre><code class="language-mermaid">' + code.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + '</code></pre>';
|
||||
previewPanel.appendChild(tmp);
|
||||
m.renderMermaidBlocks(previewPanel);
|
||||
});
|
||||
} else {
|
||||
previewPanel.style.display = 'none';
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
if (state.editorView) {
|
||||
// Attach to CodeMirror
|
||||
var extUpdate = state.editorView.state.updateListener;
|
||||
// Use the existing update listener approach - just add debounce on any change
|
||||
}
|
||||
|
||||
modal.classList.add("active");
|
||||
safeCreateIcons();
|
||||
|
||||
// Initial preview if content has mermaid
|
||||
updateMermaidPreview();
|
||||
}
|
||||
|
||||
async function waitForCodeMirror() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TabManager, closeMobileSidebar, ContextMenuManager, RightSidebarManager
|
||||
import { syncActiveFileTreeItem, searchByTag, TagFilterService, refreshSidebarTreePreservingState, focusPathInSidebar } from './sidebar.js';
|
||||
import { AutocompleteDropdown, performAdvancedSearch } from './search.js';
|
||||
import { initDashboardTabs } from './sync.js';
|
||||
import { renderMermaidBlocks } from './mermaid-viewer.js';
|
||||
import { DashboardStatsWidget, DashboardRecentWidget, DashboardBookmarkWidget, DashboardSharedWidget, DashboardConflictsWidget } from './dashboard.js';
|
||||
import { openShareDialog } from './config.js';
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -598,6 +599,9 @@ export function renderFile(data) {
|
||||
safeHighlight(block);
|
||||
});
|
||||
|
||||
// Render Mermaid diagrams
|
||||
renderMermaidBlocks(area);
|
||||
|
||||
// Wire up wikilinks
|
||||
area.querySelectorAll(".wikilink").forEach((link) => {
|
||||
link.addEventListener("click", (e) => {
|
||||
|
||||
@@ -7616,6 +7616,97 @@ body.popup-mode .content-area {
|
||||
}
|
||||
.backup-manager-error { color: var(--danger); }
|
||||
|
||||
/* ==========================================================================
|
||||
Mermaid.js Diagrams
|
||||
========================================================================== */
|
||||
|
||||
.mermaid-container {
|
||||
position: relative;
|
||||
margin: 12px 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.mermaid-rendered {
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
min-height: 80px;
|
||||
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-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); }
|
||||
|
||||
.mermaid-error {
|
||||
margin: 12px 0; padding: 12px 16px;
|
||||
border: 1px solid rgba(239,68,68,0.3); border-radius: 6px;
|
||||
background: rgba(239,68,68,0.08); color: var(--danger);
|
||||
font-size: 0.8rem; line-height: 1.5;
|
||||
}
|
||||
.mermaid-error strong { display: block; margin-bottom: 4px; }
|
||||
|
||||
.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;
|
||||
}
|
||||
.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-close:hover { background: rgba(255,255,255,0.1); }
|
||||
.mermaid-fullscreen-content { max-width: 95vw; max-height: 95vh; }
|
||||
|
||||
.mermaid-focus-panel {
|
||||
position: fixed; top: 0; right: 0; bottom: 0; width: 480px;
|
||||
z-index: 9998; background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border);
|
||||
box-shadow: -4px 0 24px rgba(0,0,0,0.3);
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.mermaid-focus-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 14px; border-bottom: 1px solid var(--border);
|
||||
font-size: 0.85rem; font-weight: 600;
|
||||
}
|
||||
.mermaid-focus-close {
|
||||
width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 4px;
|
||||
background: transparent; color: var(--text-muted); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.mermaid-focus-close:hover { color: var(--accent); }
|
||||
.mermaid-focus-content { flex: 1; overflow: auto; padding: 16px; }
|
||||
|
||||
/* Mermaid editor live preview panel */
|
||||
.mermaid-live-preview {
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
min-height: 100px; max-height: 300px;
|
||||
overflow: auto; padding: 12px;
|
||||
}
|
||||
.mermaid-live-preview .mermaid-error { margin: 0; }
|
||||
|
||||
/* Manager body: list + preview side panel */
|
||||
.backup-manager-body { display: flex; gap: 0; min-height: 400px; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user