diff --git a/desktop-app/resources/js/preview-worker.js b/desktop-app/resources/js/preview-worker.js
index 47f37ad..a86d1c4 100644
--- a/desktop-app/resources/js/preview-worker.js
+++ b/desktop-app/resources/js/preview-worker.js
@@ -9,6 +9,7 @@ let topojsonIdCounter = 0;
let stlIdCounter = 0;
let plantumlIdCounter = 0;
let d2IdCounter = 0;
+let graphvizIdCounter = 0;
const markedOptions = {
gfm: true,
@@ -348,6 +349,11 @@ function configureMarked() {
return `
`;
}
+ if (language === "graphviz" || language === "dot") {
+ const uniqueId = `graphviz-diagram-worker-${graphvizIdCounter++}`;
+ return `
`;
+ }
+
if (language === "math") {
return `
$$\n${code}\n$$
\n`;
}
@@ -530,6 +536,7 @@ self.onmessage = function(event) {
stlIdCounter = 0;
plantumlIdCounter = 0;
d2IdCounter = 0;
+ graphvizIdCounter = 0;
const result = renderSegmentedMarkdown(data.markdown || "", options);
self.postMessage({
type: "render-result",
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 1849d9b..84b2bfd 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -1084,6 +1084,15 @@ document.addEventListener("DOMContentLoaded", async function () {
return `
`;
}
+ if (language === 'graphviz' || language === 'dot') {
+ const uniqueId = 'graphviz-diagram-' + Math.random().toString(36).substr(2, 9);
+ const escapedCode = code
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+ return `
`;
+ }
+
if (language === 'math') {
return `
$$\n${code}\n$$
\n`;
}
@@ -3154,15 +3163,17 @@ document.addEventListener("DOMContentLoaded", async function () {
try {
const plantumlNodes = queryPreviewRoots(roots, '.plantuml-diagram');
if (plantumlNodes.length > 0) {
- const renderPlantumlNodes = function() {
+ const renderPlantumlNodes = async function() {
if (context.renderId !== previewRenderGeneration) return;
- plantumlNodes.forEach(node => {
+ for (const node of plantumlNodes) {
const container = node.closest('.plantuml-container');
const originalCode = node.getAttribute('data-original-code');
- if (!originalCode) return;
+ if (!originalCode) continue;
const decodedCode = decodeURIComponent(originalCode);
+ if (container) container.classList.add('is-loading');
+
try {
let modifiedCode = decodedCode;
if (!modifiedCode.toLowerCase().includes('backgroundcolor')) {
@@ -3182,34 +3193,43 @@ document.addEventListener("DOMContentLoaded", async function () {
modifiedCode = lines.join('\n');
}
}
+
+ // Try local compile first if in Neutralino
+ if (typeof Neutralino !== 'undefined') {
+ const localSvg = await compileDiagramLocally('plantuml', modifiedCode);
+ if (localSvg) {
+ node.innerHTML = localSvg;
+ if (container) container.classList.remove('is-loading');
+ addPlantumlToolbars();
+ continue;
+ }
+ }
+
const encoded = encodePlantUML(modifiedCode);
const url = 'https://www.plantuml.com/plantuml/svg/' + encoded;
- node.innerHTML = '';
- const img = document.createElement('img');
- img.src = url;
- img.alt = 'PlantUML Diagram';
- img.className = 'plantuml-img';
- img.draggable = false;
- img.addEventListener('dragstart', e => e.preventDefault());
-
- img.onload = function() {
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error();
+ const svgText = await res.text();
+ node.innerHTML = svgText;
+ const svgEl = node.querySelector('svg');
+ if (svgEl) {
+ svgEl.style.maxWidth = '100%';
+ svgEl.style.height = 'auto';
+ }
if (container) container.classList.remove('is-loading');
addPlantumlToolbars();
- };
-
- img.onerror = function() {
+ } catch (err) {
node.innerHTML = `
Offline or unable to connect to PlantUML server
`;
if (container) container.classList.remove('is-loading');
- };
-
- node.appendChild(img);
+ }
} catch (err) {
console.error("PlantUML encoding failed:", err);
node.innerHTML = `
Error encoding diagram: ${escapeHtml(err.message)}
`;
if (container) container.classList.remove('is-loading');
}
- });
+ }
};
if (typeof pako === 'undefined') {
@@ -3234,7 +3254,7 @@ document.addEventListener("DOMContentLoaded", async function () {
try {
const d2Nodes = queryPreviewRoots(roots, '.d2-diagram');
if (d2Nodes.length > 0) {
- const renderSingleD2Node = function(node) {
+ const renderSingleD2Node = async function(node) {
const container = node.closest('.d2-container');
const originalCode = node.getAttribute('data-original-code');
if (!originalCode) return;
@@ -3247,28 +3267,37 @@ document.addEventListener("DOMContentLoaded", async function () {
if (!modifiedCode.includes('style.fill') && !/style\s*:\s*\{[^}]*fill/.test(modifiedCode)) {
modifiedCode = `style.fill: transparent\n${modifiedCode}`;
}
+
+ // Try local compile first if in Neutralino
+ if (typeof Neutralino !== 'undefined') {
+ const localSvg = await compileDiagramLocally('d2', modifiedCode);
+ if (localSvg) {
+ node.innerHTML = localSvg;
+ if (container) container.classList.remove('is-loading');
+ addD2Toolbars();
+ return;
+ }
+ }
+
const encoded = encodeKrokiD2(modifiedCode);
const url = 'https://kroki.io/d2/svg/' + encoded;
- node.innerHTML = '';
- const img = document.createElement('img');
- img.src = url;
- img.alt = 'D2 Diagram';
- img.className = 'd2-img';
- img.draggable = false;
- img.addEventListener('dragstart', e => e.preventDefault());
-
- img.onload = function() {
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error();
+ const svgText = await res.text();
+ node.innerHTML = svgText;
+ const svgEl = node.querySelector('svg');
+ if (svgEl) {
+ svgEl.style.maxWidth = '100%';
+ svgEl.style.height = 'auto';
+ }
if (container) container.classList.remove('is-loading');
addD2Toolbars();
- };
-
- img.onerror = function() {
+ } catch (err) {
node.innerHTML = `
Offline or unable to connect to Kroki server
`;
if (container) container.classList.remove('is-loading');
- };
-
- node.appendChild(img);
+ }
} catch (err) {
console.error("D2 encoding failed:", err);
node.innerHTML = `
Error encoding diagram: ${escapeHtml(err.message)}
`;
@@ -3304,6 +3333,72 @@ document.addEventListener("DOMContentLoaded", async function () {
console.warn("D2 processing failed:", e);
}
+ try {
+ const graphvizNodes = queryPreviewRoots(roots, '.graphviz-diagram');
+ if (graphvizNodes.length > 0) {
+ const renderSingleGraphvizNode = async function(node) {
+ const container = node.closest('.graphviz-container');
+ const originalCode = node.getAttribute('data-original-code');
+ if (!originalCode) return;
+ const decodedCode = decodeURIComponent(originalCode);
+
+ if (container) container.classList.add('is-loading');
+
+ try {
+ const encoded = encodeKrokiD2(decodedCode);
+ const url = 'https://kroki.io/graphviz/svg/' + encoded;
+
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error();
+ const svgText = await res.text();
+ node.innerHTML = svgText;
+ const svgEl = node.querySelector('svg');
+ if (svgEl) {
+ svgEl.style.maxWidth = '100%';
+ svgEl.style.height = 'auto';
+ }
+ if (container) container.classList.remove('is-loading');
+ addGraphvizToolbars();
+ } catch (err) {
+ node.innerHTML = `
Offline or unable to connect to Kroki server
`;
+ if (container) container.classList.remove('is-loading');
+ }
+ } catch (err) {
+ console.error("Graphviz encoding failed:", err);
+ node.innerHTML = `
Error encoding diagram: ${escapeHtml(err.message)}
`;
+ if (container) container.classList.remove('is-loading');
+ }
+ };
+
+ graphvizNodes.forEach(node => {
+ node.renderGraphviz = () => renderSingleGraphvizNode(node);
+ });
+
+ const renderGraphvizNodes = function() {
+ if (context.renderId !== previewRenderGeneration) return;
+ graphvizNodes.forEach(node => node.renderGraphviz());
+ };
+
+ if (typeof pako === 'undefined') {
+ loadScript(CDN.pako).then(function() {
+ if (context.renderId !== previewRenderGeneration) return;
+ renderGraphvizNodes();
+ }).catch(function(e) {
+ console.warn('Failed to load pako for Graphviz:', e);
+ graphvizNodes.forEach(node => {
+ const container = node.closest('.graphviz-container');
+ if (container) container.classList.remove('is-loading');
+ });
+ });
+ } else {
+ renderGraphvizNodes();
+ }
+ }
+ } catch (e) {
+ console.warn("Graphviz processing failed:", e);
+ }
+
const hasMath = /\$\$|\$[^$]|\\\(|\\\[/.test(rawVal || '') || /```math\b/.test(rawVal || '');
if (hasMath) {
const typesetTargets = roots.filter(function(root) {
@@ -5471,6 +5566,1035 @@ document.addEventListener("DOMContentLoaded", async function () {
modal.addEventListener('keydown', onKey);
}
+ function getCleanCode(templateCode) {
+ if (!templateCode) return '';
+ let clean = templateCode.trim();
+ if (clean.startsWith('```')) {
+ const firstNewLine = clean.indexOf('\n');
+ if (firstNewLine !== -1) {
+ clean = clean.substring(firstNewLine + 1);
+ }
+ }
+ if (clean.endsWith('```')) {
+ clean = clean.substring(0, clean.length - 3).trim();
+ }
+ return clean;
+ }
+
+ function getDiagramApiUrl(template, theme) {
+ if (typeof pako === 'undefined') {
+ return null;
+ }
+ try {
+ const cleanCode = getCleanCode(template.code);
+ if (template.category === 'Mermaid') {
+ const obj = {
+ code: cleanCode,
+ mermaid: {
+ theme: theme === 'dark' ? 'dark' : 'default'
+ }
+ };
+ const json = JSON.stringify(obj);
+ const encoded = encodeKrokiD2(json);
+ return `https://mermaid.ink/svg/pako:${encoded}`;
+ } else if (template.category === 'PlantUML') {
+ const encoded = encodePlantUML(cleanCode);
+ return `https://www.plantuml.com/plantuml/svg/${encoded}`;
+ } else if (template.category === 'D2') {
+ const encoded = encodeKrokiD2(cleanCode);
+ const themeParam = theme === 'dark' ? '?theme=200' : '';
+ return `https://kroki.io/d2/svg/${encoded}${themeParam}`;
+ } else {
+ let engine = '';
+ switch (template.category) {
+ case 'Graphviz': engine = 'graphviz'; break;
+ case 'Vega-Lite': engine = 'vegalite'; break;
+ case 'ABC Notation': engine = 'abc'; break;
+ case 'WaveDrom': engine = 'wavedrom'; break;
+ case 'Markmap': engine = 'markmap'; break;
+ default: engine = template.category.toLowerCase().replace(/\s+/g, '');
+ }
+ const encoded = encodeKrokiD2(cleanCode);
+ return `https://kroki.io/${engine}/svg/${encoded}`;
+ }
+ } catch (e) {
+ console.warn('Failed to encode diagram for URL:', e);
+ return null;
+ }
+ }
+
+ async function compileDiagramLocally(engine, code) {
+ if (typeof Neutralino === 'undefined') return null;
+ try {
+ if (engine === 'd2') {
+ const result = await Neutralino.os.execCommand('d2 - -', { stdIn: code });
+ if (result && result.exitCode === 0 && result.stdOut) {
+ return result.stdOut;
+ }
+ } else if (engine === 'plantuml') {
+ try {
+ const result = await Neutralino.os.execCommand('plantuml -pipe -tsvg', { stdIn: code });
+ if (result && result.exitCode === 0 && result.stdOut) {
+ return result.stdOut;
+ }
+ } catch (e) {
+ const result = await Neutralino.os.execCommand('java -jar plantuml.jar -pipe -tsvg', { stdIn: code });
+ if (result && result.exitCode === 0 && result.stdOut) {
+ return result.stdOut;
+ }
+ }
+ }
+ } catch (e) {
+ console.warn(`Local execution for ${engine} failed:`, e);
+ }
+ return null;
+ }
+
+ async function fetchDiagramPreview(apiUrl) {
+ if (typeof caches === 'undefined') {
+ const response = await fetch(apiUrl);
+ if (!response.ok) throw new Error('Failed to fetch');
+ return await response.text();
+ }
+ const cache = await caches.open('diagram-previews');
+ const cachedResponse = await cache.match(apiUrl);
+ if (cachedResponse) {
+ return await cachedResponse.text();
+ }
+ const response = await fetch(apiUrl);
+ if (!response.ok) throw new Error('Failed to fetch');
+ await cache.put(apiUrl, response.clone());
+ return await response.text();
+ }
+
+ async function getOrRenderDiagramPreview(template, theme, callback) {
+ const cleanCode = getCleanCode(template.code);
+
+ if (typeof Neutralino !== 'undefined') {
+ if (template.category === 'D2') {
+ const localSvg = await compileDiagramLocally('d2', cleanCode);
+ if (localSvg) {
+ callback(localSvg);
+ return;
+ }
+ } else if (template.category === 'PlantUML') {
+ const localSvg = await compileDiagramLocally('plantuml', cleanCode);
+ if (localSvg) {
+ callback(localSvg);
+ return;
+ }
+ }
+ }
+
+ const apiUrl = getDiagramApiUrl(template, theme);
+ if (!apiUrl) {
+ callback(null);
+ return;
+ }
+
+ try {
+ const svgText = await fetchDiagramPreview(apiUrl);
+ callback(svgText);
+ } catch (e) {
+ callback(null);
+ }
+ }
+
+ async function openDiagramModal() {
+ const modal = document.getElementById('diagram-modal');
+ const sidebar = modal.querySelector('.diagram-modal-sidebar');
+ const grid = document.getElementById('diagram-modal-grid');
+ const emptyMessage = document.getElementById('diagram-modal-empty');
+ const searchInput = document.getElementById('diagram-modal-search');
+ const previewContainer = document.getElementById('diagram-modal-preview');
+ const previewCode = document.getElementById('diagram-modal-preview-code');
+ const confirmBtn = document.getElementById('diagram-modal-insert');
+ const cancelBtn = document.getElementById('diagram-modal-cancel');
+ const closeBtn = document.getElementById('diagram-modal-close');
+
+ if (!modal || !sidebar || !grid || !emptyMessage || !searchInput || !previewContainer || !previewCode || !confirmBtn || !cancelBtn || !closeBtn) return;
+
+ if (typeof pako === 'undefined') {
+ try {
+ await loadScript(CDN.pako);
+ } catch (e) {
+ console.warn('Failed to load pako library for diagram previews:', e);
+ }
+ }
+
+ const start = markdownEditor.selectionStart;
+ const end = markdownEditor.selectionEnd;
+ modal.style.display = 'flex';
+
+ // Clear and reset state
+ searchInput.value = '';
+ sidebar.textContent = '';
+ grid.textContent = '';
+ previewContainer.textContent = '';
+ if (previewCode) previewCode.value = '';
+ confirmBtn.disabled = true;
+
+ const categories = [
+ 'Mermaid',
+ 'PlantUML',
+ 'Graphviz',
+ 'D2',
+ 'Vega-Lite',
+ 'ABC Notation',
+ 'WaveDrom',
+ 'Markmap'
+ ];
+
+ const svgFlowchart = `
`;
+ const svgMermaidFlowchartLR = `
`;
+ const svgSequence = `
`;
+ const svgEr = `
`;
+ const svgClass = `
`;
+ const svgState = `
`;
+ const svgGantt = `
`;
+ const svgPie = `
`;
+ const svgGitGraph = `
`;
+ const svgJourney = `
`;
+ const svgMermaidMindmap = `
`;
+ const svgMermaidQuadrant = `
`;
+ const svgMermaidXy = `
`;
+ const svgMermaidRequirement = `
`;
+ const svgMermaidC4 = `
`;
+ const svgMermaidSankey = `
`;
+ const svgMermaidTimeline = `
`;
+
+ // PlantUML distinct styling (Yellowish actor/notes backgrounds, red outlines, retro styling)
+ const svgPlantUmlSequence = `
`;
+ const svgPlantUmlUseCase = `
`;
+ const svgPlantUmlActivity = `
`;
+ const svgPlantUmlClass = `
`;
+ const svgPlantUmlState = `
`;
+ const svgPlantUmlComponent = `
`;
+ const svgPlantUmlObject = `
`;
+ const svgPlantUmlDeployment = `
`;
+ const svgPlantUmlTiming = `
`;
+ const svgPlantUmlNetwork = `
`;
+ const svgPlantUmlMindmap = `
`;
+ const svgPlantUmlWbs = `
`;
+ const svgPlantUmlJson = `
`;
+
+ // Graphviz (Aesthetic: beige/brown, thin strokes, distinct classic nodes)
+ const svgGraphvizDigraph = `
`;
+ const svgGraphvizTree = `
`;
+ const svgGraphvizStruct = `
`;
+ const svgGraphvizFsm = `
`;
+ const svgGraphvizNetwork = `
`;
+ const svgGraphvizSubgraph = `
`;
+ const svgGraphvizEr = `
`;
+
+ // D2 (Aesthetic: monospace font, bold slate outlines, soft purple/blue/green boxes)
+ const svgD2Flow = `
`;
+ const svgD2Arch = `
`;
+ const svgD2Sequence = `
`;
+ const svgD2Erd = `
`;
+ const svgD2Grid = `
`;
+
+ // Vega-Lite (Aesthetic: statistical charts)
+ const svgVegaBar = `
`;
+ const svgVegaLine = `
`;
+ const svgVegaScatter = `
`;
+ const svgVegaArea = `
`;
+ const svgVegaStackedBar = `
`;
+
+ // ABC Notation (Aesthetic: musical notation stave and notes)
+ const svgAbcMelody = `
`;
+ const svgAbcDuet = `
`;
+ const svgAbcLyric = `
`;
+ const svgAbcChords = `
`;
+
+ // WaveDrom (Aesthetic: digital waveform timing lines)
+ const svgWaveTiming = `
`;
+ const svgWaveCounter = `
`;
+ const svgWaveBus = `
`;
+ const svgWaveReset = `
`;
+
+ // Markmap (Aesthetic: colorful mindmaps, curved connection paths)
+ const svgMarkmapMindmap = `
`;
+ const svgMarkmapRoadmap = `
`;
+ const svgMarkmapStudy = `
`;
+ const svgMarkmapStack = `
`;
+
+ // D2 additional (Aesthetic: monospace font, bold slate outlines, soft purple/blue/green boxes)
+ const svgD2Mindmap = `
`;
+ const svgD2Class = `
`;
+ const svgD2Venn = `
`;
+
+ // Vega-Lite additional (Aesthetic: statistical charts)
+ const svgVegaPie = `
`;
+ const svgVegaHeatmap = `
`;
+ const svgVegaBubble = `
`;
+
+ // ABC Notation additional (Aesthetic: musical notation stave and notes)
+ const svgAbcPolyphony = `
`;
+ const svgAbcKeySignature = `
`;
+
+ // WaveDrom additional (Aesthetic: digital waveform timing lines)
+ const svgWaveGlitches = `
`;
+ const svgWaveComplexBus = `
`;
+
+ // Markmap additional (Aesthetic: colorful mindmaps, checklist notation)
+ const svgMarkmapChecklist = `
`;
+ const svgMarkmapCode = `
`;
+
+ const templates = [
+ // Mermaid
+ {
+ id: 'mermaid-flowchart-td',
+ category: 'Mermaid',
+ title: 'Flowchart (TD)',
+ label: 'Flowchart (Top-Down)',
+ svg: svgFlowchart,
+ code: '```mermaid\ngraph TD\n Start --> End\n```\n'
+ },
+ {
+ id: 'mermaid-flowchart-lr',
+ category: 'Mermaid',
+ title: 'Flowchart (LR)',
+ label: 'Flowchart (Left-Right)',
+ svg: svgMermaidFlowchartLR,
+ code: '```mermaid\ngraph LR\n Left --> Right\n```\n'
+ },
+ {
+ id: 'mermaid-sequence',
+ category: 'Mermaid',
+ title: 'Sequence preview',
+ label: 'Sequence Diagram',
+ svg: svgSequence,
+ code: '```mermaid\nsequenceDiagram\n Alice->>Bob: Hello Bob, how are you?\n Bob-->>Alice: Jolly good!\n```\n'
+ },
+ {
+ id: 'mermaid-er',
+ category: 'Mermaid',
+ title: 'ER preview',
+ label: 'ER Diagram',
+ svg: svgEr,
+ code: '```mermaid\nerDiagram\n CUSTOMER ||--o{ ORDER : places\n```\n'
+ },
+ {
+ id: 'mermaid-class',
+ category: 'Mermaid',
+ title: 'Class Diagram',
+ label: 'Class Diagram',
+ svg: svgClass,
+ code: '```mermaid\nclassDiagram\n Animal <|-- Duck\n class Animal {\n +String name\n +makeSound()\n }\n```\n'
+ },
+ {
+ id: 'mermaid-state',
+ category: 'Mermaid',
+ title: 'State Diagram',
+ label: 'State Diagram',
+ svg: svgState,
+ code: '```mermaid\nstateDiagram-v2\n [*] --> Active\n Active --> [*]\n```\n'
+ },
+ {
+ id: 'mermaid-gantt',
+ category: 'Mermaid',
+ title: 'Gantt Chart',
+ label: 'Gantt Chart',
+ svg: svgGantt,
+ code: '```mermaid\ngantt\n title A Gantt Diagram\n section Section\n A task :a1, 2026-06-23, 30d\n```\n'
+ },
+ {
+ id: 'mermaid-pie',
+ category: 'Mermaid',
+ title: 'Pie Chart',
+ label: 'Pie Chart',
+ svg: svgPie,
+ code: '```mermaid\npie title Pets owned by staff\n "Dogs" : 386\n "Cats" : 85\n```\n'
+ },
+ {
+ id: 'mermaid-git',
+ category: 'Mermaid',
+ title: 'Git Graph',
+ label: 'Git Graph',
+ svg: svgGitGraph,
+ code: '```mermaid\ngitGraph\n commit\n branch hotfix\n checkout hotfix\n commit\n checkout main\n merge hotfix\n```\n'
+ },
+ {
+ id: 'mermaid-journey',
+ category: 'Mermaid',
+ title: 'User Journey',
+ label: 'User Journey',
+ svg: svgJourney,
+ code: '```mermaid\njourney\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n```\n'
+ },
+ {
+ id: 'mermaid-mindmap',
+ category: 'Mermaid',
+ title: 'Mindmap',
+ label: 'Mindmap Diagram',
+ svg: svgMermaidMindmap,
+ code: '```mermaid\nmindmap\n root((Goal))\n Topic 1\n Subtopic 1\n Topic 2\n```\n'
+ },
+ {
+ id: 'mermaid-quadrant',
+ category: 'Mermaid',
+ title: 'Quadrant Chart',
+ label: 'Quadrant Chart',
+ svg: svgMermaidQuadrant,
+ code: '```mermaid\nquadrantChart\n title Reach and Engagement\n x-axis Low Reach --> High Reach\n y-axis Low Engagement --> High Engagement\n quadrant-1 We should expand\n quadrant-2 Need to promote\n quadrant-3 Re-evaluate\n quadrant-4 Keep improving\n Campaign A: [0.3, 0.6]\n Campaign B: [0.45, 0.23]\n```\n'
+ },
+ {
+ id: 'mermaid-xy',
+ category: 'Mermaid',
+ title: 'XY Chart',
+ label: 'XY Chart',
+ svg: svgMermaidXy,
+ code: '```mermaid\nxychart-beta\n title "Sales Revenue"\n x-axis [jan, feb, mar, apr, may]\n y-axis "Revenue ($)" 0 --> 1000\n bar [500, 600, 700, 800, 900]\n line [480, 580, 710, 820, 910]\n```\n'
+ },
+ {
+ id: 'mermaid-requirement',
+ category: 'Mermaid',
+ title: 'Requirements',
+ label: 'Requirements Diagram',
+ svg: svgMermaidRequirement,
+ code: '```mermaid\nrequirementDiagram\n requirement test_req {\n id: 1\n text: "Verify system response time."\n risk: medium\n verifymethod: test\n }\n element test_case {\n type: "simulation"\n }\n test_case - satisfies -> test_req\n```\n'
+ },
+ {
+ id: 'mermaid-c4',
+ category: 'Mermaid',
+ title: 'C4 Container',
+ label: 'C4 Container Diagram',
+ svg: svgMermaidC4,
+ code: '```mermaid\nC4Context\n title System Context for Internet Banking\n Enterprise_Boundary(b1, "Banking") {\n System(banking_sys, "Banking System", "Stores accounts")\n }\n```\n'
+ },
+ {
+ id: 'mermaid-sankey',
+ category: 'Mermaid',
+ title: 'Sankey Chart',
+ label: 'Sankey Flow Chart',
+ svg: svgMermaidSankey,
+ code: '```mermaid\nsankey-beta\n source,target,value\n Electricity,Grid,120\n Gas,Grid,80\n```\n'
+ },
+ {
+ id: 'mermaid-timeline',
+ category: 'Mermaid',
+ title: 'Timeline',
+ label: 'Timeline Diagram',
+ svg: svgMermaidTimeline,
+ code: '```mermaid\ntimeline\n title History of Web\n 2000 : HTML4\n 2014 : HTML5\n```\n'
+ },
+
+ // PlantUML
+ {
+ id: 'plantuml-sequence',
+ category: 'PlantUML',
+ title: 'Sequence Diagram',
+ label: 'Sequence Diagram',
+ svg: svgPlantUmlSequence,
+ code: '```plantuml\n@startuml\nAlice -> Bob: Authentication Request\nBob --> Alice: Authentication Response\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-usecase',
+ category: 'PlantUML',
+ title: 'Use Case Diagram',
+ label: 'Use Case Diagram',
+ svg: svgPlantUmlUseCase,
+ code: '```plantuml\n@startuml\nleft to right direction\nactor Guest\nrectangle Hotel {\n usecase "Book Room" as UC1\n}\nGuest --> UC1\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-activity',
+ category: 'PlantUML',
+ title: 'Activity Diagram',
+ label: 'Activity Diagram',
+ svg: svgPlantUmlActivity,
+ code: '```plantuml\n@startuml\n:Start;\n:Hello World;\n:End;\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-class',
+ category: 'PlantUML',
+ title: 'Class Diagram',
+ label: 'Class Diagram',
+ svg: svgPlantUmlClass,
+ code: '```plantuml\n@startuml\nclass Dummy {\n -field1\n #field2\n ~method1()\n +method2()\n}\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-state',
+ category: 'PlantUML',
+ title: 'State Diagram',
+ label: 'State Diagram',
+ svg: svgPlantUmlState,
+ code: '```plantuml\n@startuml\n[*] --> State1\nState1 --> State2 : Transition\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-component',
+ category: 'PlantUML',
+ title: 'Component Diagram',
+ label: 'Component Diagram',
+ svg: svgPlantUmlComponent,
+ code: '```plantuml\n@startuml\n[Web GUI] --> [App Service] : JSON HTTP\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-object',
+ category: 'PlantUML',
+ title: 'Object Diagram',
+ label: 'Object Instances',
+ svg: svgPlantUmlObject,
+ code: '```plantuml\n@startuml\nobject user1 {\n name = "Alice"\n role = "Admin"\n}\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-deployment',
+ category: 'PlantUML',
+ title: 'Deployment',
+ label: 'Deployment Nodes',
+ svg: svgPlantUmlDeployment,
+ code: '```plantuml\n@startuml\nnode "Application Server" {\n component [Web Application]\n}\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-timing',
+ category: 'PlantUML',
+ title: 'Timing Diagram',
+ label: 'Timing Signal Wave',
+ svg: svgPlantUmlTiming,
+ code: '```plantuml\n@startuml\nrobust "WebState" as WS\n@0\nWS is Idle\n@100\nWS is Busy\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-network',
+ category: 'PlantUML',
+ title: 'Network (nwdiag)',
+ label: 'Network Map',
+ svg: svgPlantUmlNetwork,
+ code: '```plantuml\n@startuml\nnwdiag {\n network dmz {\n web [address = "192.168.1.1"];\n db [address = "192.168.1.2"];\n }\n}\n@enduml\n```\n'
+ },
+ {
+ id: 'plantuml-mindmap',
+ category: 'PlantUML',
+ title: 'Mindmap',
+ label: 'Mindmap Outline',
+ svg: svgPlantUmlMindmap,
+ code: '```plantuml\n@startmindmap\n* Idea\n** Topic A\n** Topic B\n@endmindmap\n```\n'
+ },
+ {
+ id: 'plantuml-wbs',
+ category: 'PlantUML',
+ title: 'WBS Hierarchy',
+ label: 'Work Breakdown',
+ svg: svgPlantUmlWbs,
+ code: '```plantuml\n@startwbs\n* Project\n** Phase 1\n** Phase 2\n@endwbs\n```\n'
+ },
+ {
+ id: 'plantuml-json',
+ category: 'PlantUML',
+ title: 'JSON Viewer',
+ label: 'JSON Document',
+ svg: svgPlantUmlJson,
+ code: '```plantuml\n@startjson\n{\n "name": "Widget",\n "count": 42,\n "active": true\n}\n@endjson\n```\n'
+ },
+
+ // Graphviz
+ {
+ id: 'graphviz-digraph',
+ category: 'Graphviz',
+ title: 'Directed Graph',
+ label: 'Directed Graph',
+ svg: svgGraphvizDigraph,
+ code: '```graphviz\ndigraph G {\n Hello -> World\n}\n```\n'
+ },
+ {
+ id: 'graphviz-tree',
+ category: 'Graphviz',
+ title: 'Hierarchy Tree',
+ label: 'Hierarchy Tree',
+ svg: svgGraphvizTree,
+ code: '```graphviz\ndigraph Tree {\n node [shape=circle];\n Parent -> Left;\n Parent -> Right;\n}\n```\n'
+ },
+ {
+ id: 'graphviz-struct',
+ category: 'Graphviz',
+ title: 'Record Struct',
+ label: 'Record Structure',
+ svg: svgGraphvizStruct,
+ code: '```graphviz\ndigraph G {\n node [shape=record];\n struct1 [label="
left| mid| right"];\n}\n```\n'
+ },
+ {
+ id: 'graphviz-fsm',
+ category: 'Graphviz',
+ title: 'FSM Diagram',
+ label: 'Finite State Machine',
+ svg: svgGraphvizFsm,
+ code: '```graphviz\ndigraph FSM {\n rankdir=LR;\n S1 -> S2 [label="Input"];\n}\n```\n'
+ },
+ {
+ id: 'graphviz-network',
+ category: 'Graphviz',
+ title: 'Network Topology',
+ label: 'Network Topology',
+ svg: svgGraphvizNetwork,
+ code: '```graphviz\ngraph Net {\n Router -- Switch;\n Switch -- Client1;\n Switch -- Client2;\n}\n```\n'
+ },
+ {
+ id: 'graphviz-subgraph',
+ category: 'Graphviz',
+ title: 'Cluster Subgraph',
+ label: 'Grouped Nodes',
+ svg: svgGraphvizSubgraph,
+ code: '```graphviz\ndigraph G {\n subgraph cluster_0 {\n label="Group A";\n A -> B;\n }\n B -> C;\n}\n```\n'
+ },
+ {
+ id: 'graphviz-er',
+ category: 'Graphviz',
+ title: 'ER Diagram',
+ label: 'ER (Graphviz style)',
+ svg: svgGraphvizEr,
+ code: '```graphviz\ndigraph ER {\n node [shape=box]; Entity;\n node [shape=diamond]; Rel;\n Entity -> Rel;\n}\n```\n'
+ },
+
+ // D2
+ {
+ id: 'd2-flow',
+ category: 'D2',
+ title: 'Simple Flow',
+ label: 'Simple Flow',
+ svg: svgD2Flow,
+ code: '```d2\nx -> y: hello world\n```\n'
+ },
+ {
+ id: 'd2-arch',
+ category: 'D2',
+ title: 'Architecture Diagram',
+ label: 'Architecture Diagram',
+ svg: svgD2Arch,
+ code: '```d2\ncloud platform {\n app: Application Server\n db: PostgreSQL Database\n app -> db\n}\n```\n'
+ },
+ {
+ id: 'd2-sequence',
+ category: 'D2',
+ title: 'Sequence Diagram',
+ label: 'Sequence Diagram',
+ svg: svgD2Sequence,
+ code: '```d2\nclient -> server: Get User Profile\nserver -> client: Profile Data\n```\n'
+ },
+ {
+ id: 'd2-erd',
+ category: 'D2',
+ title: 'ERD Table',
+ label: 'Entity Relationship',
+ svg: svgD2Erd,
+ code: '```d2\nusers: {\n id: int {constraint: primary_key}\n name: string\n}\nposts: {\n id: int {constraint: primary_key}\n user_id: int\n}\nusers.id -> posts.user_id\n```\n'
+ },
+ {
+ id: 'd2-grid',
+ category: 'D2',
+ title: 'Grid Layout',
+ label: 'Grid Layout',
+ svg: svgD2Grid,
+ code: '```d2\ngrid-demo: {\n style.layout: grid\n Box 1\n Box 2\n}\n```\n'
+ },
+ {
+ id: 'd2-mindmap',
+ category: 'D2',
+ title: 'Mindmap',
+ label: 'Mindmap Outline',
+ svg: svgD2Mindmap,
+ code: '```d2\nmindmap-demo: {\n shape: mindmap\n Root\n Topic A\n Topic B\n}\n```\n'
+ },
+ {
+ id: 'd2-class',
+ category: 'D2',
+ title: 'Class Diagram',
+ label: 'Object Types',
+ svg: svgD2Class,
+ code: '```d2\nProduct: {\n sku: string\n price: float\n}\n```\n'
+ },
+ {
+ id: 'd2-venn',
+ category: 'D2',
+ title: 'Venn Diagram',
+ label: 'Overlap Set',
+ svg: svgD2Venn,
+ code: '```d2\nvenn-demo: {\n shape: venn\n A\n B\n}\n```\n'
+ },
+
+ // Vega-Lite
+ {
+ id: 'vega-bar',
+ category: 'Vega-Lite',
+ title: 'Bar Chart',
+ label: 'Bar Chart',
+ svg: svgVegaBar,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "data": {\n "values": [\n {"a": "A", "b": 28}, {"a": "B", "b": 55}, {"a": "C", "b": 43}\n ]\n },\n "mark": "bar",\n "encoding": {\n "x": {"field": "a", "type": "nominal"},\n "y": {"field": "b", "type": "quantitative"}\n }\n}\n```\n'
+ },
+ {
+ id: 'vega-line',
+ category: 'Vega-Lite',
+ title: 'Line Chart',
+ label: 'Line Chart',
+ svg: svgVegaLine,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "data": {\n "values": [\n {"x": 1, "y": 10}, {"x": 2, "y": 15}, {"x": 3, "y": 13}\n ]\n },\n "mark": "line",\n "encoding": {\n "x": {"field": "x", "type": "quantitative"},\n "y": {"field": "y", "type": "quantitative"}\n }\n}\n```\n'
+ },
+ {
+ id: 'vega-scatter',
+ category: 'Vega-Lite',
+ title: 'Scatter Plot',
+ label: 'Scatter Plot',
+ svg: svgVegaScatter,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "data": {\n "values": [\n {"x": 1, "y": 1.5}, {"x": 2, "y": 2.5}, {"x": 3, "y": 1.0}\n ]\n },\n "mark": "point",\n "encoding": {\n "x": {"field": "x", "type": "quantitative"},\n "y": {"field": "y", "type": "quantitative"}\n }\n}\n```\n'
+ },
+ {
+ id: 'vega-area',
+ category: 'Vega-Lite',
+ title: 'Area Chart',
+ label: 'Area Chart',
+ svg: svgVegaArea,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "data": {\n "values": [\n {"x": 1, "y": 10}, {"x": 2, "y": 15}, {"x": 3, "y": 13}\n ]\n },\n "mark": "area",\n "encoding": {\n "x": {"field": "x", "type": "quantitative"},\n "y": {"field": "y", "type": "quantitative"}\n }\n}\n```\n'
+ },
+ {
+ id: 'vega-stacked-bar',
+ category: 'Vega-Lite',
+ title: 'Stacked Bar',
+ label: 'Stacked Bar Chart',
+ svg: svgVegaStackedBar,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "data": {\n "values": [\n {"x": "A", "y": 10, "group": "one"},\n {"x": "A", "y": 15, "group": "two"},\n {"x": "B", "y": 20, "group": "one"},\n {"x": "B", "y": 5, "group": "two"}\n ]\n },\n "mark": "bar",\n "encoding": {\n "x": {"field": "x", "type": "nominal"},\n "y": {"field": "y", "type": "quantitative"},\n "color": {"field": "group", "type": "nominal"}\n }\n}\n```\n'
+ },
+ {
+ id: 'vega-pie',
+ category: 'Vega-Lite',
+ title: 'Pie Chart',
+ label: 'Pie Chart',
+ svg: svgVegaPie,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "description": "A simple pie chart.",\n "data": {\n "values": [\n {"category": 1, "value": 4},\n {"category": 2, "value": 6},\n {"category": 3, "value": 10}\n ]\n },\n "mark": "arc",\n "encoding": {\n "theta": {"field": "value", "type": "quantitative"},\n "color": {"field": "category", "type": "nominal"}\n }\n}\n```\n'
+ },
+ {
+ id: 'vega-heatmap',
+ category: 'Vega-Lite',
+ title: 'Heatmap',
+ label: 'Heatmap Matrix',
+ svg: svgVegaHeatmap,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "data": {\n "values": [\n {"x": 1, "y": 1, "z": 10},\n {"x": 1, "y": 2, "z": 20},\n {"x": 2, "y": 1, "z": 30},\n {"x": 2, "y": 2, "z": 40}\n ]\n },\n "mark": "rect",\n "encoding": {\n "x": {"field": "x", "type": "ordinal"},\n "y": {"field": "y", "type": "ordinal"},\n "color": {"field": "z", "type": "quantitative"}\n }\n}\n```\n'
+ },
+ {
+ id: 'vega-bubble',
+ category: 'Vega-Lite',
+ title: 'Bubble Plot',
+ label: 'Bubble Plot',
+ svg: svgVegaBubble,
+ code: '```vega-lite\n{\n "$schema": "https://vega.github.io/schema/vega-lite/v5.json",\n "data": {\n "values": [\n {"x": 1, "y": 10, "size": 100},\n {"x": 2, "y": 20, "size": 400},\n {"x": 3, "y": 15, "size": 200}\n ]\n },\n "mark": "point",\n "encoding": {\n "x": {"field": "x", "type": "quantitative"},\n "y": {"field": "y", "type": "quantitative"},\n "size": {"field": "size", "type": "quantitative"}\n }\n}\n```\n'
+ },
+
+ // ABC Notation
+ {
+ id: 'abc-melody',
+ category: 'ABC Notation',
+ title: 'Simple Tune',
+ label: 'Simple Tune',
+ svg: svgAbcMelody,
+ code: '```abc\nX: 1\nT: Simple Scale\nM: 4/4\nK: C\nC D E F | G A B c |\n```\n'
+ },
+ {
+ id: 'abc-duet',
+ category: 'ABC Notation',
+ title: 'Duet Accord',
+ label: 'Duet Accord',
+ svg: svgAbcDuet,
+ code: '```abc\nX: 2\nT: Simple Duet\nM: 4/4\nK: C\nV:1\nC D E F | G A B c |\nV:2\nE F G A | B c d e |\n```\n'
+ },
+ {
+ id: 'abc-lyric',
+ category: 'ABC Notation',
+ title: 'Folk & Lyrics',
+ label: 'Lyrics Song',
+ svg: svgAbcLyric,
+ code: '```abc\nX: 3\nT: Folk Song\nM: 4/4\nK: C\nC D E C | E G G2 |\nw: Do Re Mi Do | Mi Sol Sol\n```\n'
+ },
+ {
+ id: 'abc-chords',
+ category: 'ABC Notation',
+ title: 'Chords Strum',
+ label: 'Guitar Chords',
+ svg: svgAbcChords,
+ code: '```abc\nX: 4\nT: Chords Strum\nM: 4/4\nK: C\n"C"C D E F | "G"G A B c |\n```\n'
+ },
+ {
+ id: 'abc-polyphony',
+ category: 'ABC Notation',
+ title: 'Polyphony Voices',
+ label: 'Multi-Voice Harmony',
+ svg: svgAbcPolyphony,
+ code: '```abc\nX: 5\nT: Polyphonic Harmony\nM: 4/4\nK: C\n%%score V1 V2\nV:1 clef=treble\nC2 E2 G2 c2 | e4 z4 |\nV:2 clef=bass\nC,,4 E,,4 | G,,4 C,,4 |\n```\n'
+ },
+ {
+ id: 'abc-keysig',
+ category: 'ABC Notation',
+ title: 'Key Signature & Tempo',
+ label: 'Signature and Speed',
+ svg: svgAbcKeySignature,
+ code: '```abc\nX: 6\nT: Major Tune\nM: 3/4\nL: 1/8\nQ: 1/4=120\nK: G\n|: G2 B2 d2 | g4 fg | a2 A2 B2 | c4 z2 :|\n```\n'
+ },
+
+ // WaveDrom
+ {
+ id: 'wavedrom-timing',
+ category: 'WaveDrom',
+ title: 'Timing Diagram',
+ label: 'Timing Diagram',
+ svg: svgWaveTiming,
+ code: '```wavedrom\n{ signal: [\n { name: "clk", wave: "p......" },\n { name: "bus", wave: "x.==.x.", data: ["head", "body"] }\n]}\n```\n'
+ },
+ {
+ id: 'wavedrom-counter',
+ category: 'WaveDrom',
+ title: 'Binary Counter',
+ label: 'Binary Counter',
+ svg: svgWaveCounter,
+ code: '```wavedrom\n{ signal: [\n { name: "clk", wave: "p......" },\n { name: "q", wave: "01.01.0" }\n]}\n```\n'
+ },
+ {
+ id: 'wavedrom-bus',
+ category: 'WaveDrom',
+ title: 'Data Bus States',
+ label: 'Data Bus States',
+ svg: svgWaveBus,
+ code: '```wavedrom\n{ signal: [\n { name: "bus", wave: "x.=.=.x", data: ["read", "write"] }\n]}\n```\n'
+ },
+ {
+ id: 'wavedrom-reset',
+ category: 'WaveDrom',
+ title: 'Reset Sequence',
+ label: 'Reset & Enable',
+ svg: svgWaveReset,
+ code: '```wavedrom\n{ signal: [\n { name: "reset", wave: "1.0.1" },\n { name: "enable", wave: "0.1.0" }\n]}\n```\n'
+ },
+ {
+ id: 'wavedrom-glitches',
+ category: 'WaveDrom',
+ title: 'Signal Glitches',
+ label: 'Glitchy Waveform',
+ svg: svgWaveGlitches,
+ code: '```wavedrom\n{ signal: [\n { name: "clk", wave: "p......" },\n { name: "signal", wave: "0.h.l.h.0" }\n]}\n```\n'
+ },
+ {
+ id: 'wavedrom-complex-bus',
+ category: 'WaveDrom',
+ title: 'Complex Transaction',
+ label: 'Address & Data Buses',
+ svg: svgWaveComplexBus,
+ code: '```wavedrom\n{ signal: [\n { name: "clk", wave: "p......" },\n { name: "addr", wave: "x.=.x.=", data: ["A0", "A1"] },\n { name: "data", wave: "x...=.x", data: ["D0"] }\n]}\n```\n'
+ },
+
+ // Markmap
+ {
+ id: 'markmap-mindmap',
+ category: 'Markmap',
+ title: 'Mindmap',
+ label: 'Mindmap',
+ svg: svgMarkmapMindmap,
+ code: '```markmap\n# markmap\n## Features\n- Links\n- Formatting\n```\n'
+ },
+ {
+ id: 'markmap-roadmap',
+ category: 'Markmap',
+ title: 'Roadmap',
+ label: 'Project Roadmap',
+ svg: svgMarkmapRoadmap,
+ code: '```markmap\n# Roadmap\n## Q1\n### Plan\n### Design\n## Q2\n### Build\n```\n'
+ },
+ {
+ id: 'markmap-study',
+ category: 'Markmap',
+ title: 'Study Plan',
+ label: 'Study Topics',
+ svg: svgMarkmapStudy,
+ code: '```markmap\n# Course\n## Math\n### Algebra\n### Calculus\n## Science\n### Physics\n```\n'
+ },
+ {
+ id: 'markmap-stack',
+ category: 'Markmap',
+ title: 'Tech Stack',
+ label: 'Tech Stack',
+ svg: svgMarkmapStack,
+ code: '```markmap\n# stack\n## frontend\n### HTML/JS\n## backend\n### Node.js\n```\n'
+ },
+ {
+ id: 'markmap-checklist',
+ category: 'Markmap',
+ title: 'Checklist Map',
+ label: 'Checkbox Map',
+ svg: svgMarkmapChecklist,
+ code: '```markmap\n# Project Tasks\n## Done\n- [x] Initial design\n- [x] Codebase setup\n## Pending\n- [ ] Write tests\n- [ ] Deploy release\n```\n'
+ },
+ {
+ id: 'markmap-code',
+ category: 'Markmap',
+ title: 'Code Mindmap',
+ label: 'Inline Code Blocks',
+ svg: svgMarkmapCode,
+ code: '```markmap\n# Development\n## Languages\n- `JavaScript`\n- `Python`\n## Functions\n- `main()`\n- `helper_func()`\n```\n'
+ }
+ ];
+
+ let activeCategory = 'Mermaid';
+ let selectedTemplate = null;
+
+ function renderSidebar() {
+ sidebar.textContent = '';
+ categories.forEach(cat => {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'diagram-sidebar-btn';
+ if (cat === activeCategory) btn.classList.add('is-active');
+ btn.textContent = cat;
+ btn.addEventListener('click', () => {
+ activeCategory = cat;
+ renderSidebar();
+ renderGrid();
+ });
+ sidebar.appendChild(btn);
+ });
+ }
+
+ function renderGrid() {
+ grid.textContent = '';
+ const query = searchInput.value.toLowerCase().trim();
+
+ const filtered = templates.filter(t => {
+ const matchesCategory = activeCategory === t.category;
+ const matchesSearch = !query ||
+ t.title.toLowerCase().includes(query) ||
+ t.label.toLowerCase().includes(query) ||
+ t.category.toLowerCase().includes(query);
+ return matchesCategory && matchesSearch;
+ });
+
+ if (filtered.length === 0) {
+ emptyMessage.style.display = 'block';
+ } else {
+ emptyMessage.style.display = 'none';
+ }
+
+ filtered.forEach(t => {
+ const card = document.createElement('div');
+ card.className = 'diagram-card';
+ if (selectedTemplate && selectedTemplate.id === t.id) {
+ card.classList.add('is-selected');
+ }
+
+ const previewDiv = document.createElement('div');
+ previewDiv.className = 'diagram-card-preview';
+
+ const isMermaidSpecial = (t.id === 'mermaid-sequence' || t.id === 'mermaid-er');
+ const titleColor = isMermaidSpecial ? '#ff4081' : 'var(--text-color)';
+ const titleWeight = isMermaidSpecial ? 'bold' : 'normal';
+ const catClass = t.category.toLowerCase().replace(/\s+/g, '');
+
+ previewDiv.innerHTML = `
+
+
${t.title}
+
+ ${t.svg}
+
+
+ `;
+
+ const theme = document.documentElement.getAttribute("data-theme") || "light";
+
+ getOrRenderDiagramPreview(t, theme, svgText => {
+ if (svgText) {
+ const svgContainer = previewDiv.querySelector('.diagram-svg-container');
+ if (svgContainer) {
+ svgContainer.innerHTML = svgText;
+ const svgEl = svgContainer.querySelector('svg');
+ if (svgEl) {
+ svgEl.style.maxWidth = '100%';
+ svgEl.style.maxHeight = '100%';
+ svgEl.style.width = 'auto';
+ svgEl.style.height = 'auto';
+ }
+ }
+ }
+ });
+
+ const labelDiv = document.createElement('div');
+ labelDiv.className = 'diagram-card-label';
+ labelDiv.textContent = t.label;
+
+ card.appendChild(previewDiv);
+ card.appendChild(labelDiv);
+
+ card.addEventListener('click', () => {
+ selectedTemplate = t;
+ const cards = grid.querySelectorAll('.diagram-card');
+ cards.forEach(c => c.classList.remove('is-selected'));
+ card.classList.add('is-selected');
+
+ if (previewCode) previewCode.value = t.code.trim();
+ confirmBtn.disabled = false;
+
+ const catClass = t.category.toLowerCase().replace(/\s+/g, '');
+ // Render bottom preview container with API image & fallback
+ previewContainer.innerHTML = `
+
+ ${t.svg}
+
+ `;
+
+ const clickedTheme = document.documentElement.getAttribute("data-theme") || "light";
+
+ getOrRenderDiagramPreview(t, clickedTheme, svgText => {
+ if (svgText) {
+ const svgContainer = previewContainer.querySelector('.diagram-svg-container');
+ if (svgContainer) {
+ svgContainer.innerHTML = svgText;
+ const svgEl = svgContainer.querySelector('svg');
+ if (svgEl) {
+ svgEl.style.maxWidth = '100%';
+ svgEl.style.maxHeight = '100%';
+ svgEl.style.width = 'auto';
+ svgEl.style.height = 'auto';
+ }
+ }
+ }
+ });
+ });
+
+ grid.appendChild(card);
+ });
+ }
+
+
+
+ renderSidebar();
+ renderGrid();
+
+ searchInput.addEventListener('input', renderGrid);
+
+ function insertTemplate() {
+ if (!selectedTemplate) return;
+ modal.style.display = 'none';
+ cleanup();
+ insertMarkdownBlock(selectedTemplate.code, start, end);
+ }
+
+ function closeModal() {
+ modal.style.display = 'none';
+ cleanup();
+ }
+
+ function onKey(e) {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ closeModal();
+ }
+ }
+
+ function cleanup() {
+ confirmBtn.removeEventListener('click', insertTemplate);
+ cancelBtn.removeEventListener('click', closeModal);
+ closeBtn.removeEventListener('click', closeModal);
+ modal.removeEventListener('keydown', onKey);
+ searchInput.removeEventListener('input', renderGrid);
+ }
+
+ confirmBtn.addEventListener('click', insertTemplate);
+ cancelBtn.addEventListener('click', closeModal);
+ closeBtn.addEventListener('click', closeModal);
+ modal.addEventListener('keydown', onKey);
+ }
+
function insertMarkdownLink() {
const modal = document.getElementById('link-modal');
const urlInput = document.getElementById('link-modal-url');
@@ -7303,6 +8427,7 @@ document.addEventListener("DOMContentLoaded", async function () {
}
else if (action === 'symbols') openSymbolsModal();
else if (action === 'alert') openAlertModal();
+ else if (action === 'diagram') openDiagramModal();
else if (action === 'terminal-block') insertMarkdownBlock('```bash\nnpm run dev\n```\n');
else if (action === 'fullscreen') {
if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen();
@@ -10168,7 +11293,7 @@ document.addEventListener("DOMContentLoaded", async function () {
function svgToCanvas(svgEl) {
return new Promise((resolve, reject) => {
const bbox = svgEl.getBoundingClientRect();
- const scale = window.devicePixelRatio || 1;
+ const scale = 2; // 2x scale for high quality without excessive file size
const width = Math.max(Math.round(bbox.width), 1);
const height = Math.max(Math.round(bbox.height), 1);
@@ -10685,6 +11810,169 @@ document.addEventListener("DOMContentLoaded", async function () {
// PLANTUML TOOLBARS & EXPORT ENGINE
// ==========================================================================
+ /** Adds a solid white background to a transparent PNG blob. */
+ function addWhiteBackgroundToPngBlob(blob) {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ const url = URL.createObjectURL(blob);
+ img.onload = () => {
+ URL.revokeObjectURL(url);
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = img.naturalWidth || img.width;
+ canvas.height = img.naturalHeight || img.height;
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0);
+ canvas.toBlob(newBlob => {
+ if (newBlob) {
+ resolve(newBlob);
+ } else {
+ reject(new Error('Canvas toBlob failed'));
+ }
+ }, 'image/png');
+ } catch (err) {
+ reject(err);
+ }
+ };
+ img.onerror = (e) => {
+ URL.revokeObjectURL(url);
+ reject(new Error('Failed to load image for background addition'));
+ };
+ img.src = url;
+ });
+ }
+
+ async function getSvgOriginalDimensions(url) {
+ try {
+ const res = await fetch(url);
+ if (!res.ok) return null;
+ const text = await res.text();
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(text, 'image/svg+xml');
+ const svg = doc.querySelector('svg');
+ if (!svg) return null;
+
+ let width = parseFloat(svg.getAttribute('width'));
+ let height = parseFloat(svg.getAttribute('height'));
+
+ const viewBox = svg.getAttribute('viewBox');
+ if (viewBox) {
+ const parts = viewBox.trim().split(/\s+/);
+ if (parts.length === 4) {
+ const vbWidth = parseFloat(parts[2]);
+ const vbHeight = parseFloat(parts[3]);
+ if (!isNaN(vbWidth) && !isNaN(vbHeight)) {
+ width = vbWidth;
+ height = vbHeight;
+ }
+ }
+ }
+
+ if (!isNaN(width) && !isNaN(height) && width > 0 && height > 0) {
+ return { width, height, text };
+ }
+ } catch (e) {
+ console.warn('Failed to parse SVG dimensions:', e);
+ }
+ return null;
+ }
+
+ /** Generates a high-quality PNG blob from a rendered diagram image. */
+ async function getDiagramPngBlob(imgEl, pngUrl) {
+ // Attempt to fetch SVG text to parse the exact original coordinates (viewBox)
+ const originalDim = await getSvgOriginalDimensions(imgEl.src);
+
+ return new Promise((resolve, reject) => {
+ try {
+ const canvas = document.createElement('canvas');
+ const scale = 2; // 2x scale for high quality without being excessively large
+
+ let width = imgEl.naturalWidth || imgEl.width || 800;
+ let height = imgEl.naturalHeight || imgEl.height || 600;
+
+ if (originalDim) {
+ width = originalDim.width;
+ height = originalDim.height;
+ }
+
+ canvas.width = width * scale;
+ canvas.height = height * scale;
+
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ ctx.scale(scale, scale);
+
+ const img = new Image();
+ img.onload = () => {
+ ctx.drawImage(img, 0, 0, width, height);
+ canvas.toBlob(blob => {
+ if (blob) {
+ resolve(blob);
+ } else {
+ reject(new Error('Canvas toBlob failed'));
+ }
+ }, 'image/png');
+ };
+ img.onerror = () => {
+ // Fallback to direct imgEl drawing if data URL loading fails
+ try {
+ ctx.drawImage(imgEl, 0, 0, width, height);
+ canvas.toBlob(blob => {
+ if (blob) resolve(blob);
+ else reject(new Error('Canvas toBlob failed'));
+ }, 'image/png');
+ } catch (err) {
+ reject(err);
+ }
+ };
+
+ if (originalDim && originalDim.text) {
+ const blob = new Blob([originalDim.text], { type: 'image/svg+xml;charset=utf-8' });
+ img.src = URL.createObjectURL(blob);
+ } else {
+ img.src = imgEl.src;
+ }
+ } catch (err) {
+ reject(err);
+ }
+ });
+ }
+
+ /** Helper to download an SVG diagram with fallback if fetch fails. */
+ async function downloadSvgHelper(imgEl, filename, btn, originalHtml) {
+ try {
+ const res = await fetch(imgEl.src);
+ if (!res.ok) throw new Error(`HTTP status ${res.status}`);
+ const blob = await res.blob();
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(url);
+ btn.innerHTML = '';
+ setTimeout(() => { btn.innerHTML = originalHtml; }, 1500);
+ } catch (e) {
+ console.warn('SVG fetch download failed, attempting fallback direct link download:', e);
+ try {
+ const a = document.createElement('a');
+ a.href = imgEl.src;
+ a.download = filename;
+ a.target = '_blank';
+ a.click();
+ btn.innerHTML = '';
+ } catch (err) {
+ console.error('SVG download completely failed:', err);
+ btn.innerHTML = '';
+ }
+ setTimeout(() => { btn.innerHTML = originalHtml; }, 1500);
+ }
+ }
+
/** Downloads the PlantUML diagram in the given container as a PNG file. */
async function downloadPlantumlPng(container, btn) {
const imgEl = container.querySelector('img');
@@ -10693,8 +11981,7 @@ document.addEventListener("DOMContentLoaded", async function () {
btn.innerHTML = '';
try {
const pngUrl = imgEl.src.replace('/svg/', '/png/');
- const res = await fetch(pngUrl);
- const blob = await res.blob();
+ const blob = await getDiagramPngBlob(imgEl, pngUrl);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
@@ -10717,8 +12004,7 @@ document.addEventListener("DOMContentLoaded", async function () {
btn.innerHTML = '';
try {
const pngUrl = imgEl.src.replace('/svg/', '/png/');
- const res = await fetch(pngUrl);
- const blob = await res.blob();
+ const blob = await getDiagramPngBlob(imgEl, pngUrl);
try {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
@@ -10741,21 +12027,7 @@ document.addEventListener("DOMContentLoaded", async function () {
if (!imgEl) return;
const original = btn.innerHTML;
btn.innerHTML = '';
- try {
- const res = await fetch(imgEl.src);
- const blob = await res.blob();
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `diagram-${Date.now()}.svg`;
- a.click();
- URL.revokeObjectURL(url);
- btn.innerHTML = '';
- setTimeout(() => { btn.innerHTML = original; }, 1500);
- } catch (e) {
- console.error('PlantUML SVG export failed:', e);
- btn.innerHTML = original;
- }
+ await downloadSvgHelper(imgEl, `diagram-${Date.now()}.svg`, btn, original);
}
/** Opens the zoom modal with the PlantUML image from the given container. */
@@ -10842,8 +12114,7 @@ document.addEventListener("DOMContentLoaded", async function () {
btn.innerHTML = '';
try {
const pngUrl = imgEl.src.replace('/svg/', '/png/');
- const res = await fetch(pngUrl);
- const blob = await res.blob();
+ const blob = await getDiagramPngBlob(imgEl, pngUrl);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
@@ -10866,8 +12137,7 @@ document.addEventListener("DOMContentLoaded", async function () {
btn.innerHTML = '';
try {
const pngUrl = imgEl.src.replace('/svg/', '/png/');
- const res = await fetch(pngUrl);
- const blob = await res.blob();
+ const blob = await getDiagramPngBlob(imgEl, pngUrl);
try {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
@@ -10890,21 +12160,7 @@ document.addEventListener("DOMContentLoaded", async function () {
if (!imgEl) return;
const original = btn.innerHTML;
btn.innerHTML = '';
- try {
- const res = await fetch(imgEl.src);
- const blob = await res.blob();
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `diagram-${Date.now()}.svg`;
- a.click();
- URL.revokeObjectURL(url);
- btn.innerHTML = '';
- setTimeout(() => { btn.innerHTML = original; }, 1500);
- } catch (e) {
- console.error('D2 SVG export failed:', e);
- btn.innerHTML = original;
- }
+ await downloadSvgHelper(imgEl, `diagram-${Date.now()}.svg`, btn, original);
}
/** Opens the zoom modal with the D2 image from the given container. */
@@ -10979,6 +12235,139 @@ document.addEventListener("DOMContentLoaded", async function () {
});
}
+ // ==========================================================================
+ // GRAPHVIZ TOOLBARS & EXPORT ENGINE
+ // ==========================================================================
+
+ /** Downloads the Graphviz diagram in the given container as a PNG file. */
+ async function downloadGraphvizPng(container, btn) {
+ const imgEl = container.querySelector('img');
+ if (!imgEl) return;
+ const original = btn.innerHTML;
+ btn.innerHTML = '';
+ try {
+ const pngUrl = imgEl.src.replace('/svg/', '/png/');
+ const blob = await getDiagramPngBlob(imgEl, pngUrl);
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `diagram-${Date.now()}.png`;
+ a.click();
+ URL.revokeObjectURL(url);
+ btn.innerHTML = '';
+ setTimeout(() => { btn.innerHTML = original; }, 1500);
+ } catch (e) {
+ console.error('Graphviz PNG export failed:', e);
+ btn.innerHTML = original;
+ }
+ }
+
+ /** Copies the Graphviz diagram in the given container as a PNG image to the clipboard. */
+ async function copyGraphvizImage(container, btn) {
+ const imgEl = container.querySelector('img');
+ if (!imgEl) return;
+ const original = btn.innerHTML;
+ btn.innerHTML = '';
+ try {
+ const pngUrl = imgEl.src.replace('/svg/', '/png/');
+ const blob = await getDiagramPngBlob(imgEl, pngUrl);
+ try {
+ await navigator.clipboard.write([
+ new ClipboardItem({ 'image/png': blob })
+ ]);
+ btn.innerHTML = ' Copied!';
+ } catch (clipErr) {
+ console.error('Clipboard write failed:', clipErr);
+ btn.innerHTML = '';
+ }
+ setTimeout(() => { btn.innerHTML = original; }, 1800);
+ } catch (e) {
+ console.error('Graphviz copy failed:', e);
+ btn.innerHTML = original;
+ }
+ }
+
+ /** Downloads the SVG source of a Graphviz diagram. */
+ async function downloadGraphvizSvg(container, btn) {
+ const imgEl = container.querySelector('img');
+ if (!imgEl) return;
+ const original = btn.innerHTML;
+ btn.innerHTML = '';
+ await downloadSvgHelper(imgEl, `diagram-${Date.now()}.svg`, btn, original);
+ }
+
+ /** Opens the zoom modal with the Graphviz image from the given container. */
+ function openGraphvizZoomModal(container) {
+ const imgEl = container.querySelector('img');
+ if (!imgEl) return;
+
+ mermaidModalDiagram.textContent = '';
+ modalZoomScale = 1;
+ modalPanX = 0;
+ modalPanY = 0;
+
+ const imgClone = imgEl.cloneNode(true);
+ imgClone.removeAttribute('width');
+ imgClone.removeAttribute('height');
+ imgClone.style.width = 'auto';
+ imgClone.style.height = 'auto';
+ imgClone.style.maxWidth = '80vw';
+ imgClone.style.maxHeight = '60vh';
+ imgClone.style.transformOrigin = 'center';
+ imgClone.draggable = false;
+ imgClone.addEventListener('dragstart', e => e.preventDefault());
+ mermaidModalDiagram.appendChild(imgClone);
+ modalCurrentSvgEl = imgClone;
+
+ mermaidZoomModal.classList.add('active');
+ }
+
+ function addGraphvizToolbars() {
+ markdownPreview.querySelectorAll('.graphviz-container').forEach(container => {
+ if (container.querySelector('.graphviz-toolbar')) return; // already added
+ const imgEl = container.querySelector('img');
+ if (!imgEl) return; // diagram not yet rendered or failed
+
+ const toolbar = document.createElement('div');
+ toolbar.className = 'graphviz-toolbar';
+ toolbar.setAttribute('aria-label', 'Diagram actions');
+
+ const btnZoom = document.createElement('button');
+ btnZoom.className = 'graphviz-toolbar-btn';
+ btnZoom.title = 'Zoom diagram';
+ btnZoom.setAttribute('aria-label', 'Zoom diagram');
+ btnZoom.innerHTML = '';
+ btnZoom.addEventListener('click', () => openGraphvizZoomModal(container));
+
+ const btnPng = document.createElement('button');
+ btnPng.className = 'graphviz-toolbar-btn';
+ btnPng.title = 'Download PNG';
+ btnPng.setAttribute('aria-label', 'Download PNG');
+ btnPng.innerHTML = ' PNG';
+ btnPng.addEventListener('click', () => downloadGraphvizPng(container, btnPng));
+
+ const btnCopy = document.createElement('button');
+ btnCopy.className = 'graphviz-toolbar-btn';
+ btnCopy.title = 'Copy image to clipboard';
+ btnCopy.setAttribute('aria-label', 'Copy image to clipboard');
+ btnCopy.innerHTML = ' Copy';
+ btnCopy.addEventListener('click', () => copyGraphvizImage(container, btnCopy));
+
+ const btnSvg = document.createElement('button');
+ btnSvg.className = 'graphviz-toolbar-btn';
+ btnSvg.title = 'Download SVG';
+ btnSvg.setAttribute('aria-label', 'Download SVG');
+ btnSvg.innerHTML = ' SVG';
+ btnSvg.addEventListener('click', () => downloadGraphvizSvg(container, btnSvg));
+
+ toolbar.appendChild(btnZoom);
+ toolbar.appendChild(btnCopy);
+ toolbar.appendChild(btnPng);
+ toolbar.appendChild(btnSvg);
+ container.appendChild(toolbar);
+ });
+ }
+
function zoomStl(view, factor) {
if (!view || !view.camera || !view.controls) return;
const camera = view.camera;
diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css
index 34fee93..3207c8d 100644
--- a/desktop-app/resources/styles.css
+++ b/desktop-app/resources/styles.css
@@ -1609,7 +1609,8 @@ a:focus {
.mermaid-container:hover .mermaid-toolbar,
.abc-container:hover .abc-toolbar,
.plantuml-container:hover .plantuml-toolbar,
-.d2-container:hover .d2-toolbar {
+.d2-container:hover .d2-toolbar,
+.graphviz-container:hover .graphviz-toolbar {
opacity: 1;
}
@@ -1617,7 +1618,8 @@ a:focus {
.abc-toolbar-btn,
.stl-toolbar-btn,
.plantuml-toolbar-btn,
-.d2-toolbar-btn {
+.d2-toolbar-btn,
+.graphviz-toolbar-btn {
background-color: var(--button-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
@@ -1636,7 +1638,8 @@ a:focus {
.abc-toolbar-btn:hover,
.stl-toolbar-btn:hover,
.plantuml-toolbar-btn:hover,
-.d2-toolbar-btn:hover {
+.d2-toolbar-btn:hover,
+.graphviz-toolbar-btn:hover {
background-color: var(--button-hover);
color: var(--accent-color);
}
@@ -1645,7 +1648,8 @@ a:focus {
.abc-toolbar-btn:active,
.stl-toolbar-btn:active,
.plantuml-toolbar-btn:active,
-.d2-toolbar-btn:active {
+.d2-toolbar-btn:active,
+.graphviz-toolbar-btn:active {
background-color: var(--button-active);
}
@@ -1653,7 +1657,8 @@ a:focus {
.abc-toolbar-btn i,
.stl-toolbar-btn i,
.plantuml-toolbar-btn i,
-.d2-toolbar-btn i {
+.d2-toolbar-btn i,
+.graphviz-toolbar-btn i {
font-size: 14px;
}
@@ -4218,14 +4223,14 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang=
.plantuml-diagram {
width: 100%;
- display: flex;
- justify-content: center;
+ text-align: center;
}
-.plantuml-diagram img {
+.plantuml-diagram img,
+.plantuml-diagram svg {
max-width: 100%;
height: auto;
- display: block;
+ display: inline-block;
user-select: none;
-webkit-user-drag: none;
}
@@ -4233,7 +4238,9 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang=
[data-theme="dark"] .plantuml-diagram img,
[data-theme="dark"] .plantuml-img,
[data-theme="dark"] .d2-diagram img,
-[data-theme="dark"] .d2-img {
+[data-theme="dark"] .d2-img,
+[data-theme="dark"] .graphviz-diagram img,
+[data-theme="dark"] .graphviz-img {
filter: invert(0.9) hue-rotate(180deg);
}
@@ -4293,14 +4300,14 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang=
.d2-diagram {
width: 100%;
- display: flex;
- justify-content: center;
+ text-align: center;
}
-.d2-diagram img {
+.d2-diagram img,
+.d2-diagram svg {
max-width: 100%;
height: auto;
- display: block;
+ display: inline-block;
user-select: none;
-webkit-user-drag: none;
}
@@ -4321,6 +4328,74 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang=
opacity: 1;
}
+/* --- Graphviz Styles --- */
+.graphviz-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 1.5em 0;
+ overflow-x: auto;
+ position: relative;
+}
+
+.graphviz-container.is-loading {
+ min-height: 120px;
+ background-color: var(--skeleton-bg);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ position: relative;
+ overflow: hidden;
+ animation: skeleton-pulse 2.2s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate;
+}
+
+.graphviz-container.is-loading .graphviz-diagram {
+ opacity: 0;
+}
+
+.graphviz-container.is-loading::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ transform: translateX(-100%);
+ background-image: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0) 0%,
+ var(--skeleton-glow) 50%,
+ rgba(255, 255, 255, 0) 100%
+ );
+ animation: skeleton-shimmer 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+}
+
+.graphviz-diagram {
+ width: 100%;
+ text-align: center;
+}
+
+.graphviz-diagram img,
+.graphviz-diagram svg {
+ max-width: 100%;
+ height: auto;
+ display: inline-block;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+/* --- Graphviz Toolbar Styling --- */
+.graphviz-toolbar {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ display: flex;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ z-index: 10;
+}
+
+.graphviz-container:hover .graphviz-toolbar {
+ opacity: 1;
+}
+
/* Accessibility: respect user's motion preferences */
@media (prefers-reduced-motion: reduce) {
.skeleton-placeholder,
@@ -4338,7 +4413,9 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang=
.plantuml-container.is-loading,
.plantuml-container.is-loading::after,
.d2-container.is-loading,
- .d2-container.is-loading::after {
+ .d2-container.is-loading::after,
+ .graphviz-container.is-loading,
+ .graphviz-container.is-loading::after {
animation: none;
}
.drag-overlay-inner {
@@ -4365,4 +4442,342 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang=
.abcjs-cursor {
stroke: red;
stroke-width: 2px;
+}
+
+/* Insert Diagram Modal Styles */
+.diagram-modal-box {
+ max-width: 1000px !important;
+ width: 95vw;
+ height: 90vh;
+ max-height: 800px;
+ display: flex;
+ flex-direction: column;
+}
+
+.diagram-modal-body {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.diagram-modal-sidebar {
+ width: 220px;
+ border-right: 1px solid var(--border-color);
+ padding: 16px 8px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.diagram-sidebar-btn {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 14px;
+ border: none;
+ background: transparent;
+ color: var(--text-color);
+ text-align: left;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: background-color 0.15s ease, color 0.15s ease;
+}
+
+.diagram-sidebar-btn:hover {
+ background-color: var(--button-hover-bg, rgba(120, 120, 120, 0.1));
+}
+
+.diagram-sidebar-btn.is-active {
+ background-color: var(--accent-color, #0076ff);
+ color: #fff;
+}
+
+.diagram-modal-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ padding: 16px 20px;
+ min-width: 0;
+ overflow-y: auto;
+}
+
+.diagram-modal-search-wrapper {
+ margin-bottom: 16px;
+}
+
+.diagram-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.diagram-card {
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--button-bg, rgba(200, 200, 200, 0.05));
+ cursor: pointer;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
+ min-height: 140px;
+}
+
+.diagram-card:hover {
+ transform: translateY(-2px);
+ border-color: var(--accent-color, #0076ff);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.diagram-card.is-selected {
+ border-color: var(--accent-color, #0076ff);
+ outline: 2px solid var(--accent-color, #0076ff);
+}
+
+.diagram-card-preview {
+ flex: 1;
+ background: #ffffff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px;
+ min-height: 90px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+html[data-theme="dark"] .diagram-card-preview {
+ background: #1e1e1e;
+}
+
+.diagram-card-label {
+ padding: 8px 12px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ text-align: center;
+ color: var(--text-color);
+ background: var(--header-bg);
+}
+
+.diagram-modal-preview-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: auto;
+ border-top: 1px solid var(--border-color);
+ padding-top: 16px;
+}
+
+.diagram-modal-preview-title {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+.diagram-modal-preview-split {
+ display: flex;
+ gap: 16px;
+ height: 200px;
+}
+
+.diagram-preview-code-pane {
+ flex: 1;
+ display: flex;
+ min-width: 0;
+}
+
+.diagram-preview-textarea {
+ width: 100%;
+ height: 100%;
+ resize: none;
+ font-family: var(--font-mono, monospace);
+ font-size: 0.85rem;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--editor-bg, #ffffff);
+ color: var(--text-color);
+ outline: none;
+}
+
+html[data-theme="dark"] .diagram-preview-textarea {
+ background: #1e1e1e;
+}
+
+.diagram-preview-container {
+ flex: 1;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: #ffffff;
+ height: 100%;
+ overflow: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ min-width: 0;
+}
+
+html[data-theme="dark"] .diagram-preview-container {
+ background: #1e1e1e;
+}
+
+.diagram-preview-container svg {
+ max-width: 100%;
+ max-height: 100%;
+ width: auto;
+ height: auto;
+}
+
+.diagram-preview-placeholder {
+ color: var(--text-muted, #888);
+ font-style: italic;
+ font-size: 0.9rem;
+}
+
+/* Dynamic Card Previews Styling */
+.diagram-card-preview svg,
+.diagram-card-preview img,
+.diagram-card-preview .mermaid-container,
+.diagram-card-preview .mermaid,
+.diagram-card-preview .abc-container,
+.diagram-card-preview .abc-notation,
+.diagram-card-preview .plantuml-container,
+.diagram-card-preview .plantuml-diagram,
+.diagram-card-preview .d2-container,
+.diagram-card-preview .d2-diagram,
+.diagram-card-preview .graphviz-container,
+.diagram-card-preview .graphviz-diagram {
+ max-width: 95% !important;
+ max-height: 80px !important;
+ width: auto !important;
+ height: auto !important;
+}
+
+.diagram-card-preview .mermaid-container,
+.diagram-card-preview .plantuml-container,
+.diagram-card-preview .d2-container,
+.diagram-card-preview .graphviz-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+}
+
+/* Hide card level toolbars to maintain clean card design */
+.diagram-card-preview .mermaid-toolbar,
+.diagram-card-preview .plantuml-toolbar,
+.diagram-card-preview .d2-toolbar,
+.diagram-card-preview .graphviz-toolbar {
+ display: none !important;
+}
+
+@media (max-width: 768px) {
+ .diagram-modal-body {
+ flex-direction: column;
+ }
+ .diagram-modal-sidebar {
+ width: 100%;
+ height: 60px;
+ flex-direction: row;
+ border-right: none;
+ border-bottom: 1px solid var(--border-color);
+ padding: 8px;
+ }
+ .diagram-modal-box {
+ height: 95vh;
+ }
+ .diagram-modal-preview-split {
+ flex-direction: column;
+ height: auto;
+ gap: 12px;
+ }
+ .diagram-preview-code-pane {
+ height: 100px;
+ }
+ .diagram-preview-container {
+ height: 120px;
+ }
+}
+
+/* Theme adaptive fallbacks for offline mock SVGs */
+html[data-theme="dark"] .diagram-svg-container svg text {
+ fill: #e5e9f0 !important;
+}
+
+html[data-theme="dark"] .diagram-svg-container svg line,
+html[data-theme="dark"] .diagram-svg-container svg path {
+ stroke: #81a1c1 !important;
+}
+
+/* Specific category mock tuning */
+html[data-theme="dark"] .diagram-svg-mermaid svg rect,
+html[data-theme="dark"] .diagram-svg-mermaid svg circle {
+ fill: #2e3440 !important;
+ stroke: #81a1c1 !important;
+}
+
+html[data-theme="dark"] .diagram-svg-plantuml svg rect,
+html[data-theme="dark"] .diagram-svg-plantuml svg circle,
+html[data-theme="dark"] .diagram-svg-plantuml svg ellipse,
+html[data-theme="dark"] .diagram-svg-plantuml svg polygon {
+ fill: #3b2019 !important; /* retro mahogany adaptation */
+ stroke: #ff6b6b !important; /* soft red outline */
+}
+html[data-theme="dark"] .diagram-svg-plantuml svg text {
+ fill: #ffd8a8 !important; /* soft amber text */
+}
+
+html[data-theme="dark"] .diagram-svg-d2 svg rect,
+html[data-theme="dark"] .diagram-svg-d2 svg circle {
+ fill: #2d3748 !important;
+ stroke: #cbd5e0 !important;
+}
+
+html[data-theme="dark"] .diagram-svg-graphviz svg rect,
+html[data-theme="dark"] .diagram-svg-graphviz svg circle,
+html[data-theme="dark"] .diagram-svg-graphviz svg ellipse {
+ fill: #2d2625 !important;
+ stroke: #a1887f !important;
+}
+
+html[data-theme="dark"] .diagram-svg-vegalite svg rect {
+ fill: #43a047 !important; /* glowing theme bar chart fill */
+}
+html[data-theme="dark"] .diagram-svg-vegalite svg circle {
+ fill: #ab47bc !important;
+}
+html[data-theme="dark"] .diagram-svg-vegalite svg line,
+html[data-theme="dark"] .diagram-svg-vegalite svg path {
+ stroke: #e2e8f0 !important;
+}
+
+html[data-theme="dark"] .diagram-svg-wavedrom svg path {
+ stroke: #ff7043 !important;
+}
+
+html[data-theme="dark"] .diagram-svg-markmap svg rect {
+ fill: #37474f !important;
+ stroke: #90a4ae !important;
+}
+html[data-theme="dark"] .diagram-svg-markmap svg path {
+ stroke: #90a4ae !important;
+}
+html[data-theme="dark"] .diagram-svg-markmap svg circle {
+ fill: #ff7043 !important;
+}
+
+html[data-theme="dark"] .diagram-svg-abcnotation svg line {
+ stroke: #555555 !important;
+}
+html[data-theme="dark"] .diagram-svg-abcnotation svg path {
+ fill: #eceff4 !important;
+ stroke: #eceff4 !important;
}
\ No newline at end of file
diff --git a/index.html b/index.html
index 7a509a4..027b941 100644
--- a/index.html
+++ b/index.html
@@ -385,6 +385,7 @@ Menu
+