631 lines
20 KiB
HTML
631 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Mermaid Diagram Viewer</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 20px;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
padding: 30px;
|
||
text-align: center;
|
||
color: white;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header p {
|
||
font-size: 1.1em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.content {
|
||
display: grid;
|
||
grid-template-columns: 400px 1fr;
|
||
gap: 30px;
|
||
padding: 30px;
|
||
}
|
||
|
||
.panel {
|
||
background: #f8f9fa;
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.panel h2 {
|
||
color: #667eea;
|
||
margin-bottom: 20px;
|
||
font-size: 1.5em;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.tab-btn {
|
||
flex: 1;
|
||
padding: 12px 20px;
|
||
border: none;
|
||
background: white;
|
||
color: #667eea;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.tab-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.tab-btn.active {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 3px dashed #667eea;
|
||
border-radius: 15px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
background: white;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #764ba2;
|
||
background: #f0f4ff;
|
||
}
|
||
|
||
.upload-area.dragover {
|
||
border-color: #764ba2;
|
||
background: #e8ecff;
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 3em;
|
||
margin-bottom: 15px;
|
||
color: #667eea;
|
||
}
|
||
|
||
.upload-area p {
|
||
color: #666;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
#fileInput {
|
||
display: none;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
min-height: 300px;
|
||
padding: 15px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 10px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.95em;
|
||
resize: vertical;
|
||
transition: border-color 0.3s ease;
|
||
background: white;
|
||
}
|
||
|
||
textarea:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
padding: 15px;
|
||
margin-top: 15px;
|
||
border: none;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 10px;
|
||
font-size: 1.1em;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||
}
|
||
|
||
.btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
#diagramOutput {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
min-height: 400px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
position: relative;
|
||
cursor: grab;
|
||
}
|
||
|
||
#diagramOutput.grabbing {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
#diagramOutput svg {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
#diagramWrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.zoom-controls {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
z-index: 100;
|
||
}
|
||
|
||
.zoom-btn {
|
||
width: 45px;
|
||
height: 45px;
|
||
border: none;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 10px;
|
||
font-size: 1.5em;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.zoom-btn:hover {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||
}
|
||
|
||
.zoom-btn:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.zoom-level {
|
||
background: rgba(102, 126, 234, 0.9);
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
font-size: 0.9em;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.placeholder {
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 1.2em;
|
||
}
|
||
|
||
.placeholder-icon {
|
||
font-size: 4em;
|
||
margin-bottom: 15px;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.error {
|
||
background: #fee;
|
||
border: 2px solid #fcc;
|
||
color: #c33;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.success {
|
||
background: #efe;
|
||
border: 2px solid #cfc;
|
||
color: #3c3;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
@media (max-width: 968px) {
|
||
.content {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🎨 Mermaid Diagram Viewer</h1>
|
||
<p>Upload a file or paste your Mermaid code to visualize beautiful diagrams</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="panel">
|
||
<h2>📝 Input</h2>
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" onclick="switchTab('paste')">Paste Code</button>
|
||
<button class="tab-btn" onclick="switchTab('upload')">Upload File</button>
|
||
</div>
|
||
|
||
<div id="pasteTab" class="tab-content active">
|
||
<textarea id="mermaidCode" placeholder="Paste your Mermaid diagram code here...
|
||
|
||
Example:
|
||
graph TD
|
||
A[Start] --> B{Decision}
|
||
B -->|Yes| C[Option 1]
|
||
B -->|No| D[Option 2]
|
||
C --> E[End]
|
||
D --> E"></textarea>
|
||
<button class="btn" onclick="renderFromTextarea()">🎨 Render Diagram</button>
|
||
</div>
|
||
|
||
<div id="uploadTab" class="tab-content">
|
||
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
|
||
<div class="upload-icon">📁</div>
|
||
<p><strong>Click to upload</strong> or drag and drop</p>
|
||
<p style="font-size: 0.9em; margin-top: 10px; opacity: 0.7;">Supports .mmd, .mermaid, .txt files</p>
|
||
</div>
|
||
<input type="file" id="fileInput" accept=".mmd,.mermaid,.txt" onchange="handleFile(event)">
|
||
</div>
|
||
|
||
<div id="message"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>🖼️ Preview</h2>
|
||
<div id="diagramOutput">
|
||
<div class="placeholder">
|
||
<div class="placeholder-icon">📊</div>
|
||
<p>Your diagram will appear here</p>
|
||
</div>
|
||
<div class="zoom-controls" id="zoomControls" style="display: none;">
|
||
<button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button>
|
||
<div class="zoom-level" id="zoomLevel">100%</div>
|
||
<button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">−</button>
|
||
<button class="zoom-btn" onclick="resetZoom()" title="Reset Zoom" style="font-size: 1.2em;">⟲</button>
|
||
<button class="zoom-btn" onclick="fitToScreen()" title="Fit to Screen" style="font-size: 1.2em;">⛶</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
mermaid.initialize({
|
||
startOnLoad: false,
|
||
theme: 'default',
|
||
securityLevel: 'loose'
|
||
});
|
||
|
||
let currentTab = 'paste';
|
||
let diagramCounter = 0;
|
||
let zoomScale = 1;
|
||
let panX = 0;
|
||
let panY = 0;
|
||
let isDragging = false;
|
||
let startX = 0;
|
||
let startY = 0;
|
||
let panAndZoomSetup = false;
|
||
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||
|
||
if (tab === 'paste') {
|
||
document.querySelector('.tab-btn:first-child').classList.add('active');
|
||
document.getElementById('pasteTab').classList.add('active');
|
||
} else {
|
||
document.querySelector('.tab-btn:last-child').classList.add('active');
|
||
document.getElementById('uploadTab').classList.add('active');
|
||
}
|
||
}
|
||
|
||
function showMessage(message, type) {
|
||
const messageDiv = document.getElementById('message');
|
||
messageDiv.innerHTML = `<div class="${type}">${message}</div>`;
|
||
setTimeout(() => {
|
||
messageDiv.innerHTML = '';
|
||
}, 5000);
|
||
}
|
||
|
||
function renderDiagram(code) {
|
||
const output = document.getElementById('diagramOutput');
|
||
|
||
if (!code.trim()) {
|
||
showMessage('Please provide Mermaid code', 'error');
|
||
return;
|
||
}
|
||
|
||
diagramCounter++;
|
||
const graphId = 'mermaid-graph-' + diagramCounter;
|
||
|
||
// Create a wrapper for the diagram
|
||
const diagramWrapper = document.createElement('div');
|
||
diagramWrapper.id = 'diagramWrapper';
|
||
diagramWrapper.innerHTML = `<div id="${graphId}"></div>`;
|
||
|
||
// Clear output but keep zoom controls
|
||
const zoomControls = document.getElementById('zoomControls');
|
||
output.innerHTML = '';
|
||
output.appendChild(diagramWrapper);
|
||
output.appendChild(zoomControls);
|
||
|
||
try {
|
||
mermaid.render(graphId, code).then(result => {
|
||
diagramWrapper.innerHTML = result.svg;
|
||
|
||
// Wait a bit for DOM to update
|
||
setTimeout(() => {
|
||
resetZoom();
|
||
zoomControls.style.display = 'flex';
|
||
if (!panAndZoomSetup) {
|
||
setupPanAndZoom();
|
||
panAndZoomSetup = true;
|
||
}
|
||
showMessage('✅ Diagram rendered successfully!', 'success');
|
||
}, 100);
|
||
}).catch(error => {
|
||
diagramWrapper.innerHTML = '<div class="placeholder"><div class="placeholder-icon">❌</div><p>Failed to render diagram</p></div>';
|
||
zoomControls.style.display = 'none';
|
||
showMessage('❌ Error: ' + error.message, 'error');
|
||
});
|
||
} catch (error) {
|
||
diagramWrapper.innerHTML = '<div class="placeholder"><div class="placeholder-icon">❌</div><p>Failed to render diagram</p></div>';
|
||
zoomControls.style.display = 'none';
|
||
showMessage('❌ Error: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
function updateTransform() {
|
||
const output = document.getElementById('diagramOutput');
|
||
const svg = output.querySelector('svg');
|
||
if (svg) {
|
||
svg.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomScale})`;
|
||
document.getElementById('zoomLevel').textContent = Math.round(zoomScale * 100) + '%';
|
||
}
|
||
}
|
||
|
||
function zoomIn() {
|
||
zoomScale = Math.min(zoomScale + 0.2, 10);
|
||
updateTransform();
|
||
}
|
||
|
||
function zoomOut() {
|
||
zoomScale = Math.max(zoomScale - 0.2, 0.2);
|
||
updateTransform();
|
||
}
|
||
|
||
function resetZoom() {
|
||
zoomScale = 1;
|
||
panX = 0;
|
||
panY = 0;
|
||
updateTransform();
|
||
}
|
||
|
||
function fitToScreen() {
|
||
const output = document.getElementById('diagramOutput');
|
||
if (!output) return;
|
||
|
||
const svg = output.querySelector('svg');
|
||
if (!svg) return;
|
||
|
||
try {
|
||
const outputRect = output.getBoundingClientRect();
|
||
const svgRect = svg.getBBox();
|
||
|
||
if (svgRect.width === 0 || svgRect.height === 0) return;
|
||
|
||
const scaleX = (outputRect.width - 40) / svgRect.width;
|
||
const scaleY = (outputRect.height - 40) / svgRect.height;
|
||
|
||
zoomScale = Math.min(scaleX, scaleY, 1);
|
||
panX = 0;
|
||
panY = 0;
|
||
updateTransform();
|
||
} catch (error) {
|
||
console.warn('Could not fit to screen:', error);
|
||
}
|
||
}
|
||
|
||
function setupPanAndZoom() {
|
||
const output = document.getElementById('diagramOutput');
|
||
if (!output) return;
|
||
|
||
// Mouse wheel zoom
|
||
const handleWheel = (e) => {
|
||
e.preventDefault();
|
||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||
zoomScale = Math.max(0.2, Math.min(10, zoomScale + delta));
|
||
updateTransform();
|
||
};
|
||
output.addEventListener('wheel', handleWheel, { passive: false });
|
||
|
||
// Pan functionality
|
||
const handleMouseDown = (e) => {
|
||
if (e.target.closest('.zoom-controls')) return;
|
||
isDragging = true;
|
||
startX = e.clientX - panX;
|
||
startY = e.clientY - panY;
|
||
output.classList.add('grabbing');
|
||
};
|
||
output.addEventListener('mousedown', handleMouseDown);
|
||
|
||
const handleMouseMove = (e) => {
|
||
if (!isDragging) return;
|
||
panX = e.clientX - startX;
|
||
panY = e.clientY - startY;
|
||
updateTransform();
|
||
};
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
|
||
const handleMouseUp = () => {
|
||
isDragging = false;
|
||
output.classList.remove('grabbing');
|
||
};
|
||
document.addEventListener('mouseup', handleMouseUp);
|
||
|
||
// Touch support for mobile
|
||
let touchStartX = 0, touchStartY = 0;
|
||
let touchStartDistance = 0;
|
||
let initialScale = 1;
|
||
|
||
const handleTouchStart = (e) => {
|
||
if (e.touches.length === 1) {
|
||
touchStartX = e.touches[0].clientX - panX;
|
||
touchStartY = e.touches[0].clientY - panY;
|
||
} else if (e.touches.length === 2) {
|
||
touchStartDistance = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY
|
||
);
|
||
initialScale = zoomScale;
|
||
}
|
||
};
|
||
output.addEventListener('touchstart', handleTouchStart);
|
||
|
||
const handleTouchMove = (e) => {
|
||
e.preventDefault();
|
||
if (e.touches.length === 1) {
|
||
panX = e.touches[0].clientX - touchStartX;
|
||
panY = e.touches[0].clientY - touchStartY;
|
||
updateTransform();
|
||
} else if (e.touches.length === 2) {
|
||
const distance = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY
|
||
);
|
||
zoomScale = Math.max(0.2, Math.min(10, initialScale * (distance / touchStartDistance)));
|
||
updateTransform();
|
||
}
|
||
};
|
||
output.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||
}
|
||
|
||
function renderFromTextarea() {
|
||
const code = document.getElementById('mermaidCode').value;
|
||
renderDiagram(code);
|
||
}
|
||
|
||
function handleFile(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const content = e.target.result;
|
||
document.getElementById('mermaidCode').value = content;
|
||
renderDiagram(content);
|
||
showMessage('✅ File loaded: ' + file.name, 'success');
|
||
};
|
||
reader.onerror = function() {
|
||
showMessage('❌ Error reading file', 'error');
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
const uploadArea = document.getElementById('uploadArea');
|
||
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
|
||
const file = e.dataTransfer.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(event) {
|
||
const content = event.target.result;
|
||
document.getElementById('mermaidCode').value = content;
|
||
renderDiagram(content);
|
||
showMessage('✅ File loaded: ' + file.name, 'success');
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
});
|
||
|
||
document.getElementById('mermaidCode').addEventListener('keydown', function(e) {
|
||
if (e.ctrlKey && e.key === 'Enter') {
|
||
renderFromTextarea();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |