Browse Source

Theme change fixed. App Stable now

refactor/modularize
RUSHIL AMBARISH KADU 7 months ago
parent
commit
d525e85890
  1. 1
      steps/src/theme.js
  2. 320
      zoomsketch-issue/123.html
  3. 314
      zoomsketch-issue/mermaid_live_editor.html
  4. 159
      zoomsketch-issue/visualizer-flowchart.html

1
steps/src/theme.js

@ -35,6 +35,7 @@ function setTheme(theme) {
appState.vizData,
videoPlayer.duration
);
appState.speedGraphInstance.redraw();
}
}
}

320
zoomsketch-issue/123.html

@ -0,0 +1,320 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Visualizer — Flowchart</title>
<!-- Tailwind CDN (ok for local/doc use) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google font -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<!-- html2canvas for quick export -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" integrity="sha512-BNa5xQe3q3G1tQ7g7ARb8+1JYkK1lqVq3V4b5wE3xD1c7u6hQ1mJx0nQjS6sC6sKz8X0G5yjvVb5G5n7bQq2w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<style>
:root{
--bg: #f8fafc;
--panel: #ffffff;
--muted: #6b7280;
--accent: #0ea5a4;
--accent-2: #2563eb;
--danger: #ef4444;
--card-radius: 12px;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
body { background: var(--bg); color: #0f172a; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }
.page { max-width: 1100px; margin: 36px auto; padding: 24px; }
.card { background: var(--panel); border-radius: var(--card-radius); box-shadow: 0 6px 18px rgba(2,6,23,0.06); padding: 18px; }
.stage-title { font-weight:700; color: #0f172a; }
.node { border-radius:10px; padding:10px 14px; border:1px solid rgba(2,6,23,0.06); box-shadow: 0 2px 6px rgba(2,6,23,0.04); }
.node.small { padding:8px 10px; font-size:0.92rem; }
.node.step { background: #eef2ff; border-left: 4px solid #6366f1; }
.node.process { background: #ecfdf5; border-left: 4px solid #10b981; }
.node.io { background: #fff7ed; border-left: 4px solid #f59e0b; }
.node.terminator { background:#f1f5f9; border-left:4px solid #94a3b8; font-weight:600; }
.node.decision { background:#fff7f7; border-left:4px solid var(--danger); transform: skewX(-10deg); }
.decision .label { display:inline-block; transform: skewX(10deg); }
/* grid layout for stages */
.stages { display:grid; grid-template-columns: 1fr; gap:18px; margin-top:18px; }
@media(min-width:980px){ .stages { grid-template-columns: 1fr 1fr; } }
/* connectors (svg container) */
.connector { display:flex; align-items:center; justify-content:center; margin:12px 0; }
/* legend */
.legend-item { display:flex; gap:8px; align-items:center; }
.legend-swatch { width:14px; height:14px; border-radius:4px; }
/* tooltip */
.tooltip { position:relative; }
.tooltip [data-tip] { position:relative; cursor:help; }
.tooltip [data-tip]:focus { outline: 2px dashed #c7d2fe; outline-offset:4px; }
.tooltip [data-tip]::after {
content: attr(data-tip);
position:absolute;
left:50%;
transform: translateX(-50%);
bottom: calc(100% + 10px);
background:#0b1220;
color:#fff; font-size:13px; padding:8px 10px;
border-radius:8px; white-space:nowrap; display:none; z-index:40;
}
.tooltip [data-tip]:hover::after, .tooltip [data-tip]:focus::after { display:block; }
/* subtle nodes grid inside stage */
.stage-grid { display:flex; flex-direction:column; gap:12px; }
.stage-row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
/* export button */
.export-btn { background:linear-gradient(90deg,var(--accent-2),var(--accent)); color:#fff; padding:8px 12px; border-radius:8px; font-weight:600; cursor:pointer; }
/* print-friendly adjustments */
@media print {
.export-btn, .controls { display:none !important; }
body { background:white; }
.page { margin: 0; box-shadow:none; }
}
</style>
</head>
<body>
<main class="page">
<header class="flex items-start justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl stage-title">Visualizer — Application Flowchart</h1>
<p class="text-sm text-gray-600 mt-1">Clear, staged view of initialization, data loading, and playback sync. Shows the recommended two-stage video loading fix (metadata vs buffering).</p>
</div>
<div class="controls flex gap-3 items-center">
<button id="exportBtn" class="export-btn" title="Export flowchart as PNG">Export PNG</button>
<a href="#" id="printBtn" class="text-sm text-gray-600 hover:underline">Print</a>
</div>
</header>
<!-- Top summary card -->
<section class="card mb-5">
<div class="flex items-start justify-between gap-6">
<div>
<h2 class="text-lg font-semibold">Overview</h2>
<p class="text-sm text-gray-600 mt-1">The visualizer loads JSON radar data and a video file, synchronizes timestamps, creates p5 sketches, then runs a high-precision animation loop to keep video and radar frames in sync.</p>
</div>
<div class="text-sm">
<div class="mb-2"><strong>Key events</strong></div>
<div class="flex flex-col gap-1 text-gray-700">
<div><span class="font-medium">loadedmetadata</span> → duration available (Stage A)</div>
<div><span class="font-medium">canplaythrough</span> → buffering sufficient (Stage B)</div>
<div><span class="font-medium">requestAnimationFrame</span> / <span class="font-medium">requestVideoFrameCallback</span> → high precision playback</div>
</div>
</div>
</div>
</section>
<!-- Main flowchart area -->
<div id="chartRoot" class="card mt-2 p-6">
<div class="stages">
<!-- Stage: Initialization -->
<section aria-labelledby="s1" class="p-4">
<div class="flex items-center justify-between mb-3">
<h3 id="s1" class="text-lg font-semibold">Stage 1 — Initialization</h3>
<span class="text-sm text-gray-500">App boot & cache check</span>
</div>
<div class="stage-grid">
<div class="stage-row">
<div class="node terminator">Start application</div>
<div class="connector" aria-hidden><svg width="48" height="28" viewBox="0 0 48 28"><path d="M2 14 L46 14" stroke="#cbd5e1" stroke-width="2" fill="none"/><polygon points="40,10 46,14 40,18" fill="#cbd5e1" /></svg></div>
<div class="node process">DOMContentLoaded → initTheme() & initDB()</div>
</div>
<div class="stage-row">
<div class="node decision tooltip small" tabindex="0" data-tip="Check localStorage for saved filenames & load from IndexedDB if present.">Check localStorage for session?</div>
</div>
<div class="stage-row">
<div class="node io">If yes: load json & video blobs from IndexedDB</div>
<div class="connector" aria-hidden><svg width="58" height="28" viewBox="0 0 58 28"><path d="M2 14 L56 14" stroke="#cbd5e1" stroke-width="2" fill="none"/><polygon points="50,10 56,14 50,18" fill="#cbd5e1" /></svg></div>
<div class="node step">Call handleFiles([...blobs]) → processFilePipeline()</div>
</div>
</div>
</section>
<!-- Stage: Data load -->
<section aria-labelledby="s2" class="p-4">
<div class="flex items-center justify-between mb-3">
<h3 id="s2" class="text-lg font-semibold">Stage 2 — Data Loading & Finalization</h3>
<span class="text-sm text-gray-500">Two-stage video loading (robust)</span>
</div>
<div class="stage-grid">
<div class="stage-row">
<div class="node step">processFilePipeline() starts</div>
</div>
<div class="stage-row">
<div class="node io tooltip" tabindex="0" data-tip="Show a modal with spinner and progress text. Keep it open until Stage B resolves.">Show loading modal + spinner</div>
<div class="connector" aria-hidden style="margin-left:8px;">
<svg width="56" height="28" viewBox="0 0 56 28"><path d="M2 14 L54 14" stroke="#cbd5e1" stroke-width="2" fill="none"/><polygon points="48,10 54,14 48,18" fill="#cbd5e1" /></svg>
</div>
<div class="node decision tooltip small" tabindex="0" data-tip="If JSON present → parse via web worker; update progress; set appState.vizData.">JSON file present?</div>
</div>
<div class="stage-row">
<div class="node process">Parse JSON in worker → parseVisualizationJson()</div>
</div>
<div class="stage-row">
<div class="node decision tooltip small" tabindex="0" data-tip="If you have a video file, start video loading.">Video file present?</div>
</div>
<div class="stage-row">
<div class="node process tooltip" tabindex="0" data-tip="Stage A: attach loadedmetadata listener. Stage B: attach canplaythrough & progress listeners + fallback timer.">Setup video player & attach listeners</div>
</div>
<!-- Emphasize the two critical sub-stages -->
<div class="stage-row">
<div class="node process" style="flex:1">
<strong>Stage A — Metadata (DATA init)</strong>
<div class="text-sm text-gray-600 mt-1">Wait for <code>loadedmetadata</code> → video.duration available → call <code>finalizeSetup()</code> (create p5 sketches, draw static graphs).</div>
</div>
</div>
<div class="stage-row">
<div class="node process" style="flex:1">
<strong>Stage B — Buffering (UI finalization)</strong>
<div class="text-sm text-gray-600 mt-1">Wait for <code>canplaythrough</code> (or fallback timeout/loadeddata) → stop spinner and <strong>hide modal</strong>.</div>
</div>
</div>
<div class="stage-row">
<div class="node process text-red-700" style="border-left-color: #ef4444;">
Re-sync radar timestamps (CRITICAL) — update each frame.timestampMs relative to videoStartDate & offset
<div class="text-xs text-gray-600 mt-1">function: <code>calculateAndSetOffset()</code> + timestamp normalization before finalize</div>
</div>
</div>
<div class="stage-row">
<div class="node process">Create p5 instances: radarSketch, speedGraphSketch, zoomSketch</div>
<div class="connector" aria-hidden>
<svg width="64" height="28" viewBox="0 0 64 28"><path d="M2 14 L62 14" stroke="#cbd5e1" stroke-width="2" fill="none"/><polygon points="56,10 62,14 56,18" fill="#cbd5e1" /></svg>
</div>
<div class="node terminator">Set initial frame → updateFrame(0)</div>
</div>
<div class="stage-row">
<div class="node io tooltip" tabindex="0" data-tip="Hiding modal indicates UI is ready — play is safe.">Hide loading modal</div>
</div>
</div>
</section>
<!-- Stage: Playback -->
<section aria-labelledby="s3" class="p-4">
<div class="flex items-center justify-between mb-3">
<h3 id="s3" class="text-lg font-semibold">Stage 3 — Playback & Synchronization</h3>
<span class="text-sm text-gray-500">High-precision sync loop</span>
</div>
<div class="stage-grid">
<div class="stage-row">
<div class="node step">User clicks Play</div>
<div class="connector" aria-hidden>
<svg width="50" height="28" viewBox="0 0 50 28"><path d="M2 14 L48 14" stroke="#cbd5e1" stroke-width="2" fill="none"/><polygon points="42,10 48,14 42,18" fill="#cbd5e1" /></svg>
</div>
<div class="node process">Start animationLoop() (requestAnimationFrame) & video.play()</div>
</div>
<div class="stage-row">
<div class="node process">Compute high-precision time & map to radar timestamp</div>
</div>
<div class="stage-row">
<div class="node process">Find matching radar frame index (binary search / lookup)</div>
</div>
<div class="stage-row">
<div class="node io">Update UI overlays, redraw p5 sketches, update timeline</div>
</div>
<div class="stage-row">
<div class="node decision small">Is drift &gt; threshold (e.g. 150 ms)?</div>
</div>
<div class="stage-row">
<div class="node process">If yes → resync video player currentTime and reset master timers</div>
<div class="node step">Continue loop</div>
</div>
</div>
</section>
</div>
<!-- Legend & Troubleshooting -->
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="card p-4">
<h4 class="font-semibold mb-2">Legend</h4>
<div class="flex flex-col gap-2 text-sm text-gray-700">
<div class="legend-item"><span class="legend-swatch" style="background:#eef2ff"></span> <span>Step — UI / user action</span></div>
<div class="legend-item"><span class="legend-swatch" style="background:#ecfdf5"></span> <span>Process — internal data processing</span></div>
<div class="legend-item"><span class="legend-swatch" style="background:#fff7ed"></span> <span>I/O — modal / loading / network</span></div>
<div class="legend-item"><span class="legend-swatch" style="background:#fff7f7"></span> <span>Decision — branching</span></div>
</div>
</div>
<div class="card p-4">
<h4 class="font-semibold mb-2">Troubleshooting checklist</h4>
<ol class="text-sm text-gray-700 ml-4 list-decimal space-y-2">
<li><strong>loadedmetadata</strong> must fire before calling <code>setData(..., videoPlayer.duration)</code>. If graphs are blank, confirm timeline shows duration &gt; 0.</li>
<li>If modal never hides, verify <code>canplaythrough</code> or fallback timer resolves. Some codecs/browsers don't reliably emit canplaythrough — use a timeout fallback.</li>
<li>On cached reload (IndexedDB) the ordering may differ — ensure both paths run the same Stage A → Stage B sequence.</li>
<li>Guard <code>finalizeSetup()</code> with a flag so it only runs once (prevents double initialization).</li>
</ol>
</div>
</div>
<!-- small notes -->
<div class="mt-6 text-xs text-gray-500">
<strong>Notes:</strong> This diagram reflects the robust two-stage loading approach: Stage A for data initialization (metadata) and Stage B for UI/buffering. Use the export button to paste into reports or attach to bug tickets.
</div>
</div>
</main>
<script>
// Export functionality: capture the chartRoot area and download PNG
document.getElementById('exportBtn').addEventListener('click', async () => {
const root = document.getElementById('chartRoot');
try {
const canvas = await html2canvas(root, { scale: 2, useCORS: true, backgroundColor: null });
const url = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = url;
a.download = 'visualizer-flowchart.png';
document.body.appendChild(a);
a.click();
a.remove();
} catch (err) {
alert('Export failed: ' + err.message);
}
});
// Print button
document.getElementById('printBtn').addEventListener('click', (e) => {
e.preventDefault();
window.print();
});
// Add simple keyboard focus support for tooltips (accessibility)
document.querySelectorAll('.tooltip [data-tip]').forEach(el => {
el.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault();
// toggle a hover-like state by focusing then blurring quickly to show ::after
el.focus();
}
});
});
</script>
</body>
</html>

314
zoomsketch-issue/mermaid_live_editor.html

@ -0,0 +1,314 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Mermaid Live Editor</title>
<!-- Tailwind CSS CDN (ok for local use / demo) -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Minimal extra styling */
html,body { height:100%; }
body { background: #f8fafc; }
/* ensure the preview container expands and scrolls for large diagrams */
#preview {
width: 100%;
height: 100%;
min-height: 400px;
overflow: auto;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 1rem;
background: white;
border-radius: 0.5rem;
border: 1px solid rgba(0,0,0,0.06);
}
/* make the rendered svg responsive */
#preview svg {
max-width: 100%;
height: auto;
}
/* small monospace editor */
textarea { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace; }
.error { color: #b91c1c; }
</style>
</head>
<body class="p-6">
<div class="max-w-7xl mx-auto">
<header class="mb-4">
<h1 class="text-2xl font-semibold">Mermaid Live Editor</h1>
<p class="text-sm text-gray-600">Paste Mermaid code on the left, render on the right. Uses Mermaid v11+ (ESM) and html2canvas for PNG export.</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Editor Column -->
<section class="space-y-3">
<div class="flex items-center gap-2">
<label class="text-sm font-medium">Theme</label>
<select id="themeSelect" class="px-2 py-1 border rounded">
<option value="neutral">neutral (default)</option>
<option value="default">default</option>
<option value="dark">dark</option>
<option value="forest">forest</option>
</select>
<label class="flex items-center gap-1 ml-4 text-sm">
<input id="autoRender" type="checkbox" class="border" />
<span>Auto-render</span>
</label>
<button id="renderBtn" class="ml-auto px-3 py-1 bg-indigo-600 text-white rounded hover:bg-indigo-700">Render</button>
<button id="exportBtn" class="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700">Export PNG</button>
</div>
<div class="flex gap-2">
<button id="sampleBtn" class="px-2 py-1 bg-gray-200 rounded">Load sample</button>
<button id="clearBtn" class="px-2 py-1 bg-gray-200 rounded">Clear</button>
<button id="zoomIn" class="px-2 py-1 bg-gray-200 rounded">Zoom +</button>
<button id="zoomOut" class="px-2 py-1 bg-gray-200 rounded">Zoom −</button>
<span id="zoomPct" class="text-sm text-gray-600 ml-2">100%</span>
</div>
<label class="text-xs text-gray-600">Mermaid source</label>
<textarea id="editor" rows="18" class="w-full border rounded p-3 text-sm" spellcheck="false"></textarea>
<div id="errorBox" class="p-2 text-sm hidden rounded border border-red-200 bg-red-50 error"></div>
</section>
<!-- Preview Column -->
<section>
<label class="text-xs text-gray-600">Preview</label>
<div id="preview" class="mt-2">
<div id="renderContainer" style="transform-origin: top left;">
<!-- rendered SVG will be injected here -->
<div id="placeholder" class="text-gray-400 p-6">Rendered diagram will appear here.</div>
</div>
</div>
</section>
</div>
<footer class="mt-4 text-xs text-gray-500">
<div>Mermaid initialized with <code>startOnLoad: false</code> and <code>theme: "neutral"</code>.</div>
<div class="mt-1">If you see an error, the editor will show a clear message. Large diagrams are allowed — use zoom and scroll.</div>
</footer>
</div>
<!-- html2canvas for PNG export -->
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<!-- Mermaid ESM import and main logic -->
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
// Initialize as requested
mermaid.initialize({ startOnLoad: false, theme: "neutral" });
const editor = document.getElementById('editor');
const renderBtn = document.getElementById('renderBtn');
const preview = document.getElementById('preview');
const renderContainer = document.getElementById('renderContainer');
const errorBox = document.getElementById('errorBox');
const themeSelect = document.getElementById('themeSelect');
const autoRender = document.getElementById('autoRender');
const exportBtn = document.getElementById('exportBtn');
const sampleBtn = document.getElementById('sampleBtn');
const clearBtn = document.getElementById('clearBtn');
const zoomIn = document.getElementById('zoomIn');
const zoomOut = document.getElementById('zoomOut');
const zoomPct = document.getElementById('zoomPct');
let zoom = 1;
// sample mermaid content
const sample = `flowchart TD
A[Start] --> B{Is it working?}
B -- Yes --> C[Celebrate]
B -- No --> D[Debug]
D --> B
C --> E[Deploy]
`;
// Helper: set error message or hide
function showError(msg) {
if (!msg) {
errorBox.classList.add('hidden');
errorBox.textContent = '';
} else {
errorBox.classList.remove('hidden');
errorBox.textContent = msg;
}
}
// Render function (robust to different mermaid.render return shapes)
async function renderMermaid() {
const code = editor.value.trim();
if (!code) {
showError("Editor is empty — paste some Mermaid code and click Render.");
clearPreview();
return;
}
showError(null);
// Create unique id for this render
const id = "mermaid-" + Math.random().toString(36).slice(2, 9);
try {
// Try to parse first (mermaid.parse may throw for invalid code)
// Not all mermaid builds expose parse; guard it.
if (typeof mermaid.parse === 'function') {
try {
mermaid.parse(code);
} catch (parseErr) {
// parse() may throw an error object with message
throw new Error(parseErr?.message || String(parseErr));
}
}
// mermaid.render may return either a string or an object/promise depending on build.
const result = await mermaid.render(id, code);
// result could be a string (svg) or an object like { svg: '...', bindFunctions: ... }
let svg;
if (!result) {
throw new Error("mermaid.render returned empty result.");
} else if (typeof result === 'string') {
svg = result;
} else if (typeof result === 'object' && result.svg) {
svg = result.svg;
} else {
// fallback: try to stringify
svg = String(result);
}
// Inject svg into container
renderContainer.innerHTML = svg;
// Remove placeholder if present
const placeholder = document.getElementById('placeholder');
if (placeholder) placeholder.remove();
// Reset zoom transform origin and scale
renderContainer.style.transform = `scale(${zoom})`;
showError(null);
} catch (err) {
// Provide readable error messages
const message = (err && err.message) ? err.message : String(err);
showError("Mermaid parse/render error: " + message);
// Attempt to clear/keep previous preview but indicate error
// Do not wipe the container so user can inspect last diagram if any
}
}
function clearPreview() {
renderContainer.innerHTML = '<div id="placeholder" class="text-gray-400 p-6">Rendered diagram will appear here.</div>';
renderContainer.style.transform = `scale(${zoom})`;
}
// Expose UI interactions
renderBtn.addEventListener('click', () => {
renderMermaid();
});
// Auto-render (debounced)
let autosaveTimer = null;
editor.addEventListener('input', () => {
if (autoRender.checked) {
if (autosaveTimer) clearTimeout(autosaveTimer);
autosaveTimer = setTimeout(() => {
renderMermaid();
}, 450); // gentle debounce
}
});
// Theme switch
themeSelect.addEventListener('change', () => {
const theme = themeSelect.value || 'neutral';
mermaid.initialize({ startOnLoad: false, theme: theme });
// re-render (if code exists)
renderMermaid();
});
// Export PNG using html2canvas
exportBtn.addEventListener('click', async () => {
showError(null);
// render first to ensure latest code is shown
await renderMermaid();
// Wait a tick for DOM to update
await new Promise(r => setTimeout(r, 50));
// html2canvas on the renderContainer (which contains the svg)
try {
// Temporarily set background white for export if theme isn't dark
const originalBg = renderContainer.style.backgroundColor;
renderContainer.style.backgroundColor = getComputedStyle(preview).backgroundColor || '#ffffff';
const canvas = await html2canvas(renderContainer, {
backgroundColor: null,
scale: Math.min(2, window.devicePixelRatio || 1), // better resolution but bounded
useCORS: true,
logging: false
});
// restore
renderContainer.style.backgroundColor = originalBg || '';
const a = document.createElement('a');
a.download = 'mermaid-diagram.png';
a.href = canvas.toDataURL('image/png');
a.click();
} catch (err) {
showError('Export failed: ' + (err?.message || String(err)));
}
});
// sample / clear
sampleBtn.addEventListener('click', () => {
editor.value = sample;
if (autoRender.checked) renderMermaid();
});
clearBtn.addEventListener('click', () => {
editor.value = '';
clearPreview();
});
// zoom controls
function setZoom(newZoom) {
zoom = Math.max(0.2, Math.min(3, newZoom));
renderContainer.style.transform = `scale(${zoom})`;
zoomPct.textContent = Math.round(zoom * 100) + '%';
}
zoomIn.addEventListener('click', () => setZoom(zoom + 0.2));
zoomOut.addEventListener('click', () => setZoom(zoom - 0.2));
// initialize editor with sample
editor.value = sample;
// initial mermaid theme is neutral as requested
themeSelect.value = 'neutral';
setZoom(1);
// optionally auto-render at startup
// (we don't start rendering regardless so developer can control)
// If you want initial render uncomment next line:
// renderMermaid();
// Accessibility: keyboard shortcut Ctrl/Cmd+Enter to render
window.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
renderMermaid();
}
});
// Resize handling for very large diagrams — keep transform origin consistent
new ResizeObserver(() => {
renderContainer.style.transformOrigin = 'top left';
}).observe(preview);
</script>
</body>
</html>

159
zoomsketch-issue/visualizer-flowchart.html

@ -0,0 +1,159 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Visualizer — Interactive Mermaid Flowchart</title>
<!-- Tailwind for quick layout (fine for dev) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- html2canvas for export (no integrity attr) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" crossorigin="anonymous"></script>
<style>
:root {
--bg: #f8fafb;
--card: #ffffff;
--muted: #64748b;
--accent: #2563eb;
--accent2: #0ea5a4;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
body { background: var(--bg); color: #0f172a; -webkit-font-smoothing: antialiased; }
.page { max-width: 1200px; margin: 36px auto; padding: 20px; }
.card { background: var(--card); border-radius: 12px; box-shadow: 0 6px 18px rgba(2,6,23,0.06); padding: 18px; }
</style>
</head>
<body>
<main class="page">
<header class="flex items-start justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl font-semibold">Visualizer — Interactive Flowchart (Mermaid)</h1>
<p class="text-sm text-gray-600 mt-1">Paste Mermaid source → instantly render a flowchart beside it.</p>
</div>
<div class="controls flex items-center gap-3 export-area">
<button id="exportPngBtn">Export PNG</button>
<button id="copyBtn" class="small-btn">Copy Source</button>
<button id="rerenderBtn" class="small-btn">Re-render</button>
<select id="themeSelect" class="small-btn">
<option value="neutral" selected>Neutral</option>
<option value="default">Default</option>
<option value="forest">Forest</option>
<option value="dark">Dark</option>
<option value="solarized">Solarized</option>
</select>
</div>
</header>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Diagram -->
<section class="card">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-medium">Flowchart</h2>
<div class="legend">
<span class="font-semibold">Legend:</span> Stage A = metadata • Stage B = buffering
</div>
</div>
<div id="mermaidContainer" class="bg-white rounded-lg p-4 min-h-[200px]">
<div id="mermaidRender" class="mermaid"></div>
</div>
</section>
<!-- Source -->
<aside class="card">
<h3 class="font-semibold mb-2">Mermaid Source (editable)</h3>
<pre id="mermaidSource" class="mermaid-source" contenteditable="true" spellcheck="false"></pre>
<p class="note mt-2">Press <kbd>Ctrl</kbd>+<kbd>Enter</kbd> or click “Re-render”.</p>
</aside>
</div>
</main>
<!-- Mermaid v11 as ES module -->
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
// ✅ initial diagram (starts with 'flowchart TB')
const initialText = `flowchart TB
A[Start] --> B[Load resources]
B --> C{Ready?}
C -->|Yes| D[Initialize app]
C -->|No| E([Wait])
D --> F([Done])`;
const src = document.getElementById("mermaidSource");
const renderArea = document.getElementById("mermaidRender");
// set source text content
src.textContent = initialText;
async function initMermaid(theme = "neutral") {
mermaid.initialize({
startOnLoad: false,
theme,
securityLevel: "loose",
flowchart: { curve: "linear" }
});
await mermaid.run(); // ensures theme & config loaded
}
async function renderMermaid(text) {
const code = (text || "").trim();
if (!code.startsWith("flowchart") && !code.startsWith("graph")) {
renderArea.innerHTML = `<pre style="color:#ef4444;">❌ No valid diagram type detected. Make sure your text begins with 'flowchart TB' or 'graph LR'.</pre>`;
return;
}
try {
const { svg } = await mermaid.render("mmdDiagram", code);
renderArea.innerHTML = svg;
} catch (err) {
renderArea.innerHTML = `<pre style="color:#ef4444;">Error: ${err.message}</pre>`;
console.error(err);
}
}
// ✅ initialize Mermaid first
await initMermaid();
// ✅ render initial diagram
await renderMermaid(initialText);
// Re-render button
document.getElementById("rerenderBtn").addEventListener("click", () => {
const text = src.innerText.trim();
renderMermaid(text);
});
// Theme selector
document.getElementById("themeSelect").addEventListener("change", async e => {
await initMermaid(e.target.value);
await renderMermaid(src.innerText.trim());
});
// Copy source
document.getElementById("copyBtn").addEventListener("click", async () => {
await navigator.clipboard.writeText(src.innerText.trim());
alert("Copied Mermaid source!");
});
// Export PNG
document.getElementById("exportPngBtn").addEventListener("click", async () => {
const node = document.getElementById("mermaidContainer");
const canvas = await html2canvas(node, { scale: 2, backgroundColor: null });
const a = document.createElement("a");
a.href = canvas.toDataURL("image/png");
a.download = "flowchart.png";
a.click();
});
// Ctrl+Enter shortcut
document.addEventListener("keydown", e => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
renderMermaid(src.innerText.trim());
}
});
</script>
</body>
</html>
Loading…
Cancel
Save