|
88 | 88 | transform: scale(1); |
89 | 89 | } |
90 | 90 | } |
| 91 | + |
| 92 | + /* Fullscreen modal */ |
| 93 | + :global(.mermaid-fullscreen-modal) { |
| 94 | + position: fixed; |
| 95 | + top: 0; |
| 96 | + left: 0; |
| 97 | + right: 0; |
| 98 | + bottom: 0; |
| 99 | + z-index: 9999; |
| 100 | + background: hsl(var(--background) / 0.95); |
| 101 | + backdrop-filter: blur(8px); |
| 102 | + display: flex; |
| 103 | + flex-direction: column; |
| 104 | + opacity: 0; |
| 105 | + transition: opacity 0.3s ease; |
| 106 | + overflow: hidden; |
| 107 | + } |
| 108 | + |
| 109 | + :global(.mermaid-fullscreen-modal.active) { |
| 110 | + opacity: 1; |
| 111 | + } |
| 112 | + |
| 113 | + /* SVG container with pan and zoom */ |
| 114 | + :global(.mermaid-fullscreen-content) { |
| 115 | + flex: 1; |
| 116 | + display: flex; |
| 117 | + align-items: center; |
| 118 | + justify-content: center; |
| 119 | + overflow: hidden; |
| 120 | + position: relative; |
| 121 | + cursor: grab; |
| 122 | + padding: 80px 20px 100px; |
| 123 | + } |
| 124 | + |
| 125 | + :global(.mermaid-fullscreen-content.dragging) { |
| 126 | + cursor: grabbing; |
| 127 | + } |
| 128 | + |
| 129 | + :global(.mermaid-fullscreen-svg-wrapper) { |
| 130 | + position: relative; |
| 131 | + transition: transform 0.1s ease-out; |
| 132 | + transform-origin: center center; |
| 133 | + } |
| 134 | + |
| 135 | + :global(.mermaid-fullscreen-svg-wrapper svg) { |
| 136 | + display: block; |
| 137 | + max-width: 100%; |
| 138 | + max-height: 100%; |
| 139 | + background: hsl(var(--background)); |
| 140 | + border-radius: 0.5rem; |
| 141 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
| 142 | + } |
| 143 | + |
| 144 | + /* Bottom toolbar */ |
| 145 | + :global(.mermaid-fullscreen-toolbar) { |
| 146 | + position: absolute; |
| 147 | + bottom: 0; |
| 148 | + left: 0; |
| 149 | + right: 0; |
| 150 | + display: flex; |
| 151 | + align-items: center; |
| 152 | + justify-content: center; |
| 153 | + gap: 0.5rem; |
| 154 | + padding: 1rem; |
| 155 | + background: hsl(var(--background) / 0.95); |
| 156 | + backdrop-filter: blur(8px); |
| 157 | + border-top: 1px solid hsl(var(--border)); |
| 158 | + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); |
| 159 | + } |
| 160 | + |
| 161 | + :global(.mermaid-toolbar-btn) { |
| 162 | + display: flex; |
| 163 | + align-items: center; |
| 164 | + justify-content: center; |
| 165 | + width: 40px; |
| 166 | + height: 40px; |
| 167 | + padding: 0; |
| 168 | + border-radius: 0.375rem; |
| 169 | + background: hsl(var(--muted)); |
| 170 | + border: 1px solid hsl(var(--border)); |
| 171 | + cursor: pointer; |
| 172 | + color: hsl(var(--foreground)); |
| 173 | + transition: all 0.2s ease; |
| 174 | + font-size: 14px; |
| 175 | + } |
| 176 | + |
| 177 | + :global(.mermaid-toolbar-btn:hover) { |
| 178 | + background: hsl(var(--muted) / 0.8); |
| 179 | + border-color: hsl(var(--primary)); |
| 180 | + color: hsl(var(--primary)); |
| 181 | + } |
| 182 | + |
| 183 | + :global(.mermaid-toolbar-btn:active) { |
| 184 | + transform: scale(0.95); |
| 185 | + } |
| 186 | + |
| 187 | + :global(.mermaid-toolbar-btn:disabled) { |
| 188 | + opacity: 0.5; |
| 189 | + cursor: not-allowed; |
| 190 | + } |
| 191 | + |
| 192 | + :global(.mermaid-toolbar-btn svg) { |
| 193 | + width: 18px; |
| 194 | + height: 18px; |
| 195 | + } |
| 196 | + |
| 197 | + :global(.mermaid-zoom-info) { |
| 198 | + min-width: 80px; |
| 199 | + text-align: center; |
| 200 | + font-size: 14px; |
| 201 | + color: hsl(var(--foreground) / 0.8); |
| 202 | + font-variant-numeric: tabular-nums; |
| 203 | + } |
91 | 204 | </style> |
92 | 205 |
|
93 | 206 | <script is:inline> |
|
155 | 268 | </svg> |
156 | 269 | `; |
157 | 270 |
|
| 271 | + // Expand button |
| 272 | + const expandBtn = document.createElement('button'); |
| 273 | + expandBtn.className = 'mermaid-control-btn'; |
| 274 | + expandBtn.setAttribute('title', 'Expand to fullscreen'); |
| 275 | + expandBtn.innerHTML = ` |
| 276 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 277 | + <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path> |
| 278 | + </svg> |
| 279 | + `; |
| 280 | + |
158 | 281 | // Add buttons to controls |
159 | 282 | controls.appendChild(copyBtn); |
160 | 283 | controls.appendChild(downloadBtn); |
| 284 | + controls.appendChild(expandBtn); |
161 | 285 |
|
162 | 286 | // Add to diagram container |
163 | 287 | diagram.appendChild(controls); |
|
218 | 342 | console.error('Failed to download:', err); |
219 | 343 | } |
220 | 344 | }); |
| 345 | + |
| 346 | + // Expand to fullscreen functionality |
| 347 | + expandBtn.addEventListener('click', (e) => { |
| 348 | + e.stopPropagation(); |
| 349 | + openFullscreenViewer(diagram); |
| 350 | + }); |
| 351 | + }); |
| 352 | + } |
| 353 | + |
| 354 | + // Fullscreen viewer functionality |
| 355 | + function openFullscreenViewer(diagram) { |
| 356 | + const svgElement = diagram.querySelector('svg'); |
| 357 | + if (!svgElement) return; |
| 358 | + |
| 359 | + const originalCode = originalCodeMap.get(diagram) || diagram.getAttribute('data-diagram') || ''; |
| 360 | + |
| 361 | + // Create modal |
| 362 | + const modal = document.createElement('div'); |
| 363 | + modal.className = 'mermaid-fullscreen-modal'; |
| 364 | + |
| 365 | + // Create content container |
| 366 | + const content = document.createElement('div'); |
| 367 | + content.className = 'mermaid-fullscreen-content'; |
| 368 | + |
| 369 | + // Create SVG wrapper |
| 370 | + const svgWrapper = document.createElement('div'); |
| 371 | + svgWrapper.className = 'mermaid-fullscreen-svg-wrapper'; |
| 372 | + |
| 373 | + // Clone SVG |
| 374 | + const clonedSvg = svgElement.cloneNode(true); |
| 375 | + svgWrapper.appendChild(clonedSvg); |
| 376 | + content.appendChild(svgWrapper); |
| 377 | + |
| 378 | + // Create toolbar |
| 379 | + const toolbar = document.createElement('div'); |
| 380 | + toolbar.className = 'mermaid-fullscreen-toolbar'; |
| 381 | + |
| 382 | + // Zoom state |
| 383 | + let scale = 1; |
| 384 | + let panX = 0; |
| 385 | + let panY = 0; |
| 386 | + let isDragging = false; |
| 387 | + let startX = 0; |
| 388 | + let startY = 0; |
| 389 | + let startPanX = 0; |
| 390 | + let startPanY = 0; |
| 391 | + |
| 392 | + // Update transform |
| 393 | + function updateTransform() { |
| 394 | + svgWrapper.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`; |
| 395 | + zoomInfo.textContent = `${Math.round(scale * 100)}%`; |
| 396 | + zoomOutBtn.disabled = scale <= 0.25; |
| 397 | + zoomInBtn.disabled = scale >= 5; |
| 398 | + } |
| 399 | + |
| 400 | + // Zoom in button |
| 401 | + const zoomInBtn = document.createElement('button'); |
| 402 | + zoomInBtn.className = 'mermaid-toolbar-btn'; |
| 403 | + zoomInBtn.setAttribute('title', 'Zoom in'); |
| 404 | + zoomInBtn.innerHTML = ` |
| 405 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 406 | + <circle cx="11" cy="11" r="8"></circle> |
| 407 | + <line x1="21" y1="21" x2="16.65" y2="16.65"></line> |
| 408 | + <line x1="11" y1="8" x2="11" y2="14"></line> |
| 409 | + <line x1="8" y1="11" x2="14" y2="11"></line> |
| 410 | + </svg> |
| 411 | + `; |
| 412 | + zoomInBtn.addEventListener('click', () => { |
| 413 | + scale = Math.min(scale * 1.2, 5); |
| 414 | + updateTransform(); |
| 415 | + }); |
| 416 | + |
| 417 | + // Zoom out button |
| 418 | + const zoomOutBtn = document.createElement('button'); |
| 419 | + zoomOutBtn.className = 'mermaid-toolbar-btn'; |
| 420 | + zoomOutBtn.setAttribute('title', 'Zoom out'); |
| 421 | + zoomOutBtn.innerHTML = ` |
| 422 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 423 | + <circle cx="11" cy="11" r="8"></circle> |
| 424 | + <line x1="21" y1="21" x2="16.65" y2="16.65"></line> |
| 425 | + <line x1="8" y1="11" x2="14" y2="11"></line> |
| 426 | + </svg> |
| 427 | + `; |
| 428 | + zoomOutBtn.addEventListener('click', () => { |
| 429 | + scale = Math.max(scale / 1.2, 0.25); |
| 430 | + updateTransform(); |
| 431 | + }); |
| 432 | + |
| 433 | + // Reset button |
| 434 | + const resetBtn = document.createElement('button'); |
| 435 | + resetBtn.className = 'mermaid-toolbar-btn'; |
| 436 | + resetBtn.setAttribute('title', 'Reset zoom'); |
| 437 | + resetBtn.innerHTML = ` |
| 438 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 439 | + <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path> |
| 440 | + <path d="M21 3v5h-5"></path> |
| 441 | + <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path> |
| 442 | + <path d="M3 21v-5h5"></path> |
| 443 | + </svg> |
| 444 | + `; |
| 445 | + resetBtn.addEventListener('click', () => { |
| 446 | + scale = 1; |
| 447 | + panX = 0; |
| 448 | + panY = 0; |
| 449 | + updateTransform(); |
| 450 | + }); |
| 451 | + |
| 452 | + // Copy code button |
| 453 | + const copyCodeBtn = document.createElement('button'); |
| 454 | + copyCodeBtn.className = 'mermaid-toolbar-btn'; |
| 455 | + copyCodeBtn.setAttribute('title', 'Copy source code'); |
| 456 | + copyCodeBtn.innerHTML = ` |
| 457 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 458 | + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> |
| 459 | + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> |
| 460 | + </svg> |
| 461 | + `; |
| 462 | + copyCodeBtn.addEventListener('click', async () => { |
| 463 | + if (originalCode) { |
| 464 | + try { |
| 465 | + await navigator.clipboard.writeText(originalCode); |
| 466 | + copyCodeBtn.innerHTML = ` |
| 467 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 468 | + <path d="M20 6L9 17l-5-5"></path> |
| 469 | + </svg> |
| 470 | + `; |
| 471 | + setTimeout(() => { |
| 472 | + copyCodeBtn.innerHTML = ` |
| 473 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 474 | + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> |
| 475 | + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> |
| 476 | + </svg> |
| 477 | + `; |
| 478 | + }, 2000); |
| 479 | + } catch (err) { |
| 480 | + console.error('Failed to copy:', err); |
| 481 | + } |
| 482 | + } |
| 483 | + }); |
| 484 | + |
| 485 | + // Zoom info |
| 486 | + const zoomInfo = document.createElement('div'); |
| 487 | + zoomInfo.className = 'mermaid-zoom-info'; |
| 488 | + zoomInfo.textContent = '100%'; |
| 489 | + |
| 490 | + // Close button |
| 491 | + const closeBtn = document.createElement('button'); |
| 492 | + closeBtn.className = 'mermaid-toolbar-btn'; |
| 493 | + closeBtn.setAttribute('title', 'Close'); |
| 494 | + closeBtn.innerHTML = ` |
| 495 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 496 | + <line x1="18" y1="6" x2="6" y2="18"></line> |
| 497 | + <line x1="6" y1="6" x2="18" y2="18"></line> |
| 498 | + </svg> |
| 499 | + `; |
| 500 | + closeBtn.addEventListener('click', () => { |
| 501 | + modal.classList.remove('active'); |
| 502 | + setTimeout(() => { |
| 503 | + document.body.removeChild(modal); |
| 504 | + }, 300); |
| 505 | + }); |
| 506 | + |
| 507 | + // Build toolbar |
| 508 | + toolbar.appendChild(zoomOutBtn); |
| 509 | + toolbar.appendChild(zoomInfo); |
| 510 | + toolbar.appendChild(zoomInBtn); |
| 511 | + toolbar.appendChild(resetBtn); |
| 512 | + toolbar.appendChild(copyCodeBtn); |
| 513 | + toolbar.appendChild(closeBtn); |
| 514 | + |
| 515 | + // Mouse wheel zoom |
| 516 | + content.addEventListener('wheel', (e) => { |
| 517 | + e.preventDefault(); |
| 518 | + const delta = e.deltaY > 0 ? 0.9 : 1.1; |
| 519 | + const oldScale = scale; |
| 520 | + scale = Math.max(0.25, Math.min(5, scale * delta)); |
| 521 | + |
| 522 | + // Zoom towards mouse position |
| 523 | + const rect = content.getBoundingClientRect(); |
| 524 | + const mouseX = e.clientX - rect.left; |
| 525 | + const mouseY = e.clientY - rect.top; |
| 526 | + const scaleDiff = scale - oldScale; |
| 527 | + panX -= (mouseX - rect.width / 2 - panX) * scaleDiff / oldScale; |
| 528 | + panY -= (mouseY - rect.height / 2 - panY) * scaleDiff / oldScale; |
| 529 | + |
| 530 | + updateTransform(); |
| 531 | + }, { passive: false }); |
| 532 | + |
| 533 | + // Pan with mouse drag |
| 534 | + content.addEventListener('mousedown', (e) => { |
| 535 | + if (e.button === 0) { // Left mouse button |
| 536 | + isDragging = true; |
| 537 | + content.classList.add('dragging'); |
| 538 | + startX = e.clientX; |
| 539 | + startY = e.clientY; |
| 540 | + startPanX = panX; |
| 541 | + startPanY = panY; |
| 542 | + } |
221 | 543 | }); |
| 544 | + |
| 545 | + document.addEventListener('mousemove', (e) => { |
| 546 | + if (isDragging) { |
| 547 | + panX = startPanX + (e.clientX - startX); |
| 548 | + panY = startPanY + (e.clientY - startY); |
| 549 | + updateTransform(); |
| 550 | + } |
| 551 | + }); |
| 552 | + |
| 553 | + document.addEventListener('mouseup', () => { |
| 554 | + if (isDragging) { |
| 555 | + isDragging = false; |
| 556 | + content.classList.remove('dragging'); |
| 557 | + } |
| 558 | + }); |
| 559 | + |
| 560 | + // Close on Escape key |
| 561 | + const handleEscape = (e) => { |
| 562 | + if (e.key === 'Escape') { |
| 563 | + closeBtn.click(); |
| 564 | + document.removeEventListener('keydown', handleEscape); |
| 565 | + } |
| 566 | + }; |
| 567 | + document.addEventListener('keydown', handleEscape); |
| 568 | + |
| 569 | + // Assemble modal |
| 570 | + modal.appendChild(content); |
| 571 | + modal.appendChild(toolbar); |
| 572 | + document.body.appendChild(modal); |
| 573 | + |
| 574 | + // Trigger animation |
| 575 | + requestAnimationFrame(() => { |
| 576 | + modal.classList.add('active'); |
| 577 | + }); |
| 578 | + |
| 579 | + // Initial transform |
| 580 | + updateTransform(); |
222 | 581 | } |
223 | 582 |
|
224 | 583 | // Wait for DOM ready and check again |
|
0 commit comments