Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> <title>Mobile Mermaid Previewer</title> <style> /* Mobile‑friendly layout: the page uses a column layout on small screens and switches to a side‑by‑side layout when there’s more horizontal space available. The preview container uses absolute positioning internally so that panning and zooming can be applied smoothly via transforms. Touch‑friendly sizing and ample padding keep controls easy to tap on a phone. */ html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; font-family: sans-serif; background: #fdfdfd; } #app { display: flex; flex-direction: column; height: 100%; } @media (min-width: 800px) { #app { flex-direction: row; } } #editor-container { flex: 1; min-height: 150px; max-height: 50%; padding: 0.5rem; box-sizing: border-box; display: flex; flex-direction: column; } @media (min-width: 800px) { #editor-container { max-height: none; width: 40%; border-right: 1px solid #ddd; } } #editor-container textarea { flex: 1; resize: none; width: 100%; height: 100%; font-family: monospace; font-size: 14px; line-height: 1.4; padding: 0.5rem; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; } #toolbar { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; } #toolbar button { padding: 0.4rem 0.8rem; font-size: 14px; border: 1px solid #888; border-radius: 4px; background: #f3f3f3; cursor: pointer; } #toolbar button:active { background: #e3e3e3; } #preview-container { position: relative; flex: 2; background: #fff; overflow: hidden; touch-action: none; /* allow pointer event handlers to manage panning */ } @media (min-width: 800px) { #preview-container { width: 60%; max-height: none; } } #inner { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; } #error { color: #b00020; white-space: pre-wrap; padding: 0.5rem; display: none; overflow-y: auto; max-height: 6rem; border: 1px solid #f2caca; border-radius: 4px; background: #fee; margin-top: 0.5rem; } /* Minimal Tailwind-like utility classes to avoid external dependencies. These definitions approximate the classes used in the markup. They are intentionally scoped narrowly to keep the file self-contained. */ .flex { display: flex; } .flex-col { flex-direction: column; } .flex-row { flex-direction: row; } .flex-wrap { flex-wrap: wrap; } .items-center { align-items: center; } .gap-2 { gap: 0.5rem; } .mb-2 { margin-bottom: 0.5rem; } .p-2 { padding: 0.5rem; } .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; } .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; } .text-sm { font-size: 0.875rem; line-height: 1.25rem; } .border { border-width: 1px; border-style: solid; border-color: #e5e7eb; } .border-gray-200 { border-color: #e5e7eb; } .border-red-200 { border-color: #fecaca; } .rounded { border-radius: 0.25rem; } .bg-gray-200 { background-color: #e5e7eb; } .bg-gray-300 { background-color: #d1d5db; } .hover\:bg-gray-300:hover { background-color: #d1d5db; } .bg-red-100 { background-color: #fef2f2; } .text-red-700 { color: #b91c1c; } .flex-1 { flex: 1 1 0%; } .w-full { width: 100%; } .h-full { height: 100%; } .h-screen { height: 100vh; } .resize-none { resize: none; } .font-mono { font-family: monospace; } .leading-tight { line-height: 1.25; } .overflow-y-auto { overflow-y: auto; } .max-h-24 { max-height: 6rem; } .mt-2 { margin-top: 0.5rem; } .relative { position: relative; } .absolute { position: absolute; } .left-0 { left: 0; } .top-0 { top: 0; } .bg-white { background-color: #fff; } .overflow-hidden { overflow: hidden; } .hidden { display: none !important; } /* Responsive variants: apply styles at medium breakpoint (min-width: 768px) */ @media (min-width: 768px) { .md\:flex-row { flex-direction: row; } .md\:w-2\/5 { width: 40%; } .md\:w-3\/5 { width: 60%; } .md\:w-full { width: 100%; } .md\:border-r { border-right-width: 1px; border-right-style: solid; border-right-color: #e5e7eb; } } </style> <!-- Load Tailwind for styling and then Mermaid from CDN. Tailwind is loaded via the official CDN which processes the page at runtime. Mermaid is pinned to a specific version for stability. --> <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> </head> <body> <div id="app" class="flex flex-col h-screen w-full md:flex-row"> <!-- Editor section --> <div id="editor-container" class="flex flex-col flex-1 p-2 md:w-2/5 md:border-r border-gray-200"> <div id="toolbar" class="flex flex-wrap items-center gap-2 mb-2"> <button id="loadExample" class="px-2 py-1 text-sm border rounded bg-gray-200 hover:bg-gray-300">Load Example</button> <button id="fit" class="px-2 py-1 text-sm border rounded bg-gray-200 hover:bg-gray-300">Fit To Screen</button> <button id="clear" class="px-2 py-1 text-sm border rounded bg-gray-200 hover:bg-gray-300">Clear</button> <!-- Button to collapse/expand the editor on mobile --> <button id="toggleEditor" class="px-2 py-1 text-sm border rounded bg-gray-200 hover:bg-gray-300">Hide Editor</button> </div> <textarea id="editor" class="flex-1 w-full h-full resize-none font-mono text-sm leading-tight p-2 border rounded" placeholder="Paste your Mermaid diagram text here..."></textarea> <div id="error" class="text-red-700 whitespace-pre-wrap p-2 hidden overflow-y-auto max-h-24 border border-red-200 bg-red-100 rounded mt-2"></div> </div> <!-- Preview section --> <div id="preview-container" class="relative flex-1 md:w-3/5 bg-white overflow-hidden" style="touch-action: none;"> <div id="inner" class="absolute left-0 top-0"></div> </div> </div> <script> (function() { // Wait for Mermaid to be ready and then initialise it. Use loose // security level so that diagrams with clickable links don’t fail. mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' }); const editor = document.getElementById('editor'); const editorContainer = document.getElementById('editor-container'); const errorBox = document.getElementById('error'); const inner = document.getElementById('inner'); const previewContainer = document.getElementById('preview-container'); const fitButton = document.getElementById('fit'); const clearButton = document.getElementById('clear'); const loadExampleButton = document.getElementById('loadExample'); const toggleEditorButton = document.getElementById('toggleEditor'); // Simple debounce helper. It delays invoking a function until it // hasn’t been called for a certain number of milliseconds. function debounce(fn, delay) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } // Track transformation state for panning and zooming. These values // control the CSS transform applied to the inner wrapper around the SVG. let scale = 1; let offsetX = 0; let offsetY = 0; let lastPointerPositions = new Map(); let lastDistance = 0; let doubleTapTimer = null; // Apply the transform to the inner wrapper based on current state. function updateTransform() { inner.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`; } // Compute distance between two pointer events. function distance(p1, p2) { const dx = p2.clientX - p1.clientX; const dy = p2.clientY - p1.clientY; return Math.hypot(dx, dy); } // Fit the diagram into the preview area: reset translation and scale // such that the SVG fits completely within the container while // preserving aspect ratio. If no SVG exists yet, do nothing. function fitToScreen() { const svg = inner.querySelector('svg'); if (!svg) return; // Temporarily reset transform to measure natural dimensions. inner.style.transform = 'none'; const bbox = svg.getBBox(); const viewWidth = previewContainer.clientWidth; const viewHeight = previewContainer.clientHeight; const scaleX = viewWidth / bbox.width; const scaleY = viewHeight / bbox.height; scale = Math.min(scaleX, scaleY) * 0.95; // leave a small margin offsetX = (viewWidth - bbox.width * scale) / 2 - bbox.x * scale; offsetY = (viewHeight - bbox.height * scale) / 2 - bbox.y * scale; updateTransform(); } // Render the diagram from the text in the editor. Use Mermaid’s // asynchronous API and handle any errors gracefully. After rendering, // call fitToScreen to set an initial scale and translation. async function renderDiagram() { const source = editor.value.trim(); if (!source) { inner.innerHTML = ''; errorBox.classList.add('hidden'); return; } try { const { svg, bindFunctions } = await mermaid.render('mermaid-svg-' + Date.now(), source); inner.innerHTML = svg; // Bind Mermaid interaction functions (e.g. for links) bindFunctions?.(inner); errorBox.classList.add('hidden'); // Without a timeout the browser may calculate sizes before the SVG // has been fully laid out. Delay the fit operation to the next // microtask to ensure dimensions are correct. setTimeout(() => { fitToScreen(); }, 0); } catch (err) { // Show a user‑friendly error message if Mermaid parsing fails. errorBox.textContent = 'Mermaid error:\n' + err.message; errorBox.classList.remove('hidden'); inner.innerHTML = ''; } } const debouncedRender = debounce(renderDiagram, 300); // Editor event: re-render when the text changes. editor.addEventListener('input', debouncedRender); // Fit button resets the view to fit the diagram again. fitButton.addEventListener('click', () => { fitToScreen(); }); // Clear button empties the editor and preview. clearButton.addEventListener('click', () => { editor.value = ''; inner.innerHTML = ''; errorBox.classList.add('hidden'); }); // Load example diagram into the editor. Use one of Mermaid’s // built‑in examples that shows varied layout. loadExampleButton.addEventListener('click', () => { const example = `flowchart TD\n A[Start] --> B{Is it working?}\n B -- Yes --> C[Great]\n B -- No --> D[Check your input]\n C --> E[Keep using it]\n D --> B`; editor.value = example; renderDiagram(); }); // Toggle editor visibility to conserve screen space. When the editor is // hidden the preview expands to fill the space. The button text // updates to reflect the current state. After toggling, refit the // diagram to account for the new container size. toggleEditorButton.addEventListener('click', () => { const isHidden = editorContainer.classList.toggle('hidden'); toggleEditorButton.textContent = isHidden ? 'Show Editor' : 'Hide Editor'; // Adjust preview width on larger screens when the editor is collapsed. if (isHidden) { previewContainer.classList.add('md:w-full'); previewContainer.classList.remove('md:w-3/5'); } else { previewContainer.classList.remove('md:w-full'); previewContainer.classList.add('md:w-3/5'); } // Delay fit to allow the layout to settle setTimeout(() => { fitToScreen(); }, 0); }); // Pointer event handlers for pan and pinch zoom. We track pointers // individually so that multiple touches can be recognised. When two // pointers are down the distance between them determines the scale. previewContainer.addEventListener('pointerdown', (e) => { // Only handle primary button touches or touches; ignore right click. if (e.button !== 0) return; previewContainer.setPointerCapture(e.pointerId); lastPointerPositions.set(e.pointerId, { clientX: e.clientX, clientY: e.clientY }); // Detect double tap for reset: if a pointerdown occurs within 300ms // of the last pointerdown and we have only one active pointer, reset. if (doubleTapTimer && lastPointerPositions.size === 1) { clearTimeout(doubleTapTimer); doubleTapTimer = null; fitToScreen(); } else if (lastPointerPositions.size === 1) { doubleTapTimer = setTimeout(() => { doubleTapTimer = null; }, 300); } }); previewContainer.addEventListener('pointermove', (e) => { if (!lastPointerPositions.has(e.pointerId)) return; const prevPos = lastPointerPositions.get(e.pointerId); const newPos = { clientX: e.clientX, clientY: e.clientY }; lastPointerPositions.set(e.pointerId, newPos); if (lastPointerPositions.size === 1) { // Pan with one pointer const dx = newPos.clientX - prevPos.clientX; const dy = newPos.clientY - prevPos.clientY; offsetX += dx; offsetY += dy; updateTransform(); } else if (lastPointerPositions.size === 2) { // Pinch zoom with two pointers const pointers = Array.from(lastPointerPositions.values()); const newDist = distance(pointers[0], pointers[1]); if (lastDistance === 0) { lastDistance = newDist; return; } const scaleChange = newDist / lastDistance; // Compute the midpoint between the two pointers as the zoom focus. const midX = (pointers[0].clientX + pointers[1].clientX) / 2; const midY = (pointers[0].clientY + pointers[1].clientY) / 2; // Translate world coordinates: when scaling around a point, // adjust offset so that the diagram appears to zoom from the pinch // centre. We invert the transform to get the relative position. const rect = previewContainer.getBoundingClientRect(); const focalX = (midX - rect.left - offsetX) / scale; const focalY = (midY - rect.top - offsetY) / scale; scale *= scaleChange; offsetX = midX - rect.left - focalX * scale; offsetY = midY - rect.top - focalY * scale; updateTransform(); lastDistance = newDist; } }); previewContainer.addEventListener('pointerup', (e) => { previewContainer.releasePointerCapture(e.pointerId); lastPointerPositions.delete(e.pointerId); if (lastPointerPositions.size < 2) { lastDistance = 0; } }); previewContainer.addEventListener('pointercancel', (e) => { previewContainer.releasePointerCapture(e.pointerId); lastPointerPositions.delete(e.pointerId); if (lastPointerPositions.size < 2) { lastDistance = 0; } }); // Mouse wheel zoom for desktop browsers. We treat the mouse wheel // like a pinch around the cursor position. Clamp the scale to a // reasonable range to prevent accidental huge zoom. Use preventDefault // to stop the page from scrolling when zooming. previewContainer.addEventListener('wheel', (e) => { e.preventDefault(); const zoomFactor = Math.exp(-e.deltaY * 0.001); const rect = previewContainer.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const worldX = (x - offsetX) / scale; const worldY = (y - offsetY) / scale; scale *= zoomFactor; scale = Math.max(0.1, Math.min(scale, 10)); offsetX = x - worldX * scale; offsetY = y - worldY * scale; updateTransform(); }, { passive: false }); // Load an initial tiny diagram so that the user sees something when // opening the page. This avoids confusion on a blank screen. editor.value = 'graph TD\n A --> B'; renderDiagram(); })(); </script> </body> </html>
No revisions found. Save the file to create a backup.
Delete
Update App