--- /dev/null
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
+ integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
+ crossorigin="anonymous" referrerpolicy="no-referrer" />
+<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
+<script type="module">
+ mermaid.initialize({
+ startOnLoad: false,
+ securityLevel: 'loose',
+ theme: 'default'
+ });
+
+ // Zoom Level Configuration
+ const ZOOM_LEVEL_MIN = 0.5;
+ const ZOOM_LEVEL_MAX = 2.0;
+ const ZOOM_LEVEL_INCREMENT = 0.1;
+ const DEFAULT_ZOOM_SCALE = 1.0;
+
+ const DRAG_THRESHOLD_PIXELS = 3;
+ const ZOOM_ANIMATION_CLASS_TIMEOUT_MS = 200;
+ const NOTIFICATION_DISPLAY_TIMEOUT_MS = 2000;
+
+ const CSS_CLASSES = {
+ CONTAINER: 'mermaid-container',
+ VIEWPORT: 'mermaid-viewport',
+ CONTENT: 'mermaid-content',
+ DIAGRAM: 'mermaid-diagram',
+ CONTROLS: 'mermaid-controls',
+ ZOOM_CONTROLS: 'mermaid-zoom-controls',
+ INTERACTION_ENABLED: 'interaction-enabled',
+ DRAGGING: 'dragging',
+ ZOOMING: 'zooming',
+ LOCK_ICON: 'fa fa-lock',
+ UNLOCK_ICON: 'fa fa-unlock',
+ INTERACTIVE_HOVER: 'interactive-hover', // Class for 'grab' cursor state
+ INTERACTIVE_PAN: 'interactive-pan', // Class for 'grabbing' cursor state
+ BUTTON_BASE: 'mermaid-viewer-button-base', // Base class for all viewer buttons
+ NOTIFICATION: 'mermaid-notification', // Keep existing notification classes
+ NOTIFICATION_SHOW: 'show' // Keep existing notification classes
+ };
+
+ class InteractiveMermaidViewer {
+ constructor(container, mermaidCode) {
+ this.container = container;
+ this.mermaidCode = mermaidCode;
+ this.scale = 1.0;
+ this.translateX = 0;
+ this.translateY = 0;
+ this.isDragging = false;
+ this.dragStarted = false;
+ this.startX = 0;
+ this.startY = 0;
+
+ const numDecimalPlaces = (ZOOM_LEVEL_INCREMENT.toString().split('.')[1] || '').length;
+ this.zoomLevels = Array.from(
+ { length: Math.round((ZOOM_LEVEL_MAX - ZOOM_LEVEL_MIN) / ZOOM_LEVEL_INCREMENT) + 1 },
+ (_, i) => parseFloat((ZOOM_LEVEL_MIN + i * ZOOM_LEVEL_INCREMENT).toFixed(numDecimalPlaces))
+ );
+
+ this.currentZoomIndex = this.zoomLevels.findIndex(level => Math.abs(level - DEFAULT_ZOOM_SCALE) < 1e-9);
+ if (this.currentZoomIndex === -1) {
+ this.currentZoomIndex = Math.floor(this.zoomLevels.length / 2);
+ }
+ this.interactionEnabled = false;
+ this.initialContentOffset = { x: 0, y: 0 };
+
+ // Cache DOM elements
+ this.toggleInteractionBtn = null;
+ this.copyCodeBtn = null;
+ this.zoomInBtn = null;
+ this.zoomOutBtn = null;
+ this.zoomResetBtn = null;
+
+ this.boundMouseMoveHandler = this.handleMouseMove.bind(this);
+ this.boundMouseUpHandler = this.handleMouseUp.bind(this);
+ this.setupViewer();
+ this.setupEventListeners();
+ }
+
+ setupViewer() {
+ this.container.innerHTML = `
+ <div class="${CSS_CLASSES.CONTROLS}">
+ <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-btn toggle-interaction" title="Toggle interaction">
+ <i class="${CSS_CLASSES.LOCK_ICON}" aria-hidden="true"></i>
+ </div>
+ <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-btn" title="Copy code">
+ <i class="fa fa-copy" aria-hidden="true"></i>
+ </div>
+ </div>
+ <div class="${CSS_CLASSES.ZOOM_CONTROLS}">
+ <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-zoom-btn zoom-in" title="Zoom in"><i class="fa fa-search-plus" aria-hidden="true"></i></div>
+ <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-zoom-btn zoom-out" title="Zoom out"><i class="fa fa-search-minus" aria-hidden="true"></i></div>
+ <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-zoom-btn zoom-reset" title="Reset"><i class="fa fa-refresh" aria-hidden="true"></i></div>
+ </div>
+ <div class="${CSS_CLASSES.VIEWPORT}">
+ <div class="${CSS_CLASSES.CONTENT}">
+ <div class="${CSS_CLASSES.DIAGRAM}">${this.mermaidCode}</div>
+ </div>
+ </div>
+ `;
+ this.viewport = this.container.querySelector(`.${CSS_CLASSES.VIEWPORT}`);
+ this.content = this.container.querySelector(`.${CSS_CLASSES.CONTENT}`);
+ this.diagram = this.container.querySelector(`.${CSS_CLASSES.DIAGRAM}`);
+
+ // Cache control elements
+ this.toggleInteractionBtn = this.container.querySelector('.toggle-interaction');
+ this.copyCodeBtn = this.container.querySelector('.mermaid-btn:not(.toggle-interaction)');
+ this.zoomInBtn = this.container.querySelector('.zoom-in');
+ this.zoomOutBtn = this.container.querySelector('.zoom-out');
+ this.zoomResetBtn = this.container.querySelector('.zoom-reset');
+
+ // Function to render the diagram and perform post-render setup
+ const renderAndSetup = () => {
+ mermaid.run({ nodes: [this.diagram] }).then(() => {
+ this.adjustContainerHeight();
+ this.calculateInitialOffset();
+ this.centerDiagram();
+ }).catch(error => {
+ console.error("Mermaid rendering error for diagram:", this.mermaidCode, error);
+ this.diagram.innerHTML = `<p style="color: red; padding: 10px;">Error rendering diagram. Check console.</p>`;
+ });
+ };
+
+ // Check if Font Awesome is loaded before rendering
+ // This checks for the 'Font Awesome 6 Free' font family, which is common.
+ // Adjust if your Font Awesome version uses a different family name for its core icons.
+ if (document.fonts && typeof document.fonts.check === 'function' && document.fonts.check('1em "Font Awesome 6 Free"')) { // Check if Font Awesome is immediately available
+ renderAndSetup();
+ } else if (document.fonts && document.fonts.ready) { // Simplified check for document.fonts.ready
+ document.fonts.ready.then(renderAndSetup).catch(err => {
+ renderAndSetup(); // Proceed with rendering even if font check fails after timeout/error
+ });
+ } else {
+ renderAndSetup();
+ }
+ }
+
+ adjustContainerHeight() {
+ const svgElement = this.content.querySelector('svg');
+ if (svgElement) {
+ // Ensure the viewport takes up the height of the rendered SVG
+ this.viewport.style.height = '100%';
+ }
+ }
+
+ calculateInitialOffset() {
+ const originalTransform = this.content.style.transform;
+ this.content.style.transform = '';
+ const contentRect = this.content.getBoundingClientRect();
+ const viewportRect = this.viewport.getBoundingClientRect();
+ this.initialContentOffset.x = contentRect.left - viewportRect.left;
+ this.initialContentOffset.y = contentRect.top - viewportRect.top;
+ this.content.style.transform = originalTransform;
+ }
+
+ _getViewportCenterClientCoords() {
+ const viewportRect = this.viewport.getBoundingClientRect();
+ return {
+ clientX: viewportRect.left + viewportRect.width / 2,
+ clientY: viewportRect.top + viewportRect.height / 2,
+ };
+ }
+
+ setupEventListeners() {
+ this.toggleInteractionBtn.addEventListener('click', () => this.toggleInteraction());
+ this.copyCodeBtn.addEventListener('click', () => this.copyCode());
+ this.zoomInBtn.addEventListener('click', () => {
+ const { clientX, clientY } = this._getViewportCenterClientCoords();
+ this.zoom(1, clientX, clientY);
+ });
+ this.zoomOutBtn.addEventListener('click', () => {
+ const { clientX, clientY } = this._getViewportCenterClientCoords();
+ this.zoom(-1, clientX, clientY);
+ });
+ this.zoomResetBtn.addEventListener('click', () => this.resetZoom());
+
+ this.viewport.addEventListener('wheel', (e) => {
+ if (!this.interactionEnabled) return;
+ // Prevent default browser scroll/zoom behavior when wheeling over the diagram
+ e.preventDefault();
+ this.content.classList.add(CSS_CLASSES.ZOOMING);
+ const clientX = e.clientX;
+ const clientY = e.clientY;
+ if (e.deltaY > 0) this.zoom(-1, clientX, clientY);
+ else this.zoom(1, clientX, clientY);
+ setTimeout(() => this.content.classList.remove(CSS_CLASSES.ZOOMING), ZOOM_ANIMATION_CLASS_TIMEOUT_MS);
+ }, { passive: false });
+
+ this.viewport.addEventListener('mousedown', (e) => {
+ if (!this.interactionEnabled || e.button !== 0) return;
+ e.preventDefault();
+ this.isDragging = true;
+ this.dragStarted = false;
+ this.startX = e.clientX;
+ this.startY = e.clientY;
+ this.dragBaseTranslateX = this.translateX;
+ this.dragBaseTranslateY = this.translateY;
+ this.viewport.classList.add(CSS_CLASSES.DRAGGING);
+ this.viewport.classList.remove(CSS_CLASSES.INTERACTIVE_HOVER);
+ this.viewport.classList.add(CSS_CLASSES.INTERACTIVE_PAN);
+ this.content.classList.remove(CSS_CLASSES.ZOOMING);
+ });
+
+ // Listen on document for mousemove to handle dragging outside viewport
+ document.addEventListener('mousemove', this.boundMouseMoveHandler);
+ // Listen on window for mouseup to ensure drag ends even if mouse is released outside
+ window.addEventListener('mouseup', this.boundMouseUpHandler, true); // Use capture phase
+
+ this.viewport.addEventListener('contextmenu', (e) => e.preventDefault());
+ this.viewport.addEventListener('selectstart', (e) => { if (this.isDragging || this.interactionEnabled) e.preventDefault(); });
+ }
+
+ toggleInteraction() {
+ this.interactionEnabled = !this.interactionEnabled;
+ const icon = this.toggleInteractionBtn.querySelector('i');
+ this.toggleInteractionBtn.setAttribute('aria-pressed', this.interactionEnabled.toString());
+
+ if (this.interactionEnabled) {
+ icon.className = CSS_CLASSES.UNLOCK_ICON;
+ this.toggleInteractionBtn.title = 'Disable manual interaction';
+ this.viewport.classList.add(CSS_CLASSES.INTERACTION_ENABLED);
+ this.viewport.classList.add(CSS_CLASSES.INTERACTIVE_HOVER); // Set grab cursor state
+ this.viewport.classList.remove(CSS_CLASSES.INTERACTIVE_PAN); // Ensure pan cursor state is off
+ } else {
+ icon.className = CSS_CLASSES.LOCK_ICON;
+ this.toggleInteractionBtn.title = 'Enable manual interaction';
+ this.viewport.classList.remove(CSS_CLASSES.INTERACTION_ENABLED);
+ this.viewport.classList.remove(CSS_CLASSES.INTERACTIVE_HOVER);
+ this.viewport.classList.remove(CSS_CLASSES.INTERACTIVE_PAN);
+ this.isDragging = false; // Ensure dragging stops if interaction is disabled mid-drag
+ this.dragStarted = false;
+ this.viewport.classList.remove(CSS_CLASSES.DRAGGING);
+ }
+ }
+
+ updateTransform() {
+ this.content.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`;
+ }
+
+ handleMouseMove(e) {
+ if (!this.isDragging) return;
+ // e.preventDefault() is called only after dragStarted is true to allow clicks if threshold isn't met.
+ const deltaX = e.clientX - this.startX;
+ const deltaY = e.clientY - this.startY;
+ if (!this.dragStarted && (Math.abs(deltaX) > DRAG_THRESHOLD_PIXELS || Math.abs(deltaY) > DRAG_THRESHOLD_PIXELS)) {
+ this.dragStarted = true;
+ }
+ if (this.dragStarted) {
+ e.preventDefault(); // Prevent text selection, etc., only when drag has truly started
+ this.translateX = this.dragBaseTranslateX + deltaX;
+ this.translateY = this.dragBaseTranslateY + deltaY;
+ this.updateTransform();
+ }
+ }
+
+ handleMouseUp() {
+ if (this.isDragging) {
+ this.isDragging = false;
+ this.dragStarted = false;
+ this.viewport.classList.remove(CSS_CLASSES.DRAGGING);
+ this.viewport.classList.remove(CSS_CLASSES.INTERACTIVE_PAN);
+ if (this.interactionEnabled) { // Revert to grab cursor if interaction is still enabled
+ this.viewport.classList.add(CSS_CLASSES.INTERACTIVE_HOVER);
+ }
+ }
+ this.content.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`;
+ }
+
+ centerDiagram() {
+ const svgElement = this.content.querySelector('svg');
+ if (svgElement) {
+ const viewportRect = this.viewport.getBoundingClientRect();
+ const svgIntrinsicWidth = svgElement.viewBox.baseVal.width || svgElement.clientWidth;
+ const svgIntrinsicHeight = svgElement.viewBox.baseVal.height || svgElement.clientHeight;
+
+ const targetContentLeftRelativeToViewport = (viewportRect.width - (svgIntrinsicWidth * this.scale)) / 2;
+ const targetContentTopRelativeToViewport = (viewportRect.height - (svgIntrinsicHeight * this.scale)) / 2;
+
+ this.translateX = targetContentLeftRelativeToViewport - this.initialContentOffset.x;
+ this.translateY = targetContentTopRelativeToViewport - this.initialContentOffset.y;
+
+ // Initial centering constraints; may need adjustment for very large diagrams.
+ this.translateX = Math.max(0, this.translateX);
+ this.translateY = Math.max(0, this.translateY);
+
+ this.updateTransform();
+ }
+ }
+
+ zoom(direction, clientX, clientY) {
+ this.content.classList.add(CSS_CLASSES.ZOOMING);
+ const oldScale = this.scale;
+ let newZoomIndex = this.currentZoomIndex + direction;
+
+ if (newZoomIndex >= 0 && newZoomIndex < this.zoomLevels.length) {
+ this.currentZoomIndex = newZoomIndex;
+ const newScale = this.zoomLevels[this.currentZoomIndex];
+
+ const viewportRect = this.viewport.getBoundingClientRect();
+ const pointXInContent = (clientX - viewportRect.left - this.translateX) / oldScale;
+ const pointYInContent = (clientY - viewportRect.top - this.translateY) / oldScale;
+
+ this.translateX = (clientX - viewportRect.left) - (pointXInContent * newScale);
+ this.translateY = (clientY - viewportRect.top) - (pointYInContent * newScale);
+ this.scale = newScale;
+ this.updateTransform();
+ }
+ setTimeout(() => this.content.classList.remove(CSS_CLASSES.ZOOMING), ZOOM_ANIMATION_CLASS_TIMEOUT_MS);
+ }
+
+ resetZoom() {
+ this.content.classList.add(CSS_CLASSES.ZOOMING);
+ this.currentZoomIndex = this.zoomLevels.findIndex(level => Math.abs(level - DEFAULT_ZOOM_SCALE) < 1e-9);
+ if (this.currentZoomIndex === -1) { // Fallback if default not exactly in levels
+ this.currentZoomIndex = Math.floor(this.zoomLevels.length / 2);
+ }
+ this.scale = this.zoomLevels[this.currentZoomIndex];
+ // Use requestAnimationFrame to ensure layout is stable before centering
+ requestAnimationFrame(() => {
+ this.centerDiagram();
+ setTimeout(() => this.content.classList.remove(CSS_CLASSES.ZOOMING), ZOOM_ANIMATION_CLASS_TIMEOUT_MS);
+ });
+ }
+
+ async copyCode() {
+ try {
+ await navigator.clipboard.writeText(this.mermaidCode);
+ this.showNotification('Copied!');
+ } catch (_error) {
+ // Fallback for older browsers or if clipboard API fails
+ const textArea = document.createElement('textarea');
+ textArea.value = this.mermaidCode;
+ // Style to make it invisible
+ textArea.style.position = 'fixed';
+ textArea.style.top = '-9999px';
+ textArea.style.left = '-9999px';
+ document.body.appendChild(textArea);
+ textArea.select();
+ try {
+ document.execCommand('copy');
+ this.showNotification('Copied!');
+ } catch (copyError) {
+ console.error('Fallback copy failed:', copyError);
+ this.showNotification('Copy failed.', true); // Error
+ }
+ document.body.removeChild(textArea);
+ }
+ }
+
+ showNotification(message, isError = false) {
+ let notification = document.querySelector(`.${CSS_CLASSES.NOTIFICATION}`);
+ if (!notification) {
+ notification = document.createElement('div');
+ notification.className = CSS_CLASSES.NOTIFICATION;
+ document.body.appendChild(notification);
+ }
+ notification.innerHTML = `<i class="fa ${isError ? 'fa-times-circle' : 'fa-check'}"></i> ${message}`;
+ notification.style.background = isError ? '#dc3545' : '#28a745'; // Red for error, green for success
+ // Ensure it's visible before triggering transition
+ notification.style.transform = 'translateX(400px)'; // Reset if previously shown
+ requestAnimationFrame(() => { // Allow repaint
+ notification.classList.add(CSS_CLASSES.NOTIFICATION_SHOW);
+ });
+
+ // Clear any existing timeout to prevent premature hiding if clicked again
+ if (this.notificationTimeout) {
+ clearTimeout(this.notificationTimeout);
+ }
+ this.notificationTimeout = setTimeout(() => {
+ notification.classList.remove(CSS_CLASSES.NOTIFICATION_SHOW);
+ this.notificationTimeout = null;
+ }, NOTIFICATION_DISPLAY_TIMEOUT_MS);
+ }
+
+ destroy() {
+ // Remove event listeners specific to this instance
+ this.toggleInteractionBtn.removeEventListener('click', this.toggleInteraction); // Need to ensure this is the same function reference
+ this.copyCodeBtn.removeEventListener('click', this.copyCode);
+ this.zoomInBtn.removeEventListener('click', this.zoom); // These would need bound versions or careful handling
+ this.zoomOutBtn.removeEventListener('click', this.zoom);
+ this.zoomResetBtn.removeEventListener('click', this.resetZoom);
+
+ this.viewport.removeEventListener('wheel', this.handleWheel); // Assuming handleWheel is the actual handler
+ this.viewport.removeEventListener('mousedown', this.handleMouseDown); // Assuming handleMouseDown
+ this.viewport.removeEventListener('contextmenu', this.handleContextMenu);
+ this.viewport.removeEventListener('selectstart', this.handleSelectStart);
+
+ document.removeEventListener('mousemove', this.boundMouseMoveHandler);
+ window.removeEventListener('mouseup', this.boundMouseUpHandler, true);
+
+ if (this.notificationTimeout) {
+ clearTimeout(this.notificationTimeout);
+ }
+ this.container.innerHTML = ''; // Clear the container's content
+ }
+ }
+
+ const mermaidViewers = [];
+ function initializeMermaidViewers() {
+ // Adjust the selector if your CMS wraps mermaid code blocks differently
+ const codeBlocks = document.querySelectorAll('pre code.language-mermaid');
+ for (const codeBlock of codeBlocks) {
+ // Ensure we don't re-initialize if this script runs multiple times or content is dynamic
+ if (codeBlock.dataset.mermaidViewerInitialized) continue;
+
+ const mermaidCode = codeBlock.textContent || codeBlock.innerHTML; // textContent is usually better
+ const container = document.createElement('div');
+ container.className = CSS_CLASSES.CONTAINER;
+
+ const replaceTarget = (codeBlock.nodeName === 'CODE') ? codeBlock.parentElement : codeBlock;
+
+ // Check if replaceTarget is already a mermaid-container (e.g. from previous init)
+ if (replaceTarget.classList.contains(CSS_CLASSES.CONTAINER)) continue;
+
+ replaceTarget.after(container);
+ replaceTarget.remove(); // Remove the original <pre> or <pre><code> block
+
+ const viewer = new InteractiveMermaidViewer(container, mermaidCode);
+ mermaidViewers.push(viewer);
+ codeBlock.dataset.mermaidViewerInitialized = 'true'; // Mark as initialized
+ }
+ }
+
+ // Initialize on DOMContentLoaded
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeMermaidViewers);
+ } else {
+ // DOMContentLoaded has already fired
+ initializeMermaidViewers();
+ }
+
+ // Re-center diagrams on window load, as images/fonts inside SVG might affect size
+ window.addEventListener('load', () => {
+ mermaidViewers.forEach(viewer => {
+ // Delay slightly to ensure mermaid rendering is fully complete and dimensions are stable
+ setTimeout(() => viewer.centerDiagram(), 100);
+ });
+ });
+
+ // Optional: If your CMS dynamically adds content, you might need a way to re-run initialization
+ // For example, using a MutationObserver or a custom event.
+ // document.addEventListener('myCMSContentLoaded', () => initializeMermaidViewers());
+
+</script>
+<style>
+ .mermaid-container {
+ background: white;
+ border: 1px solid #d0d7de;
+ border-radius: 6px;
+ position: relative;
+ margin: 20px 0;
+ }
+
+ .mermaid-viewport {
+ height: 100%;
+ /* This will now be 100% of the dynamically set container height */
+ overflow: hidden;
+ /* Keep this for panning/zooming when content exceeds viewport */
+ cursor: auto;
+ /* Default to normal system cursor */
+ }
+
+ /* Ensure viewport cursor is auto when locked, even if active.
+ The text selection (I-beam) cursor will still appear over selectable text within .mermaid-content. */
+ .mermaid-viewport:not(.interaction-enabled):active {
+ cursor: auto;
+ }
+
+ /* Set 'grab' cursor when the viewport has the 'interactive-hover' class. */
+ .mermaid-viewport.interactive-hover {
+ cursor: grab;
+ }
+
+ /* Set 'grabbing' cursor when the viewport has the 'interactive-pan' class. */
+ .mermaid-viewport.interactive-pan {
+ cursor: grabbing !important;
+ }
+
+ .mermaid-content {
+ transform-origin: 0 0;
+ /* Allow text selection by default (when interaction is locked) */
+ user-select: auto;
+ /* or 'text' */
+ will-change: transform;
+ }
+
+ /* Disable text selection ONLY when interaction is enabled on the viewport */
+ .mermaid-viewport.interaction-enabled .mermaid-content {
+ user-select: none;
+ }
+
+ /* SVG elements inherit cursor from the viewport when interaction is enabled. */
+ .mermaid-viewport.interaction-enabled .mermaid-content svg,
+ .mermaid-viewport.interaction-enabled .mermaid-content svg * {
+ cursor: inherit !important;
+ /* Force inheritance from the viewport's cursor */
+ }
+
+ .mermaid-content.zooming {
+ transition: transform 0.2s ease;
+ }
+
+ .mermaid-controls {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ display: flex;
+ gap: 5px;
+ z-index: 10;
+ }
+
+ .mermaid-viewer-button-base {
+ border: 1px solid #d0d7de;
+ border-radius: 6px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+ background: rgba(255, 255, 255, 0.9);
+ width: 32px;
+ height: 32px;
+ color: #24292f;
+ }
+
+ .mermaid-viewer-button-base:hover {
+ background: #f6f8fa;
+ }
+
+ .mermaid-zoom-controls {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ z-index: 10;
+ }
+
+ .mermaid-notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: #28a745;
+ color: white;
+ padding: 12px 16px;
+ border-radius: 6px;
+ transform: translateX(400px);
+ transition: transform 0.3s ease;
+ z-index: 1000;
+ }
+
+ .mermaid-notification.show {
+ transform: translateX(0);
+ }
+</style>
\ No newline at end of file