Created
October 14, 2025 17:06
-
-
Save kishorek/1ccd543d94d201bb10e1cb8a17f1d675 to your computer and use it in GitHub Desktop.
View Mermaid files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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