refactor(flowchart): standalone window (Cmd+Alt+F) replaces sidebar panel; v4.9.6

Five fix rounds (v4.9.1 → v4.9.5) couldn't make the sidebar flowchart
panel feel right — at 280 px sidebar with canvas + preview cramped to
~175 px each, plus the editor-container hide/show dance the
maximize/restore toggle required, the panel kept presenting as
cramped and unreliable at runtime. Strategy pivot: the flowchart
editor now lives in its own BrowserWindow, matching the ASCII Art
Generator pattern.

- src/flowchart-generator.html — new standalone HTML with header,
  toolbar (Insert at Cursor / Reset), canvas host, and preview host.
  Stylesheet hrefs are src/-relative (no ../). Forced light surface
  via background/color !important rules — mirrors the v4.9.5 CSS fix.
- src/renderer/flowchart-controller.js — pure browser IIFE. Hydrates
  from <userData>/flowchart-session.json once on mount, persists on
  every mutation with 500 ms debounce. Insert at Cursor wraps the
  generated Mermaid source in a fenced ```mermaid block and sends
  it through the existing insert-content IPC.
- src/main.js — openFlowchartGenerator() launches the standalone
  BrowserWindow (1100×720, contextIsolation:true, nodeIntegration:false).
  Tools menu entry 'Flowchart Generator' with Cmd/Ctrl+Alt+F.
- src/preload.js — new window.electronAPI.flowchart.* namespace with
  getUserDataPath/readFile/writeFile/insertAtCursor, reusing the
  existing thin text-file IPC handlers.
- The four pure modules (flowchart-shapes.js, flowchart-mermaid.js,
  flowchart-store.js, flowchart-canvas.js) gained a tiny UMD wrapper
  so they work both as CommonJS (legacy sidebar) and as browser
  globals (standalone window). No behavioural change to the 73
  flowchart unit tests in tests/flowchart-*.test.js.
- src/renderer.js — sidebar registerPanel('flowchart', ...) and the
  commandPalette entry are now commented out. Legacy panel file
  (src/sidebar/flowchart-panel.js) preserved for rollback.

Tests:
- tests/flowchart-controller.test.js — 10 new tests covering
  stylesheet paths (no ../ escape), bootstrap wiring
  (getUserDataPath once, reads flowchart-session.json), hydration
  from a saved session, Insert-at-Cursor fenced block format,
  Reset (with and without confirm), and two regression tests that
  src/renderer.js no longer has a live sidebar registration.

All 1122 tests pass; lint + format clean.

Amit Haridas
This commit is contained in:
2026-09-14 23:50:25 +05:30
parent 9557c0af4d
commit 5f846d1e5f
13 changed files with 848 additions and 21 deletions
+210
View File
@@ -0,0 +1,210 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flowchart Generator - MarkdownConverter</title>
<!--
v4.9.6 — Standalone Flowchart Generator window.
Stylesheets are src/-relative (mirrors src/index.html). NO ../ escape —
the standalone BrowserWindow's document base is src/, so a ../fonts.css
would resolve outside src/ and fail to load.
-->
<link rel="stylesheet" href="fonts.css" />
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="styles-modern.css" />
<link rel="stylesheet" href="styles-concreteinfo.css" />
<link rel="stylesheet" href="styles-sidebar.css" />
<link rel="stylesheet" href="styles-zen.css" />
<link rel="stylesheet" href="styles-welcome.css" />
<link rel="stylesheet" href="katex.min.css" />
<style>
:root {
--fc-bg: #fafafa;
--fc-text: #1f2328;
--fc-border: #d0d7de;
--fc-accent: #e5461f;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--fc-bg);
color: var(--fc-text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.fc-header {
background: linear-gradient(135deg, #464646 0%, #0d0b09 100%);
padding: 12px 20px;
border-bottom: 3px solid var(--fc-accent);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.fc-header h1 {
color: #fff;
font-size: 1.05rem;
font-weight: 600;
}
.fc-hint {
color: #9a9696;
font-size: 0.75rem;
}
.fc-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--fc-border);
background: #fff;
}
.fc-btn {
padding: 6px 14px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
border: 1px solid var(--fc-border);
background: #fff;
color: var(--fc-text);
}
.fc-btn:hover {
border-color: var(--fc-accent);
}
.fc-btn.primary {
background: var(--fc-accent);
color: #fff;
border-color: var(--fc-accent);
}
.fc-btn.primary:hover {
background: #c93a18;
}
.fc-status {
margin-left: auto;
font-size: 0.75rem;
color: #6e7681;
}
.fc-split {
display: flex;
flex: 1;
min-height: 0;
}
.fc-canvas-host {
flex: 7;
min-width: 0;
position: relative;
background: #fafafa;
border-right: 1px solid var(--fc-border);
}
.fc-canvas-host svg.flowchart-canvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
}
.fc-preview-host {
flex: 3;
min-width: 0;
display: flex;
flex-direction: column;
background: #fafafa;
}
.fc-preview-source {
flex: 1;
min-height: 120px;
margin: 0;
padding: 12px;
font-family: var(--mono-font, 'JetBrains Mono', monospace);
font-size: 12px;
overflow: auto;
white-space: pre;
border-bottom: 1px solid var(--fc-border);
background: #fff;
color: var(--fc-text);
}
.fc-preview-render {
flex: 1;
min-height: 120px;
padding: 12px;
overflow: auto;
background: #fff;
}
/* v4.9.6 — forced light surfaces inside the standalone window.
* Diverges from body theme to guarantee readability, mirroring the
* v4.9.5 fix in src/styles-sidebar.css for the (now-legacy) sidebar. */
.fc-canvas-host,
.fc-preview-host {
background: #fafafa !important;
color: #1f2328 !important;
}
.flowchart-node rect,
.flowchart-node polygon {
fill: #ffffff !important;
stroke: #1f2328 !important;
stroke-width: 1.25;
}
.flowchart-node text {
fill: #1f2328 !important;
}
.flowchart-edge {
stroke: #1f2328 !important;
}
.flowchart-node.selected rect,
.flowchart-node.selected polygon {
fill: #e3f0ff !important;
stroke: var(--fc-accent) !important;
stroke-width: 3;
}
.flowchart-edge.selected {
stroke: var(--fc-accent) !important;
stroke-width: 3;
}
</style>
</head>
<body>
<div class="fc-header">
<h1>Flowchart Generator</h1>
<span class="fc-hint"
>Click empty canvas to add nodes · Alt+drag to connect · Double-click node to edit</span
>
</div>
<div class="fc-toolbar">
<button
class="fc-btn primary"
id="fc-btn-insert"
title="Insert Mermaid block at cursor in main editor"
>
Insert at Cursor
</button>
<button class="fc-btn" id="fc-btn-reset" title="Clear all nodes and edges">Reset</button>
<span class="fc-status" id="fc-status" aria-live="polite"></span>
</div>
<div class="fc-split">
<div class="fc-canvas-host" id="canvas-host"></div>
<div class="fc-preview-host" id="preview-host">
<pre class="fc-preview-source" id="preview-source"></pre>
<div class="fc-preview-render" id="preview-render"></div>
</div>
</div>
<!--
v4.9.6 — pure-module load order matters. shapes first, then canvas
(which depends on shapes), then store, then mermaid. Controller last
and consumes all four via window globals.
-->
<script src="flowchart/flowchart-shapes.js"></script>
<script src="flowchart/flowchart-mermaid.js"></script>
<script src="flowchart/flowchart-store.js"></script>
<script src="flowchart/flowchart-canvas.js"></script>
<script src="renderer/flowchart-controller.js"></script>
</body>
</html>
+19 -2
View File
@@ -13,7 +13,12 @@
'use strict';
const { DEFAULT_WIDTH, DEFAULT_HEIGHT, shapeSvg, SHAPE_KINDS } = require('./flowchart-shapes');
// v4.9.6 — CommonJS sibling import when running under Node (the renderer.js
// sidebar panel still uses require()), browser global fallback when loaded as
// a <script> tag in the standalone window (no nodeIntegration).
const shapesModule =
(typeof window !== 'undefined' && window.FlowchartShapes) || require('./flowchart-shapes');
const { DEFAULT_WIDTH, DEFAULT_HEIGHT, shapeSvg, SHAPE_KINDS } = shapesModule;
const SVG_NS = 'http://www.w3.org/2000/svg';
@@ -322,4 +327,16 @@ function createCanvas(container, store, opts = {}) {
return { destroy, getSvg: () => svg };
}
module.exports = { createCanvas, SHAPE_KINDS };
// v4.9.6 UMD wrapper — same CommonJS export shape + browser global
// (window.FlowchartCanvas) so the standalone window's controller can load
// this module via <script> tag without nodeIntegration.
(function (root, factory) {
const exported = factory();
if (typeof module === 'object' && module.exports) {
module.exports = exported;
} else {
root.FlowchartCanvas = exported;
}
})(typeof window !== 'undefined' ? window : globalThis, function () {
return { createCanvas, SHAPE_KINDS };
});
+13 -1
View File
@@ -90,4 +90,16 @@ function toMermaid(graph) {
return lines.join('\n');
}
module.exports = { toMermaid, escapeLabel, nodeDeclaration, edgeDeclaration };
// v4.9.6 UMD wrapper — same CommonJS export shape + browser global
// (window.FlowchartMermaid) so the standalone window's controller can load
// this module via <script> tag without nodeIntegration.
(function (root, factory) {
const exported = factory();
if (typeof module === 'object' && module.exports) {
module.exports = exported;
} else {
root.FlowchartMermaid = exported;
}
})(typeof window !== 'undefined' ? window : globalThis, function () {
return { toMermaid, escapeLabel, nodeDeclaration, edgeDeclaration };
});
+24 -8
View File
@@ -55,11 +55,27 @@ function shapeSvg(kind, x, y, width, height) {
}
}
module.exports = {
shapeSvg,
SHAPE_KINDS,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
LABEL_PADDING_X,
LABEL_PADDING_Y,
};
// v4.9.6 UMD wrapper — exposes the same surface as a CommonJS module
// (used by src/sidebar/flowchart-panel.js via require()) AND as a browser
// global (used by src/renderer/flowchart-controller.js via <script> tag).
// The standalone BrowserWindow runs with contextIsolation:true +
// nodeIntegration:false, so the renderer cannot require() these modules;
// loading them as <script> tags in src/flowchart-generator.html attaches
// them to window.FlowchartShapes.
(function (root, factory) {
const exported = factory();
if (typeof module === 'object' && module.exports) {
module.exports = exported;
} else {
root.FlowchartShapes = exported;
}
})(typeof window !== 'undefined' ? window : globalThis, function () {
return {
shapeSvg,
SHAPE_KINDS,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
LABEL_PADDING_X,
LABEL_PADDING_Y,
};
});
+13 -1
View File
@@ -258,4 +258,16 @@ function create(io) {
};
}
module.exports = { create, NODE_KINDS, EDGE_KINDS };
// v4.9.6 UMD wrapper — same CommonJS export shape + browser global
// (window.FlowchartStore) so the standalone window's controller can load
// this module via <script> tag without nodeIntegration.
(function (root, factory) {
const exported = factory();
if (typeof module === 'object' && module.exports) {
module.exports = exported;
} else {
root.FlowchartStore = exported;
}
})(typeof window !== 'undefined' ? window : globalThis, function () {
return { create, NODE_KINDS, EDGE_KINDS };
});
+45
View File
@@ -1433,6 +1433,12 @@ function createMenu() {
accelerator: 'CmdOrCtrl+Shift+A',
click: () => openAsciiGenerator(),
},
{
// v4.9.6 — replaces the sidebar Flow Chart panel.
label: 'Flowchart Generator',
accelerator: 'CmdOrCtrl+Alt+F',
click: () => openFlowchartGenerator(),
},
{
type: 'separator',
},
@@ -5485,6 +5491,45 @@ ipcMain.on('select-image-folder', (event, inputId) => {
}
});
// ============================================
// Flowchart Generator Window (v4.9.6)
// ============================================
// Standalone BrowserWindow hosting the flowchart editor. The previous
// sidebar-panel implementation (src/sidebar/flowchart-panel.js, Ctrl+Alt+F
// through `Toggle Sidebar: Flow Chart`) has been retained as legacy but is
// no longer registered with sidebarManager — see src/renderer.js where the
// registerPanel('flowchart', …) call is now commented out. The standalone
// window mirrors the ASCII Art Generator pattern (src/ascii-generator.html
// + src/renderer/ascii-controller.js).
let flowchartGeneratorWindow = null;
function openFlowchartGenerator() {
if (flowchartGeneratorWindow) {
flowchartGeneratorWindow.focus();
return;
}
flowchartGeneratorWindow = new BrowserWindow({
width: 1100,
height: 720,
parent: mainWindow,
modal: false,
title: 'Flowchart Generator',
icon: path.join(__dirname, '../assets/icon.png'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
});
flowchartGeneratorWindow.loadFile(path.join(__dirname, 'flowchart-generator.html'));
flowchartGeneratorWindow.setMenuBarVisibility(false);
flowchartGeneratorWindow.on('closed', () => {
flowchartGeneratorWindow = null;
});
}
ipcMain.on('open-flowchart-generator', () => {
openFlowchartGenerator();
});
// ============================================
// ASCII Art Generator Window
// ============================================
+16
View File
@@ -98,6 +98,9 @@ const ALLOWED_SEND_CHANNELS = [
// ASCII generator (separate window)
'open-ascii-generator',
// Flowchart generator (standalone window — v4.9.6)
'open-flowchart-generator',
// ASCII art generator (standalone window — invoke channels)
'ascii:generate',
'ascii:list-fonts',
@@ -513,6 +516,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Generator Windows
generators: {
openAscii: () => ipcRenderer.send('open-ascii-generator'),
openFlowchart: () => ipcRenderer.send('open-flowchart-generator'),
openTable: () => ipcRenderer.send('open-table-generator'),
ascii: {
listFonts: () => ipcRenderer.invoke('ascii:list-fonts'),
@@ -524,6 +528,18 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
},
// v4.9.6 — Flowchart Generator standalone window's IO bridge. Reuses the
// existing thin text-file IPC handlers (get-user-data-path, read-text-file,
// write-text-file) which sandbox writes to <userData>. Insert at cursor
// sends the Mermaid-fenced source through the existing 'insert-content'
// channel which the renderer.js sidebar panel also uses.
flowchart: {
getUserDataPath: () => ipcRenderer.invoke('get-user-data-path'),
readFile: (p) => ipcRenderer.invoke('read-text-file', p),
writeFile: (p, content) => ipcRenderer.invoke('write-text-file', { path: p, content }),
insertAtCursor: (text) => ipcRenderer.send('insert-content', text),
},
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
});
+25 -7
View File
@@ -154,13 +154,10 @@ if (typeof window !== 'undefined' && !window.electronAPI) {
};
}
// Lazy-loaded: filesystem helpers used by the flowchart panel for
// <userData>/flowchart-session.json auto-save.
const flowchartIO = {
getUserDataPath: () => ipcRenderer.invoke('get-user-data-path'),
readFile: (p) => ipcRenderer.invoke('read-text-file', p),
writeFile: (p, content) => ipcRenderer.invoke('write-text-file', { path: p, content }),
};
// v4.9.6 — flowchart panel was removed (now a standalone window); the
// thin IPC helpers it used are now defined inside the standalone window's
// controller (src/renderer/flowchart-controller.js) under
// window.electronAPI.flowchart.*, exposed via src/preload.js.
// Use window.ModalManager if already set by script tag, otherwise require it.
// This prevents "Identifier 'ModalManager' has already been declared" when
@@ -2414,6 +2411,21 @@ document.addEventListener('DOMContentLoaded', async () => {
}),
});
// v4.9.6 — Flow Chart Editor now lives in a standalone BrowserWindow
// (src/flowchart-generator.html + src/renderer/flowchart-controller.js),
// launched from Tools → Flowchart Generator (Cmd/Ctrl+Alt+F). The sidebar
// panel implementation below is preserved as legacy and the unit tests
// (tests/flowchart-panel.test.js, tests/flowchart-canvas.test.js, etc.)
// remain green. The standalone-window controller reuses the same pure
// modules — see the UMD wrappers at the bottom of flowchart-shapes.js,
// flowchart-mermaid.js, flowchart-store.js, and flowchart-canvas.js.
//
// The `flowchart` sidebar panel is no longer registered with
// sidebarManager; the corresponding command-palette entry is also
// commented out below. To restore the sidebar experience, uncomment the
// block below AND the matching commandPalette.register('Toggle Sidebar:
// Flow Chart', …) call.
/*
// Flow Chart Editor panel — visual Mermaid flowchart builder.
// Persists to <userData>/flowchart-session.json via injected IPC helpers.
// Reuses tabManager.insertAtCursor for the "Insert at Cursor" button.
@@ -2474,6 +2486,7 @@ document.addEventListener('DOMContentLoaded', async () => {
});
},
});
*/
// Wiki-link navigation: open the target note (creating it on demand after
// confirmation). Delegated on document so it works in every tab's preview.
@@ -2874,9 +2887,14 @@ document.addEventListener('DOMContentLoaded', async () => {
commandPalette.register('Toggle Sidebar: Outline', 'Ctrl+Shift+O', () =>
sidebarManager.togglePanel('outline')
);
// v4.9.6 — sidebar Flow Chart panel is no longer registered (see comment
// block above); the standalone window owns Cmd/Ctrl+Alt+F via the Tools
// menu. Kept the legacy entry commented out for rollback.
/*
commandPalette.register('Toggle Sidebar: Flow Chart', 'Ctrl+Alt+F', () =>
sidebarManager.togglePanel('flowchart')
);
*/
commandPalette.register('Print Preview', 'Ctrl+P', () => {
const tab = tabManager.tabs.get(tabManager.activeTabId);
const preview = document.getElementById(`preview-${tab.id}`);
+249
View File
@@ -0,0 +1,249 @@
/**
* Flowchart Generator — renderer controller for the standalone window.
*
* Drives the DOM in src/flowchart-generator.html. Boots the graph store,
* hydrates from <userData>/flowchart-session.json (reusing the existing
* thin text-file IPC handlers), persists on every mutation with a
* 500 ms debounce, and wires "Insert at Cursor" to the existing
* `insert-content` IPC so the Mermaid source lands in the main editor
* wrapped in a fenced code block.
*
* Pure browser script — no require(), no node. Pure modules
* (flowchart-store / flowchart-canvas / flowchart-mermaid / flowchart-shapes)
* are loaded via <script> tags in src/flowchart-generator.html and exposed
* as window globals (window.FlowchartStore, window.FlowchartCanvas,
* window.FlowchartMermaid, window.FlowchartShapes).
*
* @module flowchart-controller
*/
(function () {
'use strict';
const api =
window.electronAPI && window.electronAPI.flowchart ? window.electronAPI.flowchart : null;
const els = {
canvasHost: document.getElementById('canvas-host'),
previewSource: document.getElementById('preview-source'),
previewRender: document.getElementById('preview-render'),
btnInsert: document.getElementById('fc-btn-insert'),
btnReset: document.getElementById('fc-btn-reset'),
status: document.getElementById('fc-status'),
};
// Guard rails — these should never be null in a correctly-launched window.
// Fail loudly with a visible status message rather than silently no-op'ing
// if the HTML or the preload bridge are misconfigured.
function fatal(msg) {
if (els.status) els.status.textContent = msg;
console.error('[flowchart-controller]', msg);
}
if (!els.canvasHost || !els.previewSource || !els.previewRender) {
fatal('Required DOM elements missing — check src/flowchart-generator.html');
return;
}
if (
!window.FlowchartStore ||
!window.FlowchartCanvas ||
!window.FlowchartMermaid ||
!window.FlowchartShapes
) {
fatal('Flowchart pure modules not loaded — verify script tags in src/flowchart-generator.html');
return;
}
if (!api) {
fatal('window.electronAPI.flowchart missing — check src/preload.js');
return;
}
const { create: createStore } = window.FlowchartStore;
const { createCanvas } = window.FlowchartCanvas;
const { toMermaid } = window.FlowchartMermaid;
// FlowchartShapes is intentionally unused here but its presence is
// required for createCanvas() to function; the guard above guarantees it.
const PREVIEW_DEBOUNCE_MS = 250;
const PERSIST_DEBOUNCE_MS = 500;
const PERSISTENCE_FILENAME = 'flowchart-session.json';
let _userDataPath = null;
let _persistenceFile = null;
let _previewTimer = null;
let _persistTimer = null;
let _store = null;
let _canvas = null;
function setStatus(msg) {
if (els.status) els.status.textContent = msg || '';
if (msg) setTimeout(() => setStatus(''), 2500);
}
function runPreview() {
if (!_store || !els.previewSource || !els.previewRender) return;
const graph = _store.getGraph();
const source = toMermaid(graph);
els.previewSource.textContent = source;
// v4.9.6 — text preview only. The SVG canvas on the left is the
// visual preview (hand-rolled, no Mermaid runtime needed in this
// window's renderer). Mirrors the v4.9.5 fix that forced a light
// surface to guarantee visibility.
els.previewRender.textContent = '';
}
function debouncedPreview() {
if (_previewTimer) clearTimeout(_previewTimer);
_previewTimer = setTimeout(() => {
_previewTimer = null;
runPreview();
}, PREVIEW_DEBOUNCE_MS);
}
function debouncedPersist() {
if (_persistTimer) clearTimeout(_persistTimer);
_persistTimer = setTimeout(() => {
_persistTimer = null;
if (!_store || !_persistenceFile) return;
api
.writeFile(_persistenceFile, _store.serialize())
.then(() => setStatus('Saved'))
.catch((err) => setStatus(`Save failed: ${err && err.message ? err.message : err}`));
}, PERSIST_DEBOUNCE_MS);
}
async function bootstrap() {
// Resolve the userData path ONCE on mount. The persistence path is
// interpolated into a string on every read/write; calling the async
// api.getUserDataPath() directly would coerce the Promise to
// "[object Promise]" and break the userData sandbox check in
// main.js:write-text-file.
try {
_userDataPath = await api.getUserDataPath();
} catch (err) {
fatal(`Could not resolve userData path: ${err && err.message ? err.message : err}`);
return;
}
_persistenceFile = `${_userDataPath}/${PERSISTENCE_FILENAME}`;
_store = createStore({
persistencePath: _persistenceFile,
readFile: api.readFile,
writeFile: api.writeFile,
now: () => Date.now(),
});
_canvas = createCanvas(els.canvasHost, _store, {
onEdgeClick: (edgeId) => {
const edge = _store.getGraph().edges.find((e) => e.id === edgeId);
if (!edge) return;
const nextKind = window.prompt('Edge kind (solid, dotted, thick):', edge.kind);
if (nextKind && ['solid', 'dotted', 'thick'].includes(nextKind)) {
_store.setEdgeKind(edgeId, nextKind);
}
const nextLabel = window.prompt('Edge label (empty to clear):', edge.label || '');
if (nextLabel !== null) {
_store.setEdgeLabel(edgeId, nextLabel);
}
},
onNodeClick: () => {
// Canvas already paints the .selected highlight; nothing else
// needed here for selection state.
},
onShapeMenu: (nodeId) => {
const next = window.prompt(
'New shape (process, decision, terminator, subroutine, document):'
);
if (next) _store.setNodeKind(nodeId, next);
},
});
_store.subscribe(() => {
debouncedPreview();
debouncedPersist();
});
// Hydrate from disk (defensively — corrupt JSON is caught by the store).
try {
const json = await api.readFile(_persistenceFile);
if (json) _store.deserialize(json);
} catch (err) {
console.warn('[flowchart-controller] failed to read session:', err);
}
if (els.btnInsert) {
els.btnInsert.addEventListener('click', () => {
if (!_store) return;
const source = toMermaid(_store.getGraph());
const fenced = '```mermaid\n' + source + '\n```';
if (api.insertAtCursor) api.insertAtCursor(fenced);
setStatus('Inserted');
});
}
if (els.btnReset) {
els.btnReset.addEventListener('click', () => {
if (!_store) return;
if (!window.confirm('Clear all nodes and edges?')) return;
_store.deserialize({ nodes: [], edges: [] });
setStatus('Reset');
});
}
// Keyboard shortcuts — Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z / Delete / Backspace.
document.addEventListener('keydown', (ev) => {
if (!_store) return;
const meta = ev.ctrlKey || ev.metaKey;
if (meta && !ev.shiftKey && ev.key.toLowerCase() === 'z') {
ev.preventDefault();
_store.undo();
return;
}
if (meta && ev.shiftKey && ev.key.toLowerCase() === 'z') {
ev.preventDefault();
_store.redo();
return;
}
if (ev.key === 'Delete' || ev.key === 'Backspace') {
// Delete applies to the canvas's currently-selected node/edge.
// The canvas doesn't expose selection state directly; we read
// it from the DOM .selected class.
const selectedNode = els.canvasHost.querySelector('.flowchart-node.selected');
if (selectedNode) {
const id = selectedNode.getAttribute('data-node-id');
if (id) {
ev.preventDefault();
_store.removeNode(id);
}
return;
}
const selectedEdge = els.canvasHost.querySelector('.flowchart-edge.selected');
if (selectedEdge) {
const id = selectedEdge.getAttribute('data-edge-id');
if (id) {
ev.preventDefault();
_store.disconnect(id);
}
}
}
});
runPreview();
setStatus('Ready');
}
// Expose a minimal handle for tests (mirrors ascii-controller.js pattern).
window.FlowchartController = {
bootstrap,
get store() {
return _store;
},
get canvas() {
return _canvas;
},
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
bootstrap();
}
})();