Skip to content

Instantly share code, notes, and snippets.

@kishorek
Created October 14, 2025 17:06
Show Gist options
  • Select an option

  • Save kishorek/1ccd543d94d201bb10e1cb8a17f1d675 to your computer and use it in GitHub Desktop.

Select an option

Save kishorek/1ccd543d94d201bb10e1cb8a17f1d675 to your computer and use it in GitHub Desktop.
View Mermaid files
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mermaid Diagram Studio</title>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: false, theme: 'default' });
window.mermaid = mermaid;
</script>
<style>
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #818cf8;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--border: #334155;
--shadow: rgba(0, 0, 0, 0.5);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header/Toolbar */
.toolbar {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px var(--shadow);
z-index: 10;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 24px;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.toolbar-actions {
display: flex;
gap: 8px;
align-items: center;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: var(--border);
}
/* Buttons */
button {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
font-family: inherit;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(0);
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-success {
background: var(--success);
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.btn-secondary:hover {
background: #475569;
}
.btn-icon {
padding: 10px;
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--bg-tertiary);
transform: none;
box-shadow: none;
}
.icon {
width: 18px;
height: 18px;
fill: currentColor;
}
/* Main Layout */
.main-layout {
display: grid;
grid-template-columns: 420px 1fr;
flex: 1;
overflow: hidden;
position: relative;
}
.main-layout.sidebar-collapsed {
grid-template-columns: 0px 1fr;
}
.sidebar {
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s ease;
}
.main-layout.sidebar-collapsed .sidebar {
margin-left: -420px;
}
.sidebar-section {
padding: 20px;
border-bottom: 1px solid var(--border);
transition: all 0.3s ease;
}
.sidebar-section.collapsed .section-content {
display: none;
}
.sidebar-section.collapsed {
padding-bottom: 20px;
}
.sidebar-section:last-child {
flex: 1;
overflow-y: auto;
border-bottom: none;
display: flex;
flex-direction: column;
}
.sidebar-section:last-child.collapsed {
flex: 0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
cursor: pointer;
user-select: none;
gap: 8px;
}
.section-header:hover .section-title {
color: var(--text-primary);
}
.section-header:active {
opacity: 0.7;
}
.section-title {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
transition: color 0.2s;
}
.collapse-icon {
width: 16px;
height: 16px;
fill: var(--text-muted);
transition: transform 0.3s ease, fill 0.2s;
flex-shrink: 0;
margin-left: 8px;
}
.section-header:hover .collapse-icon {
fill: var(--text-primary);
}
.sidebar-section.collapsed .collapse-icon {
transform: rotate(-90deg);
}
.section-content {
transition: all 0.3s ease;
}
.diagram-count {
background: var(--bg-primary);
color: var(--primary);
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
}
/* Input Fields */
input[type="text"] {
width: 100%;
padding: 12px 16px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
transition: all 0.2s;
}
input[type="text"]:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
input[type="text"]::placeholder {
color: var(--text-muted);
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
textarea {
width: 100%;
min-height: 300px;
padding: 16px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
resize: vertical;
transition: all 0.2s;
}
textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
textarea::placeholder {
color: var(--text-muted);
}
/* Saved Diagrams */
.saved-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.saved-list .empty-state {
background: transparent;
box-shadow: none;
padding: 40px 20px;
}
.saved-list .empty-state-title {
font-size: 14px;
}
.saved-list .empty-state-text {
font-size: 12px;
}
#diagram-search {
padding-left: 40px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2394a3b8'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 12px center;
background-size: 18px;
}
select {
padding: 12px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
}
select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
select:hover {
border-color: var(--primary);
}
select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
.search-sort-container {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.search-sort-container input {
flex: 1;
margin: 0;
}
.search-sort-container select {
width: auto;
min-width: 100px;
}
.saved-list::-webkit-scrollbar {
width: 6px;
}
.saved-list::-webkit-scrollbar-track {
background: transparent;
}
.saved-list::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 3px;
}
.saved-item {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
transition: all 0.2s;
cursor: pointer;
}
.saved-item:hover {
border-color: var(--primary);
background: var(--bg-tertiary);
}
.saved-item-info {
flex: 1;
min-width: 0;
}
.saved-item-name {
font-weight: 600;
font-size: 14px;
color: var(--text-primary);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.saved-item-date {
font-size: 11px;
color: var(--text-muted);
}
.saved-item-actions {
display: flex;
gap: 4px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
/* Preview Area */
.preview-area {
display: flex;
flex-direction: column;
background: var(--bg-primary);
position: relative;
}
.sidebar-toggle {
position: absolute;
top: 12px;
left: 12px;
z-index: 100;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.sidebar-toggle:hover {
background: var(--bg-tertiary);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.sidebar-toggle .icon {
fill: var(--text-secondary);
}
.sidebar-toggle:hover .icon {
fill: var(--text-primary);
}
.preview-controls {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 12px;
}
.zoom-level {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
min-width: 60px;
text-align: center;
padding: 6px 12px;
background: var(--bg-primary);
border-radius: 6px;
}
.preview-canvas {
flex: 1;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
position: relative;
cursor: default;
}
.preview-canvas.can-pan {
cursor: grab;
}
.preview-canvas.dragging {
cursor: grabbing;
user-select: none;
}
.preview-canvas.zoomed {
overflow: hidden;
}
.preview-canvas::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.preview-canvas::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
.preview-canvas::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 5px;
}
.preview-canvas::-webkit-scrollbar-thumb:hover {
background: #475569;
}
#diagram-render {
display: inline-block;
transition: transform 0.3s ease;
}
#diagram-render > div {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: inline-block;
}
.empty-state {
text-align: center;
padding: 60px 40px;
color: var(--text-muted);
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.empty-state-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
opacity: 0.5;
}
.empty-state-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-secondary);
}
.empty-state-text {
font-size: 14px;
}
/* Messages */
.message-container {
position: fixed;
top: 80px;
right: 24px;
z-index: 1000;
max-width: 400px;
}
.message {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex;
align-items: start;
gap: 12px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.message.success {
border-left: 3px solid var(--success);
}
.message.error {
border-left: 3px solid var(--danger);
}
.message-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
margin-top: 2px;
}
.message.success .message-icon {
color: var(--success);
}
.message.error .message-icon {
color: var(--danger);
}
.message-content {
flex: 1;
}
.message-title {
font-weight: 600;
margin-bottom: 4px;
font-size: 14px;
}
.message-text {
font-size: 13px;
color: var(--text-secondary);
}
/* Fullscreen */
.fullscreen-mode {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background: var(--bg-primary);
display: flex;
flex-direction: column;
}
/* Dropdown */
.dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
min-width: 200px;
padding: 8px;
display: none;
z-index: 100;
}
.dropdown-menu.active {
display: block;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-item {
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: var(--text-secondary);
transition: all 0.15s;
}
.dropdown-item:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
/* Responsive */
@media (max-width: 1024px) {
.main-layout {
grid-template-columns: 360px 1fr;
}
.main-layout.sidebar-collapsed .sidebar {
margin-left: -360px;
}
}
@media (max-width: 768px) {
.main-layout {
grid-template-columns: 320px 1fr;
}
.sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 320px;
z-index: 200;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3);
}
.main-layout.sidebar-collapsed {
grid-template-columns: 1fr;
}
.main-layout.sidebar-collapsed .sidebar {
margin-left: -320px;
}
.toolbar-left .logo span {
display: none;
}
.sidebar-toggle {
display: flex;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Toolbar -->
<div class="toolbar">
<div class="toolbar-left">
<div class="logo">
<div class="logo-icon">📊</div>
<span>Mermaid Studio</span>
</div>
</div>
<div class="toolbar-actions">
<button class="btn-primary" onclick="renderDiagram()" title="Render (Ctrl/Cmd+Enter)">
<svg class="icon" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
Render
</button>
<button class="btn-success" onclick="saveDiagram()" title="Save (Ctrl/Cmd+S)">
<svg class="icon" viewBox="0 0 24 24">
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
</svg>
Save
</button>
<div class="toolbar-divider"></div>
<div class="dropdown">
<button class="btn-ghost btn-icon" onclick="toggleDropdown()" title="More options">
<svg class="icon" viewBox="0 0 24 24">
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
</svg>
</button>
<div class="dropdown-menu" id="more-menu">
<div class="dropdown-item" onclick="exportSVG()">
<svg class="icon" viewBox="0 0 24 24">
<path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/>
</svg>
Export SVG
</div>
<div class="dropdown-item" onclick="clearEditor()">
<svg class="icon" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
Clear Editor
</div>
</div>
</div>
</div>
</div>
<!-- Main Layout -->
<div class="main-layout">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-section" id="editor-section">
<div class="section-header" onclick="toggleSection('editor-section')">
<div class="section-title">Editor</div>
<svg class="collapse-icon" viewBox="0 0 24 24">
<path d="M7 10l5 5 5-5z"/>
</svg>
</div>
<div class="section-content">
<div class="input-group">
<input type="text" id="diagram-name" placeholder="Diagram name...">
</div>
<textarea id="diagram-code" placeholder="Enter Mermaid diagram code...
Example:
graph TB
A[Start] --> B{Decision}
B -->|Yes| C[Success]
B -->|No| D[Try Again]
D --> A">graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug]
D --> A</textarea>
</div>
</div>
<div class="sidebar-section" id="saved-section">
<div class="section-header" onclick="toggleSection('saved-section')">
<div class="section-title">
<span>Saved Diagrams</span>
<span class="diagram-count" id="diagram-count">0</span>
</div>
<svg class="collapse-icon" viewBox="0 0 24 24">
<path d="M7 10l5 5 5-5z"/>
</svg>
</div>
<div class="section-content" style="display: flex; flex-direction: column; flex: 1; overflow: hidden;">
<div class="search-sort-container">
<input type="text" id="diagram-search" placeholder="Search diagrams...">
<select id="diagram-sort">
<option value="date-desc">Newest</option>
<option value="date-asc">Oldest</option>
<option value="name-asc">A → Z</option>
<option value="name-desc">Z → A</option>
</select>
</div>
<div class="saved-list" id="saved-diagrams" style="flex: 1; overflow-y: auto;">
<div class="empty-state">
<div class="empty-state-title">No saved diagrams</div>
<div class="empty-state-text">Create and save your first diagram</div>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Area -->
<div class="preview-area" id="preview-area">
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Toggle Sidebar (Ctrl+B)">
<svg class="icon" viewBox="0 0 24 24">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</button>
<div class="preview-controls">
<div class="zoom-controls">
<button class="btn-ghost btn-icon" onclick="zoomOut()" title="Zoom Out (- or Ctrl+Scroll) - Min: 10%">
<svg class="icon" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"/>
</svg>
</button>
<div class="zoom-level" id="zoom-level" title="Zoom: 10% - 1000% | Ctrl/Cmd + Scroll to zoom | Drag to pan when zoomed">100%</div>
<button class="btn-ghost btn-icon" onclick="zoomIn()" title="Zoom In (+ or Ctrl+Scroll) - Max: 1000%">
<svg class="icon" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zm.5-7h-1v2H7v1h2v2h1v-2h2V9h-2z"/>
</svg>
</button>
<button class="btn-ghost btn-icon" onclick="autoFitDiagram(0.8)" title="Fit to Screen (Ctrl/Cmd+F) - Auto-fit to 80%">
<svg class="icon" viewBox="0 0 24 24">
<path d="M3 5v4h2V5h4V3H5c-1.1 0-2 .9-2 2zm2 10H3v4c0 1.1.9 2 2 2h4v-2H5v-4zm14 4h-4v2h4c1.1 0 2-.9 2-2v-4h-2v4zm0-16h-4v2h4v4h2V5c0-1.1-.9-2-2-2z"/>
</svg>
</button>
<button class="btn-ghost btn-icon" onclick="resetZoom()" title="Reset to 100% Zoom (0)">
<svg class="icon" viewBox="0 0 24 24">
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
</svg>
</button>
</div>
<button class="btn-ghost btn-icon" onclick="toggleFullscreen()" id="fullscreen-btn" title="Fullscreen (F11)">
<svg class="icon" id="fullscreen-icon" viewBox="0 0 24 24">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>
</button>
</div>
<div class="preview-canvas">
<div id="diagram-render">
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/>
</svg>
<div class="empty-state-title">Ready to visualize</div>
<div class="empty-state-text">Click "Render" or press Ctrl/Cmd+Enter to preview your diagram</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Message Container -->
<div class="message-container" id="message-container"></div>
<script>
let currentZoom = 1;
let isFullscreen = false;
let isDragging = false;
let dragStart = { x: 0, y: 0 };
let panOffset = { x: 0, y: 0 };
document.addEventListener('DOMContentLoaded', function() {
loadSavedDiagrams();
setTimeout(() => {
renderDiagram();
}, 100);
initializePanAndZoom();
initializeSearch();
loadCollapsedStates();
});
function showMessage(text, type = 'success') {
const container = document.getElementById('message-container');
const titles = { success: 'Success', error: 'Error' };
const icons = {
success: '<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>',
error: '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>'
};
const message = document.createElement('div');
message.className = `message ${type}`;
message.innerHTML = `
<svg class="message-icon" viewBox="0 0 24 24" fill="currentColor">${icons[type]}</svg>
<div class="message-content">
<div class="message-title">${titles[type]}</div>
<div class="message-text">${text}</div>
</div>
`;
container.innerHTML = '';
container.appendChild(message);
setTimeout(() => {
message.style.animation = 'slideIn 0.3s ease reverse';
setTimeout(() => container.innerHTML = '', 300);
}, 3000);
}
function saveDiagram() {
const name = document.getElementById('diagram-name').value.trim();
const code = document.getElementById('diagram-code').value.trim();
if (!name) {
showMessage('Please enter a diagram name', 'error');
return;
}
if (!code) {
showMessage('Please enter diagram code', 'error');
return;
}
const diagrams = JSON.parse(localStorage.getItem('mermaidDiagrams') || '{}');
diagrams[name] = {
code: code,
timestamp: new Date().toISOString()
};
localStorage.setItem('mermaidDiagrams', JSON.stringify(diagrams));
showMessage(`Diagram "${name}" saved successfully!`, 'success');
loadSavedDiagrams();
}
function loadDiagram(name) {
const diagrams = JSON.parse(localStorage.getItem('mermaidDiagrams') || '{}');
if (diagrams[name]) {
document.getElementById('diagram-name').value = name;
document.getElementById('diagram-code').value = diagrams[name].code;
renderDiagram();
showMessage(`Loaded "${name}"`, 'success');
}
}
function deleteDiagram(name, event) {
event.stopPropagation();
if (!confirm(`Delete "${name}"?`)) {
return;
}
const diagrams = JSON.parse(localStorage.getItem('mermaidDiagrams') || '{}');
delete diagrams[name];
localStorage.setItem('mermaidDiagrams', JSON.stringify(diagrams));
showMessage(`Deleted "${name}"`, 'success');
loadSavedDiagrams();
if (document.getElementById('diagram-name').value === name) {
clearEditor();
}
}
function loadSavedDiagrams(searchQuery = '', sortBy = null) {
const diagrams = JSON.parse(localStorage.getItem('mermaidDiagrams') || '{}');
const container = document.getElementById('saved-diagrams');
const countElement = document.getElementById('diagram-count');
if (!sortBy) {
const sortSelect = document.getElementById('diagram-sort');
sortBy = sortSelect ? sortSelect.value : 'date-desc';
}
const totalCount = Object.keys(diagrams).length;
countElement.textContent = totalCount;
if (totalCount === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-title">No saved diagrams</div>
<div class="empty-state-text">Create and save your first diagram</div>
</div>
`;
return;
}
// Get all diagram names
let sortedNames = Object.keys(diagrams);
// Filter by search query
if (searchQuery) {
sortedNames = sortedNames.filter(name =>
name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Sort based on selection
sortedNames.sort((a, b) => {
switch(sortBy) {
case 'date-desc':
return new Date(diagrams[b].timestamp) - new Date(diagrams[a].timestamp);
case 'date-asc':
return new Date(diagrams[a].timestamp) - new Date(diagrams[b].timestamp);
case 'name-asc':
return a.toLowerCase().localeCompare(b.toLowerCase());
case 'name-desc':
return b.toLowerCase().localeCompare(a.toLowerCase());
default:
return new Date(diagrams[b].timestamp) - new Date(diagrams[a].timestamp);
}
});
if (sortedNames.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-title">No results found</div>
<div class="empty-state-text">Try a different search term</div>
</div>
`;
return;
}
container.innerHTML = sortedNames.map(name => {
const date = new Date(diagrams[name].timestamp);
const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
// Escape quotes in name for HTML attributes
const escapedName = name.replace(/'/g, "\\'");
return `
<div class="saved-item" onclick="loadDiagram('${escapedName}')" data-name="${name.toLowerCase()}">
<div class="saved-item-info">
<div class="saved-item-name">${name}</div>
<div class="saved-item-date">${dateStr}</div>
</div>
<div class="saved-item-actions">
<button class="btn-danger btn-small" onclick="deleteDiagram('${escapedName}', event)" title="Delete">
<svg class="icon" viewBox="0 0 24 24" style="width:14px;height:14px">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
</button>
</div>
</div>
`;
}).join('');
}
function initializeSearch() {
const searchInput = document.getElementById('diagram-search');
const sortSelect = document.getElementById('diagram-sort');
searchInput.addEventListener('input', function(e) {
const query = e.target.value;
loadSavedDiagrams(query);
});
// Clear search with Escape key
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
searchInput.value = '';
loadSavedDiagrams();
}
});
// Handle sort changes
sortSelect.addEventListener('change', function(e) {
const searchQuery = searchInput.value;
loadSavedDiagrams(searchQuery, e.target.value);
});
}
async function renderDiagram() {
const code = document.getElementById('diagram-code').value.trim();
const renderDiv = document.getElementById('diagram-render');
if (!code) {
renderDiv.style.transform = '';
renderDiv.innerHTML = `
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/>
</svg>
<div class="empty-state-title">Ready to visualize</div>
<div class="empty-state-text">Enter diagram code and click "Render"</div>
</div>
`;
return;
}
try {
const id = 'mermaid-' + Date.now();
renderDiv.innerHTML = '';
renderDiv.style.transform = '';
panOffset = { x: 0, y: 0 };
const wrapper = document.createElement('div');
const mermaidContainer = document.createElement('pre');
mermaidContainer.className = 'mermaid';
mermaidContainer.id = id;
mermaidContainer.textContent = code;
wrapper.appendChild(mermaidContainer);
renderDiv.appendChild(wrapper);
await new Promise(resolve => requestAnimationFrame(resolve));
const element = document.getElementById(id);
if (element) {
const { svg } = await window.mermaid.render(id + '-svg', code);
element.innerHTML = svg;
}
// Auto-fit to 80% of viewport
await new Promise(resolve => requestAnimationFrame(resolve));
autoFitDiagram(0.8);
} catch (error) {
console.error('Mermaid render error:', error);
renderDiv.style.transform = '';
renderDiv.innerHTML = `
<div class="empty-state">
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="currentColor" style="color: var(--danger);">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
<div class="empty-state-title">Render Error</div>
<div class="empty-state-text">${error.message || error}</div>
</div>
`;
}
}
function clearEditor() {
document.getElementById('diagram-name').value = '';
document.getElementById('diagram-code').value = '';
document.getElementById('diagram-render').style.transform = '';
panOffset = { x: 0, y: 0 };
renderDiagram();
closeDropdown();
}
function zoomIn() {
currentZoom = Math.min(currentZoom + 0.1, 10);
applyZoom();
}
function zoomOut() {
currentZoom = Math.max(currentZoom - 0.1, 0.1);
applyZoom();
}
function resetZoom() {
currentZoom = 1;
panOffset = { x: 0, y: 0 };
const renderDiv = document.getElementById('diagram-render');
if (renderDiv) {
renderDiv.style.transform = '';
}
applyZoom();
}
function autoFitDiagram(targetFill = 0.8) {
const canvas = document.querySelector('.preview-canvas');
const renderDiv = document.getElementById('diagram-render');
const svg = renderDiv.querySelector('svg');
if (!svg || !canvas) {
resetZoom();
return;
}
// Get the SVG dimensions
const svgRect = svg.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
if (svgRect.width === 0 || svgRect.height === 0) {
resetZoom();
return;
}
// Calculate available space (accounting for padding)
const padding = 80; // 40px padding on each side
const availableWidth = canvasRect.width - padding;
const availableHeight = canvasRect.height - padding;
// Calculate zoom to fit 80% of the viewport
const targetWidth = availableWidth * targetFill;
const targetHeight = availableHeight * targetFill;
const scaleX = targetWidth / svgRect.width;
const scaleY = targetHeight / svgRect.height;
// Use the smaller scale to ensure it fits
const optimalZoom = Math.min(scaleX, scaleY);
// Clamp between min and max zoom
currentZoom = Math.max(0.1, Math.min(10, optimalZoom));
panOffset = { x: 0, y: 0 };
applyZoom();
}
function applyZoom() {
const renderDiv = document.getElementById('diagram-render');
const canvas = document.querySelector('.preview-canvas');
if (renderDiv && !renderDiv.querySelector('.empty-state')) {
if (currentZoom === 1 && panOffset.x === 0 && panOffset.y === 0) {
renderDiv.style.transform = '';
} else {
const scale = `scale(${currentZoom})`;
const translate = `translate(${panOffset.x}px, ${panOffset.y}px)`;
renderDiv.style.transform = `${translate} ${scale}`;
renderDiv.style.transformOrigin = 'center center';
}
}
// Update cursor based on zoom level
if (currentZoom > 1) {
canvas.classList.add('can-pan');
} else {
canvas.classList.remove('can-pan');
}
document.getElementById('zoom-level').textContent = Math.round(currentZoom * 100) + '%';
}
function initializePanAndZoom() {
const canvas = document.querySelector('.preview-canvas');
// Mouse wheel zoom (with Ctrl/Cmd) and trackpad pinch zoom
canvas.addEventListener('wheel', function(e) {
// Check if Ctrl/Cmd key is pressed (or if it's a pinch gesture on trackpad)
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
const delta = -e.deltaY;
const zoomIntensity = 0.002;
const newZoom = currentZoom + delta * zoomIntensity;
currentZoom = Math.max(0.1, Math.min(10, newZoom));
applyZoom();
}
}, { passive: false });
// Drag to pan
canvas.addEventListener('mousedown', function(e) {
if (currentZoom > 1) {
isDragging = true;
dragStart = { x: e.clientX - panOffset.x, y: e.clientY - panOffset.y };
canvas.classList.add('dragging');
e.preventDefault();
}
});
document.addEventListener('mousemove', function(e) {
if (isDragging) {
panOffset = {
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
};
applyZoom();
}
});
document.addEventListener('mouseup', function() {
if (isDragging) {
isDragging = false;
canvas.classList.remove('dragging');
}
});
// Touch support for mobile
let touchStart = null;
let touchDistance = 0;
canvas.addEventListener('touchstart', function(e) {
if (e.touches.length === 2) {
// Pinch zoom
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
touchDistance = Math.sqrt(dx * dx + dy * dy);
} else if (e.touches.length === 1 && currentZoom > 1) {
// Pan
touchStart = {
x: e.touches[0].clientX - panOffset.x,
y: e.touches[0].clientY - panOffset.y
};
}
});
canvas.addEventListener('touchmove', function(e) {
if (e.touches.length === 2 && touchDistance > 0) {
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const newDistance = Math.sqrt(dx * dx + dy * dy);
const scale = newDistance / touchDistance;
currentZoom = Math.max(0.1, Math.min(10, currentZoom * scale));
touchDistance = newDistance;
applyZoom();
} else if (e.touches.length === 1 && touchStart) {
e.preventDefault();
panOffset = {
x: e.touches[0].clientX - touchStart.x,
y: e.touches[0].clientY - touchStart.y
};
applyZoom();
}
}, { passive: false });
canvas.addEventListener('touchend', function() {
touchStart = null;
touchDistance = 0;
});
}
function toggleFullscreen() {
const previewArea = document.getElementById('preview-area');
const fullscreenIcon = document.getElementById('fullscreen-icon');
isFullscreen = !isFullscreen;
if (isFullscreen) {
previewArea.classList.add('fullscreen-mode');
fullscreenIcon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
} else {
previewArea.classList.remove('fullscreen-mode');
fullscreenIcon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
}
}
function toggleDropdown() {
const menu = document.getElementById('more-menu');
menu.classList.toggle('active');
}
function closeDropdown() {
document.getElementById('more-menu').classList.remove('active');
}
function exportSVG() {
const renderDiv = document.getElementById('diagram-render');
const svg = renderDiv.querySelector('svg');
if (!svg) {
showMessage('Please render a diagram first', 'error');
return;
}
const svgData = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = (document.getElementById('diagram-name').value || 'diagram') + '.svg';
link.click();
URL.revokeObjectURL(url);
showMessage('SVG exported successfully', 'success');
closeDropdown();
}
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.dropdown')) {
closeDropdown();
}
});
// Collapse/Expand functions
function toggleSection(sectionId) {
const section = document.getElementById(sectionId);
section.classList.toggle('collapsed');
// Save state to localStorage
const collapsed = section.classList.contains('collapsed');
localStorage.setItem(`section-${sectionId}-collapsed`, collapsed);
}
function toggleSidebar() {
const mainLayout = document.querySelector('.main-layout');
mainLayout.classList.toggle('sidebar-collapsed');
// Save state to localStorage
const collapsed = mainLayout.classList.contains('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', collapsed);
}
function loadCollapsedStates() {
const mainLayout = document.querySelector('.main-layout');
// Check if sidebar state is saved
const sidebarCollapsedSaved = localStorage.getItem('sidebar-collapsed');
if (sidebarCollapsedSaved !== null) {
// Use saved state
if (sidebarCollapsedSaved === 'true') {
mainLayout.classList.add('sidebar-collapsed');
}
} else {
// Default: collapse on mobile
if (window.innerWidth <= 768) {
mainLayout.classList.add('sidebar-collapsed');
}
}
// Load section states
const sections = ['editor-section', 'saved-section'];
sections.forEach(sectionId => {
const collapsed = localStorage.getItem(`section-${sectionId}-collapsed`) === 'true';
if (collapsed) {
document.getElementById(sectionId).classList.add('collapsed');
}
});
}
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
renderDiagram();
}
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveDiagram();
}
// Ctrl/Cmd + B to toggle sidebar
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
e.preventDefault();
toggleSidebar();
}
// Ctrl/Cmd + F to fit diagram to screen
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
autoFitDiagram(0.8);
}
if (e.key === 'F11' && !e.target.matches('input, textarea')) {
e.preventDefault();
toggleFullscreen();
}
if ((e.key === '+' || e.key === '=') && !e.target.matches('input, textarea')) {
e.preventDefault();
zoomIn();
}
if (e.key === '-' && !e.target.matches('input, textarea')) {
e.preventDefault();
zoomOut();
}
if (e.key === '0' && !e.target.matches('input, textarea')) {
e.preventDefault();
resetZoom();
}
if (e.key === 'Escape') {
closeDropdown();
if (isFullscreen) {
toggleFullscreen();
}
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment