feat(flowchart): visible selection toolbar + console-log diagnostics; v4.10.0

Even after the v4.9.9 inline-modal fix the user kept reporting 'no fix still'
because hidden right-click context menus and window.prompt calls remain
unreliable in Electron renderer contexts. This release ships a *visible*
floating selection toolbar inside the canvas panel so the primary
interactions are not hidden behind modals or context menus.

- New <div id="fc-selection-toolbar"> inside #canvas-host, hidden until a
  node or edge is selected. Renders:
    * 5 shape buttons (Process / Decision / Terminator / Subroutine /
      Document) when a node is selected, calling store.setNodeKind on
      click. The active shape is highlighted.
    * 3 edge-kind buttons (Solid / Dotted / Thick) when an edge is
      selected, calling store.setEdgeKind.
    * An always-visible label input that mirrors the selected item's
      label and writes back via store.setNodeLabel / store.setEdgeLabel
      with a 100ms debounce.
    * A red Delete button calling store.removeNode / store.disconnect.
- Selection state now tracked at the controller level (_selectedId +
  _selectedKind) and exposed via window.FlowchartController so the
  keyboard Delete/Backspace shortcut and the toolbar share one source
  of truth.
- Console-log instrumentation on every canvas event (pointerdown with
  altKey, pointerup with drag result, dblclick, contextmenu, selection
  changes, and every bootstrap phase) so the user can open DevTools
  (Ctrl+Shift+I) and verify Alt+drag and double-click actually fire.
- promptInline / confirmInline kept as advanced fallback for the
  right-click 'change shape' path; the toolbar is now the primary
  interaction surface.
- Preview-render pane now shows a static info card explaining that the
  canvas on the left is the rendered chart (previously blank).
- 6 new tests in tests/flowchart-controller.test.js — toolbar hidden
  by default, selecting a node populates shape buttons + label input +
  Delete, clicking a shape button updates the kind, label input is
  debounced, edge selection populates edge-kind buttons, Delete button
  removes the selected node.

Total tests: 1127 passing (was 1121; +6 new). The 11 pre-existing
PDFOperations failures are unrelated to this change.
This commit is contained in:
2026-09-15 00:40:01 +05:30
parent 81e8561403
commit b81119026e
6 changed files with 559 additions and 55 deletions
+1 -1
View File
@@ -257,4 +257,4 @@ Amit Haridas (amit.wh@gmail.com)
## Version
v4.9.9
v4.10.0
+79 -2
View File
@@ -1,8 +1,24 @@
# PanConverter - Updates & Changelog
## Version 4.10.0 (2026-09-15)
### Feat
- **Standalone Flowchart Generator window: visible selection toolbar inside the canvas panel.** Even after the v4.9.9 inline-modal fix, the user kept reporting "no fix still" because hidden right-click context menus and `window.prompt` calls remain unreliable in Electron renderer contexts. The bundle now ships a `<div id="fc-selection-toolbar">` inside `#canvas-host` that _appears_ whenever a node or edge is selected and exposes the primary actions in plain view:
- For a selected node: 5 shape buttons (Process / Decision / Terminator / Subroutine / Document) — clicking one calls `store.setNodeKind(id, kind)` and re-renders. The active shape is highlighted.
- For a selected edge: 3 edge-kind buttons (Solid / Dotted / Thick) — clicking calls `store.setEdgeKind(id, kind)`.
- Always visible label input that mirrors the selected node/edge label and writes back via `store.setNodeLabel` / `store.setEdgeLabel` with a 100ms debounce.
- Red Delete button that calls `store.removeNode` / `store.disconnect` and collapses the toolbar.
- **Console-log diagnostics on every canvas event.** Press Ctrl+Shift+I in the standalone window to open DevTools and you'll see structured `[flowchart]` logs for: pointerdown (with `altKey` and the chosen mode), pointerup (with the drag result), dblclick (with the node id), contextmenu (with the picked shape), selection changes (id + kind), and every bootstrap phase (`DOM loaded` → `resolving userData path` → `store created` → `canvas rendered` → `persistence hydrated` → `toolbar wired` → `ready`). Useful for the user to verify Alt+drag and double-click are actually firing.
- **Preview-render pane shows an info card explaining the layout.** Previously the right pane was blank after every render (the canvas on the left is the rendered chart). Now it shows: "Visual chart is rendered on the left canvas panel. Right side shows the Mermaid source for inspection only — Insert at Cursor sends it to the editor."
- **`promptInline` / `confirmInline` kept as advanced fallback.** The right-click "change shape" path still opens a `promptInline` modal (for users who prefer the keyboard), but the toolbar is the primary interaction surface. Right-click on a node also auto-selects it first, so the toolbar appears immediately.
- **Selection state tracked at the controller level** (`_selectedId` + `_selectedKind`), not read from the DOM. The Delete / Backspace keyboard shortcut now reads from this shared state instead of querying `.flowchart-node.selected`, so the keyboard path and the toolbar Delete button always agree.
- **6 new tests in `tests/flowchart-controller.test.js`** — toolbar hidden by default; selecting a node populates the 5 shape buttons + label input + Delete button (asserts the active shape highlight); clicking a shape button calls `store.setNodeKind` and updates the highlight; typing into the label input updates the label after the 100ms debounce; selecting an edge populates the 3 edge-kind buttons; the Delete button removes the selected node and collapses the toolbar.
## Version 4.9.9 (2026-09-15)
### Fix
- **Standalone Flowchart Generator window: replaced broken `window.prompt` / `window.confirm` with an inline DOM-modal dialog.** Electron renderer contexts (the BrowserWindow hosting the standalone window) return `undefined` when `window.prompt(...)` or `window.confirm(...)` is called — meaning every shape menu, edge-kind change, edge-label edit, and reset confirmation silently did nothing. The bundle now ships two helpers (`promptInline`, `confirmInline`) that build a small overlay with a styled title, message, OK / Cancel buttons, and Enter / Escape / backdrop-click handling. The four call sites (`onEdgeClick` for kind + label, `onShapeMenu`, and the Reset click handler) are now `async` and await the helpers.
- New `window.FlowchartModals = { promptInline, confirmInline }` export on the bundle so jsdom tests can drive the modals directly without rebuilding the IIFE.
- 6 new tests in `tests/flowchart-controller.test.js` — OK / Cancel / Escape resolution paths for `promptInline`, OK / Cancel for `confirmInline`, and the `danger` flag renders a red "Delete" primary button.
@@ -10,6 +26,7 @@
## Version 4.9.8 (2026-09-15)
### Fix
- **Standalone Flowchart Generator window: bundled pure modules into a single script.** Even after the v4.9.7 `window.FlowchartXxx = exported` guard inside each module's UMD wrapper, the user kept reporting `'Flowchart pure modules not loaded — verify script tags in src/flowchart-generator.html'` in the standalone window's status bar. Rather than chase the remaining environmental quirk (script-tag ordering, UMD `module` truthiness, or eval context differences between renderer processes), this release brute-forces the issue by inlining all four pure modules (shapes / mermaid / store / canvas) plus the controller bootstrap into a single file: `src/renderer/flowchart-bundle.js`.
- New `src/renderer/flowchart-bundle.js` — one IIFE, ~720 lines. Sets `window.FlowchartShapes`, `window.FlowchartMermaid`, `window.FlowchartStore`, `window.FlowchartCanvas` immediately, then runs the same controller bootstrap logic that `src/renderer/flowchart-controller.js` exposes.
- `src/flowchart-generator.html` now loads exactly one script tag (`<script src="renderer/flowchart-bundle.js"></script>`) instead of five. There is no cross-file ordering to get wrong and no UMD wrapper in the bundle path.
@@ -19,6 +36,7 @@
## Version 4.9.7 (2026-09-14)
### Fix
- **Standalone Flowchart Generator window now loads (was: 'modules not loaded' fatal error).** Each of the four pure modules (`flowchart-shapes.js`, `flowchart-mermaid.js`, `flowchart-store.js`, `flowchart-canvas.js`) ships with a UMD wrapper. The original wrapper assigned `window.FlowchartXxx` only in the `else` branch — i.e. when `module` was undefined. But the renderer runs with `nodeIntegration: true`, so `module` is always truthy in that environment and the `else` branch never ran, leaving `window.FlowchartShapes` / `window.FlowchartMermaid` / `window.FlowchartStore` / `window.FlowchartCanvas` undefined. The standalone window's controller (`src/renderer/flowchart-controller.js`) then aborted with `fatal('Flowchart pure modules not loaded — verify script tags in src/flowchart-generator.html')`.
- Fix: every pure module's UMD wrapper now has a second `if (typeof window !== 'undefined') { window.FlowchartXxx = exported; }` block appended AFTER the CommonJS branch. Both branches can run (the CommonJS branch keeps the legacy sidebar panel working under `require()`; the new branch unconditionally exposes the global in the renderer). The factory IIFE is unchanged, so the public surface of every module is identical to v4.9.6 — no behavioural change.
- New regression guard: 4 source-grep tests in `tests/flowchart-controller.test.js` assert each module's source file contains the `window.FlowchartXxx = exported` assignment so a future refactor can't silently drop the global again.
@@ -26,22 +44,26 @@
## Version 4.9.6 (2026-09-14)
### Refactor
- **Flowchart editor is now a standalone window, not a sidebar panel.** Five fix rounds (v4.9.1 → v4.9.5) couldn't make the sidebar panel feel right — at 280 px sidebar with the canvas + preview split to ~175 px each, plus the editor-container hiding 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/ascii-generator.html` + `src/renderer/ascii-controller.js`).
- New `src/flowchart-generator.html` — standalone HTML with its own header, toolbar (Insert at Cursor / Reset), canvas host, and preview host (text-only — the canvas on the left IS the visual preview). All stylesheet `href`s are `src/`-relative — no `../` escape (lesson learned from v4.9.2). Forced light surface (`background: #fafafa !important; color: #1f2328 !important`) on the canvas + preview regardless of the project's body theme, mirroring the v4.9.5 CSS fix that traded theme consistency for guaranteed visibility.
- New `src/renderer/flowchart-controller.js` — pure browser IIFE. Wires the canvas + preview, hydrates from `<userData>/flowchart-session.json` once on mount, persists on every store mutation with a 500 ms debounce. Insert at Cursor wraps the generated `flowchart TD` source in a fenced ```` ```mermaid ```` block and sends it through the existing `insert-content` IPC channel — same one the renderer.js sidebar panel used. Keyboard shortcuts (Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z / Delete / Backspace) handled at document level.
- New `src/renderer/flowchart-controller.js` — pure browser IIFE. Wires the canvas + preview, hydrates from `<userData>/flowchart-session.json` once on mount, persists on every store mutation with a 500 ms debounce. Insert at Cursor wraps the generated `flowchart TD` source in a fenced ` ```mermaid ` block and sends it through the existing `insert-content` IPC channel — same one the renderer.js sidebar panel used. Keyboard shortcuts (Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z / Delete / Backspace) handled at document level.
- New `openFlowchartGenerator()` in `src/main.js` — `BrowserWindow` (1100×720, parent: mainWindow, `contextIsolation: true, nodeIntegration: false`) launched by an `ipcMain.on('open-flowchart-generator')` listener. Tools menu now has a "Flowchart Generator" entry with accelerator `CmdOrCtrl+Alt+F`.
- New `window.electronAPI.flowchart.*` namespace in `src/preload.js` — `getUserDataPath` / `readFile` / `writeFile` / `insertAtCursor`. Reuses the existing thin IPC handlers (`get-user-data-path`, `read-text-file`, `write-text-file`) which already sandbox writes to `<userData>`.
- 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 (the legacy sidebar panel still loads them via `require()`) and as browser globals (the standalone window loads them via `<script>` tags attached to `window.FlowchartShapes`, etc.). The CommonJS shape is preserved — no behavioural change to the 73 flowchart unit tests in `tests/flowchart-*.test.js`.
### Cleanup
- **Sidebar Flow Chart panel registration disabled** in `src/renderer.js` — the `sidebarManager.registerPanel('flowchart', …)` call and the matching `commandPalette.register('Toggle Sidebar: Flow Chart', …)` entry are now both commented out. The legacy panel implementation (`src/sidebar/flowchart-panel.js`) and its unit tests (`tests/flowchart-panel.test.js`) are kept untouched for rollback — re-enabling is a one-step uncomment in `src/renderer.js`. The unused `flowchartIO` helper that the panel needed was also removed.
### Tests
- New `tests/flowchart-controller.test.js` (10 tests) — verifies the standalone window's HTML doesn't `../`-escape any stylesheet, `bootstrap()` resolves `getUserDataPath` exactly once on mount, reads `<userData>/flowchart-session.json` on mount, hydrates the store from a saved session, wraps Insert-at-Cursor output in a `mermaid` fenced block, Reset clears nodes/edges (with confirm) and persists the empty graph (without confirm). Two regression tests assert that `src/renderer.js` no longer contains a live `sidebarManager.registerPanel('flowchart', …)` call or a live `commandPalette.register('Toggle Sidebar: Flow Chart', …)` entry.
## Version 4.9.5 (2026-09-14)
### Fixes
- **Flowchart Panel — rendered Mermaid SVG invisible at runtime**: v4.9.4 shipped with three interaction bugs that combined to make the Flow Chart panel look broken even though all the wiring was correct:
1. **Render target had zero height.** `.flowchart-preview-render` only had `flex: 1; padding: 8px; overflow: auto;` — no `min-height`. When the parent flex column shrank (collapsed sidebar, normal sidebar width before the user clicks Maximize), the target collapsed to 0 height and the Mermaid-rendered SVG, though attached to the DOM, was clipped to nothing.
2. **Dark-on-dark surfaces.** The canvas host and preview host inherited the project's `body.theme-concreteinfo` dark theme. Mermaid's `dark` theme was selected automatically in `src/renderer.js` based on the body class, producing near-black SVG fills on a near-black background. Node labels "Node" were barely legible.
@@ -50,14 +72,17 @@
Fix in three places:
- `src/styles-sidebar.css` — gave `.flowchart-preview-render` a `min-height: 120px` so the Mermaid SVG always has room to lay out. Added a `!important` light background (`#fafafa` / `#1f2328` text) to `.flowchart-canvas-host` and `.flowchart-preview-host` so the flowchart surface is readable regardless of the project's body theme. Forced explicit fills and strokes on `.flowchart-node rect` / `.flowchart-node polygon` / `.flowchart-node text` / `.flowchart-edge` (white fill, dark stroke, dark text). Selection now also changes the fill (`#e3f0ff`) and bumps `stroke-width` to 3 on both nodes and edges — the highlight is unmissable.
- `src/renderer.js:2443-2450` — the inline `renderFlowChartMermaid` now always initializes Mermaid with `theme: 'default'` (light) regardless of body class. Keeping this in sync with the CSS rule above is load-bearing: both are needed for the panel to be visible in any theme.
- **Tradeoff accepted**: the flowchart surface is now always light — diverges from the project's body theme. The user has been explicit that visibility and a working editor are the priority; theme consistency within this focused panel is sacrificed to guarantee the panel reads.
### Tests
- `tests/flowchart-panel.test.js` — new `describe('flowchart-panel: render target sizing (v4.9.5 regression)')` block (3 tests) reading the shipped CSS to assert: (a) `.flowchart-preview-render` has a non-zero `min-height`, (b) `.flowchart-canvas-host` / `.flowchart-preview-host` carry a forced background declaration with `!important`, (c) `.flowchart-node.selected rect/polygon` carry an explicit fill and `stroke-width >= 3`. Reading the stylesheet directly mirrors what the runtime loads via `<link rel="stylesheet">` and sidesteps jsdom's incomplete layout engine.
## Version 4.9.4 (2026-09-14)
### Fixes
- **Flowchart Panel — selection was invisible**: clicking a node or edge updated the canvas's internal `selectedNodeId` / `selectedEdgeId` but never repainted, so the `.flowchart-node.selected` / `.flowchart-edge.selected` CSS highlight only appeared when the user actually dragged (which triggers `store.subscribe` → `render()`). A bare click left the canvas looking unchanged, and the panel's own `selectedNodeId` (used by the panel-scoped Delete/Backspace shortcut) stayed `null`, so Delete on a freshly-clicked node silently no-op'd. Wired `opts.onNodeClick(id)` end-to-end:
- `src/flowchart/flowchart-canvas.js` — added an `opts.onNodeClick` callback parallel to the existing `opts.onEdgeClick`; on click, both branches now call a new surgical `applySelectionHighlight()` that toggles the `.selected` class on the existing `<g data-node-id>` / `<line data-edge-id>` elements without going through `render()` (which would detach the very element the user's pointer is still on, breaking `pointermove`/`pointerup` bubbling during a drag).
- `src/sidebar/flowchart-panel.js` — the panel's `onNodeClick` handler mirrors the id into the panel's `selectedNodeId` (clearing `selectedEdgeId`) so Delete/Backspace routes correctly. Same symmetry was already in place for `onEdgeClick`.
@@ -67,6 +92,7 @@
- The class lookup walks up from the panel container to the nearest `.main-content` ancestor (with a `document.querySelector('.main-content')` fallback) so the panel doesn't need to know whether the sidebar lives inside `#sidebar` or any future container.
### Tests
- `tests/flowchart-panel.test.js` — added two new `describe` blocks (8 tests total):
- "selection wiring (canvas click → panel state + SVG class)": clicking a node applies `.selected` to the matching `<g>`, clicking a second node moves `.selected` from the first to the second, clicking an edge applies `.selected` to the matching `<line>`, and a regression test verifying Delete removes a freshly-clicked node (was broken in v4.9.3 because the panel's `selectedNodeId` was never updated by canvas clicks).
- "maximize / takeover": the maximize button is exposed in the toolbar, clicking it toggles `.flowchart-takeover` on `.main-content` and flips the button label/active class, and `destroy()` clears the class so the editor stays usable.
@@ -75,14 +101,17 @@
## Version 4.9.3 (2026-09-14)
### Fixes
- **Flowchart Panel — preview pane accumulated raw Mermaid source**: when the user fired several `addNode` mutations within the 250 ms preview debounce, `mermaid.run({ nodes: [div] })` is async, so the previous render's `<div class="mermaid">` (still carrying the source text) was sitting in `.flowchart-preview-render` when the next render cleared the target. The first render's eventual `element.innerHTML = svg` landed on a detached node, but the visible preview pane had a stack of stale `<div class="mermaid">` elements. Fixed in `src/renderer.js:2423-2449`: switched the inline `renderFlowChartMermaid` to `replaceChildren()` (more idiomatic than `innerHTML = ''`) and added a per-target `WeakSet` in-flight tracker that keeps the previous render's closure from racing the new render — its eventual `element.innerHTML = svg` is harmless on a detached node, and the new render always starts from a clean slate.
### Tests
- `tests/flowchart-panel.test.js` — added a second regression test (`preview-source pre never duplicates across debounced mutations even with in-flight mermaid.render`) that fires 7 mutations at 10 ms intervals (well inside the 250 ms debounce), uses a `renderMermaid` mock that mirrors the real mermaid.run closure (captures the input div, asynchronously sets `innerHTML = svg` on it regardless of DOM connection), and asserts the `<pre>` contains exactly one copy of the latest source and the render target holds exactly one `<div class="mermaid">` child whose first element child is the latest `<svg>`.
## Version 4.9.2 (2026-09-14)
### Fixes
- **Standalone ASCII Art Generator — broken stylesheet path**: `src/ascii-generator.html:7` linked `<link rel="stylesheet" href="../fonts.css" />`. The HTML loads via `BrowserWindow.loadFile(path.join(__dirname, 'ascii-generator.html'))` where `__dirname` is `src/`, so `../fonts.css` escaped the `src/` directory and resolved to a non-existent `<project>/fonts.css`. Changed to `fonts.css` (same file, src/-relative — mirrors `src/index.html:29`). The window rendered without its font rules, leaving the header in the fallback system stack.
- **Standalone ASCII Art Generator — dead Box/Templates UI**: the controller (`src/renderer/ascii-controller.js`) shipped three mode tabs (`Text Banner` / `Box-Frame` / `Templates`), 18 `.template-btn[data-template]` buttons, and a Box form (`#box-text` / `#box-style` / `#box-padding`) but wired none of them. Clicking any tab or button was a no-op. Wired all of them:
- `setMode(mode)` toggles `.active` on the right `.mode-tab` and the matching `.mode-section` (`text-mode` / `box-mode` / `templates-mode`).
@@ -91,16 +120,19 @@
- All 11 brief-required behaviours (text-input / font-picker / font-search / insert / copy / save / generate, last-font persistence, debounced preview) remain intact.
### Tests
- New `tests/ascii-controller.test.js` — 6 tests covering the stylesheet path (no `..` escape), pure box renderer (single + ascii styles), mode-tab switching (Box and Templates), and template button → preview wiring.
## Version 4.9.1 (2026-09-14)
### Fixes
- **Flowchart Panel save failed**: `getUserDataPath()` IPC was not awaited in `src/renderer.js:2439-2449`, so the persistence path was computed as `"[object Promise]/flowchart-session.json"` and rejected by the `write-text-file` userData sandbox. Pre-resolved the path on panel register and cached it; persistence (read and write) now works correctly.
## Version 4.9.0 (2026-09-14)
### New: Visual Flow Chart Editor (Sidebar panel → "Flow Chart")
- Pure `flowchart-store.js` — graph data store with bounded undo/redo (depth 50)
and injectable persistence IO (testable without touching the filesystem)
- 5 node shapes (process, decision, terminator, subroutine, document) × 3 edge
@@ -123,6 +155,7 @@
- 73 new tests
### New: ASCII Art Generator upgrade
- Pure `AsciiArt` orchestrator (`generate / listFonts / getFontMeta`) unifying
hand-coded fonts + figlet + 19 named templates behind one API
- 17 hand-coded font tables extracted from inline renderer code (5 existing +
@@ -131,7 +164,7 @@
- `figlet@^1.8.0` dep with lazy-load + cache adapter (≥328 bundled fonts; pure
JS, no native bindings)
- 6 IPC handlers: `ascii:generate / list-fonts / get-font-meta / save / copy /
last-font`
last-font`
- Standalone window rewrite: removed the ~385-line inline script; added a
searchable font picker, Copy to Clipboard, and Save to File
- Major cleanup: -1029 net lines of dead code (the in-app modal
@@ -141,6 +174,7 @@
- 97 new tests
### New: Editor Theme Registry (extension)
- 12 new themes added: Catppuccin Latte / Frappé / Macchiato / Mocha, One Light,
Tokyo Night Storm, Synthwave '84, Outrun, Winter is Coming (Light + Dark),
Solarized Dark High Contrast, Spring Light
@@ -154,6 +188,7 @@
- Aria-friendly: the high-contrast option is announced to assistive tech
### Bug fixes
- Plan 3: rail button tooltip `Ctrl+Alt+F` now actually wired (the shortcut
existed but the panel toggle was missing)
- Plan 3: `destroy()` cleanup on panel unmount — timers, listeners, and the
@@ -163,6 +198,7 @@
actually restored from figlet's bundled fonts
### Housekeeping
- 1093 tests passing across 36 snapshots in 90 suites
- Lint + Prettier clean across all 3 plans
- Linux build verified end-to-end (AppImage + deb + snap produced)
@@ -172,6 +208,7 @@
## Version 4.7.1 (2026-09-05)
### New: Export Themes (Word + PDF)
- Theme picker in the export dialog (basic and advanced mode) for PDF and DOCX:
**Default (Pandoc), Modern, Classic, Sepia, Minimal, Elegant**
- PDF: LaTeX header recolors/reformats headings + links (xcolor/titlesec,
@@ -181,10 +218,12 @@
Default instead of failing the export
### Branding
- New M↓ brand identity: app icons, favicons, tray icon, welcome mark,
README wordmark (vector kit in assets/markdown-converter-assets/)
### Fixes
- **Windows**: pdfjs text/image extraction failed on Windows —
standardFontDataUrl is now a proper file:// URL (raw backslash paths
failed pdfjs's trailing-slash URL validation)
@@ -202,6 +241,7 @@
## Version 4.7.0 (2026-09-05)
### Bundling & Legal Compliance
- **MarkItDown is now bundled**: `npm run bundle:markitdown` freezes Microsoft's
markitdown (MIT) + embedded Python runtime into a single ~75MB per-platform
binary (`bin/<platform>/markitdown`) via PyInstaller (ML extras excluded);
@@ -225,6 +265,7 @@
## Version 4.6.1 (2026-09-05)
### New Features
- **MarkItDown import** — "File → Import with MarkItDown (Any Format)…" embeds
Microsoft's [markitdown](https://github.com/microsoft/markitdown) (MIT) as an
any-file → Markdown path: PDF, DOCX, PPTX, XLSX, Outlook .msg/.eml, EPUB,
@@ -243,6 +284,7 @@
without a trailing `/v1`
### Bug Fixes
- File → Open PDF crashed the PDF editor (null operation matched no section; now
defaults to Merge)
- Backlinks panel required the wrong module path (failed at registration)
@@ -258,6 +300,7 @@
### New Features
#### AI Assistant Plugin (multi-provider)
- Chat sidebar panel with rolling conversation history and insert-reply-into-document
- Providers: OpenAI, Anthropic, Ollama, LM Studio, and any OpenAI-compatible endpoint
- All provider traffic proxied through the main process — API keys never enter the renderer and the CSP stays closed to AI endpoints
@@ -265,30 +308,36 @@
- Answers the writing-studio `ai:analyze` contract, finally enabling the Proofread panel
#### Collaboration Plugin (inline comments)
- Anchor-based comments stored in `.comments/` sidecar files (never exported, never committed)
- Comments sidebar panel: add at cursor, list, resolve, delete, jump to anchor
- Drift detection flags moved/edited anchors; F8 navigates to the next open comment
#### Local Knowledge Base (wiki-links + backlinks)
- `[[Note]]`, `[[Note|alias]]`, `[[Note#section]]` render as links in the preview (code blocks excluded)
- Clicking a wiki-link opens the note or offers to create it
- Backlinks sidebar panel scans the folder (bounded BFS) for documents linking to the current one
#### Crash Recovery / Session Restore
- Open tabs (paths + unsaved buffer content) snapshotted to localStorage, debounced on edits, on tab changes, on unload, and once a minute
- Restore prompt on launch with per-tab restore, clean-fresh option, and 2MB content budget
#### Document Version History
- Every save snapshots the previous on-disk content to `<userData>/versions/`
- History sidebar panel: list, restore (with safety snapshot), unified diff vs. current, delete, manual "save version now"
- Per-document pruning (20 versions), path-hash storage, id-validated reads
#### Editor
- Vim keybindings (View → Vim Mode, persisted, live toggle via CodeMirror Compartment)
- Snippet Tab-expansion: type a snippet name and press Tab to insert it
- Zen Mode word-goal setter (the HUD progress bar finally has UI)
#### Export / Conversion
- **Real PDF encryption**: pdf-lib swapped for @cantoo/pdf-lib — encrypt/decrypt/permissions now actually work (UI auto-enables via the capability probe)
- **XLSX export**: markdown tables → native Excel workbook, one sheet per table (no Pandoc needed)
- **ODT headers/footers + page size**: real ODF styles.xml patching replaces the empty stub
@@ -297,6 +346,7 @@
- Writing heatmap (GitHub-style 30-day grid) in the writing-studio Goals panel
#### Platform
- Quick Note global scratchpad (Ctrl+Alt+Q, works when unfocused; appends to `notes/quick-notes.md`)
- Deep link protocol `markdownconverter://open?path=…`
- REPL confirmation dialog before first code execution per language per session (unsandboxed-execution guard rail)
@@ -304,6 +354,7 @@
- Plugin sidebar panels get automatic rail icons via `registerPanel({icon})`
### Bug Fixes
- `Ctrl+Shift+P` collision: PDF (Enhanced) export now `Ctrl+Alt+Shift+P`; Command Palette keeps `Ctrl+Shift+P`
- Universal Converter's Pandoc tool no longer always reports "not installed" (`checkConverterAvailable` gained a pandoc case with bundled-binary check)
- CLI headless export: removed dangling `--css` / `--reference-doc` flags that made pandoc exit with an error; `--self-contained` replaced with `--standalone` (Pandoc 3.x)
@@ -311,11 +362,13 @@
- Removed duplicate `styles-zen.css` include
### Security
- AI provider requests carry size caps (200KB prompt), timeouts (120s), and user-safe error surfaces
- Version-history reads validate ids against traversal; history listing requires a valid document path
- PlantUML local rendering removes the diagram-text exfiltration path when a local CLI exists (CVE-MC-007 follow-up)
### Tests
- New suites: OdtStyling, AiProviders, ai-assistant prompts, collaboration comment-store, wiki-links/backlinks, session-store, XlsxExporter, VersionHistory
- PDFOperations encryption tests rewritten for the real-encryption reality
@@ -324,12 +377,14 @@
## Version 4.0.0 (2026-03-04)
### Major Changes
- **CodeMirror 6 Editor** — Replaced textarea with CodeMirror 6 featuring syntax highlighting, code folding, bracket matching, multiple cursors, and auto-indent
- **Sidebar Panel System** — Collapsible sidebar with File Explorer, Git, Snippets, and Templates panels
- **Command Palette** — Ctrl+Shift+P to search and execute all app actions
- **Code Execution (REPL)** — Run JavaScript, Python, and Bash code blocks directly from the preview
### New Features
- Print Preview dialog with paper size, orientation, margins, scale, and page range controls
- Image paste from clipboard and drag-drop support with auto-save to assets folder
- Document templates library (10 templates: blog post, meeting notes, tech spec, changelog, README, project plan, API docs, tutorial, release notes, comparison)
@@ -342,6 +397,7 @@
- Breadcrumb bar showing current file path
### New Export/Import Formats
- Reveal.js slides (.html)
- Beamer slides (.pdf)
- Confluence/Jira wiki markup (.txt)
@@ -349,12 +405,14 @@
- Developer formats: JSON, YAML, XML, TOML
### Security
- Content Security Policy (CSP) meta tag
- File size validation (50MB limit)
- Error message sanitization (stripped file paths)
- Conversion rate limiting (2-second debounce)
### Dependencies Updated
- marked: 16.x to 17.x (with marked-highlight extension)
- pdfjs-dist: 3.x to 5.x (new worker model)
- html2pdf.js: 0.10 to 0.14
@@ -362,10 +420,12 @@
- dompurify, docx, and others updated to latest
### Testing
- 80 tests across 7 test suites
- New tests for sidebar manager, command palette, print preview, markdown extensions, and utility functions
### Breaking Changes
- Editor is now CodeMirror 6 (replaces textarea)
- marked API changed to use marked.use() instead of marked.setOptions()
- pdfjs-dist upgraded to v5 with new worker model
@@ -377,6 +437,7 @@
### 🎨 UI/UX Improvements
#### Subtle & Small Preview Popout Button
- Redesigned popout button with minimalist aesthetic
- Removed border for cleaner appearance
- Reduced size: 11px font, 2px×6px padding (previously 14px font, 4px×8px padding)
@@ -385,6 +446,7 @@
- **File**: `src/styles.css:195-211`
#### Simplified Table Headers in Preview
- Removed gradient background from table headers in modern theme
- Changed from `var(--primary-gradient)` (purple gradient) to simple light gray (#f0f0f0)
- Updated text color to dark (#333333) for better readability
@@ -394,9 +456,11 @@
### 📥 Enhanced Import Capabilities
#### Comprehensive Format-to-Markdown Conversion
Dramatically expanded the "Import Document" feature to support 30+ file formats:
**Supported Formats:**
- **Documents**: DOCX, ODT, RTF, HTML, HTM, TEX, EPUB, PDF, TXT
- **Presentations**: PPTX, ODP
- **Markup Languages**: RST, Textile, MediaWiki, Org-mode, AsciiDoc, TWiki, OPML
@@ -407,6 +471,7 @@ Dramatically expanded the "Import Document" feature to support 30+ file formats:
- **Data Formats**: CSV, TSV, JSON
**Format-Specific Optimizations:**
- PDF text extraction with XeLaTeX engine
- CSV/TSV automatic table conversion
- JSON structure handling
@@ -418,6 +483,7 @@ Dramatically expanded the "Import Document" feature to support 30+ file formats:
### 🎨 Exhaustive ASCII Art Generator
#### 5 New Text Banner Styles
Complete alphabet (A-Z) and numbers (0-9) support for all styles:
1. **Standard** - Classic ASCII art with slashes and underscores
@@ -429,15 +495,18 @@ Complete alphabet (A-Z) and numbers (0-9) support for all styles:
**File**: `src/renderer.js:3397-3537`
#### 19 Professional ASCII Templates
Organized into 4 categories with expanded options:
**Arrows & Flow (4 templates):**
- Arrow Right - Horizontal flow indicators
- Arrow Down - Vertical flow indicators
- Decision - Binary decision diagrams
- Process Flow - Multi-step process visualization
**Diagrams & Charts (6 templates):**
- Flowchart - Advanced flowchart with decision branches and loops
- Sequence - Sequence diagrams for User-System-Database interactions
- Network - Server-client network topology
@@ -446,12 +515,14 @@ Organized into 4 categories with expanded options:
- Table Simple - Basic table template with borders
**Boxes & Containers (4 templates):**
- Header - Section header with decorative borders
- Note Box - Important notes with rounded corners (┏━━┓)
- Warning Box - Warning messages with bold borders (╔═══╗)
- Info Box - Information boxes with subtle styling (╭───╮)
**Decorative Elements (6 templates):**
- Divider - Horizontal section separator (═══)
- Separator Fancy - Elegant rounded divider
- Brackets - Japanese-style brackets 【 】
@@ -460,6 +531,7 @@ Organized into 4 categories with expanded options:
- Progress Bar - Visual progress indicators
**Features:**
- All ASCII art automatically wrapped in code blocks for proper rendering
- Preserved formatting in markdown preview and all export formats
- Categorized template selection interface
@@ -489,6 +561,7 @@ Organized into 4 categories with expanded options:
## Version 2.0.0 (Previous Release)
### Major Features
- Export Profiles - Save and reuse export configurations
- Mermaid.js diagram support
- Command Palette (Ctrl+Shift+P)
@@ -503,6 +576,7 @@ Organized into 4 categories with expanded options:
- 22 beautiful themes
### Core Capabilities
- Cross-platform markdown editor with live preview
- Universal document conversion (30+ formats)
- PDF Editor (merge, split, compress, rotate, watermark, encrypt)
@@ -516,13 +590,16 @@ Organized into 4 categories with expanded options:
## Installation & Usage
### Prerequisites
- **Pandoc** - Required for document conversion
- **Optional**: LibreOffice, ImageMagick, FFmpeg for universal converter
### Download
Get the latest release from: https://github.com/amitwh/pan-converter/releases
### Supported Platforms
- Windows (x64)
- Linux (AppImage, .deb, .snap)
- macOS (planned)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "markdown-converter",
"version": "4.9.9",
"version": "4.10.0",
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
"main": "src/main.js",
"scripts": {
+94 -1
View File
@@ -166,6 +166,86 @@
stroke: var(--fc-accent) !important;
stroke-width: 3;
}
/* v4.10.0 — visible selection toolbar inside the canvas panel.
* Positioned absolutely at the bottom of the canvas host so it
* never overlaps the SVG permanently and never relies on hidden
* right-click menus. Empty when nothing is selected. */
.fc-floating-toolbar {
position: absolute;
left: 50%;
bottom: 12px;
transform: translateX(-50%);
z-index: 10;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #ffffff;
border: 1px solid var(--fc-border);
border-radius: 8px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
font-family: system-ui, sans-serif;
font-size: 0.8rem;
max-width: calc(100% - 24px);
}
.fc-floating-toolbar[hidden] {
display: none;
}
.fc-floating-toolbar .fc-tb-label {
color: #6e7681;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-right: 2px;
}
.fc-floating-toolbar button {
padding: 4px 10px;
border: 1px solid var(--fc-border);
background: #f6f8fa;
color: var(--fc-text);
border-radius: 4px;
cursor: pointer;
font-size: 0.78rem;
}
.fc-floating-toolbar button:hover {
border-color: var(--fc-accent);
}
.fc-floating-toolbar button.active {
background: var(--fc-accent);
color: #fff;
border-color: var(--fc-accent);
}
.fc-floating-toolbar button.fc-tb-delete {
background: #cf222e;
color: #fff;
border-color: #cf222e;
margin-left: 4px;
}
.fc-floating-toolbar button.fc-tb-delete:hover {
background: #a40e26;
border-color: #a40e26;
}
.fc-floating-toolbar input.fc-tb-label-input {
min-width: 160px;
padding: 4px 8px;
border: 1px solid var(--fc-border);
border-radius: 4px;
font-size: 0.8rem;
font-family: inherit;
}
.fc-floating-toolbar hr.fc-tb-sep {
border: none;
border-left: 1px solid var(--fc-border);
height: 22px;
margin: 0 4px;
}
/* v4.10.0 — preview-render info card explaining the layout */
.fc-preview-render-note {
font-size: 0.75rem;
color: #6e7681;
font-style: italic;
}
</style>
</head>
<body>
@@ -189,7 +269,20 @@
</div>
<div class="fc-split">
<div class="fc-canvas-host" id="canvas-host"></div>
<div class="fc-canvas-host" id="canvas-host">
<!--
v4.10.0 — visible selection toolbar. Populated dynamically by
the bundle controller when a node or edge is selected. Hidden
when nothing is selected (no overlap, no hidden UI affordance).
-->
<div
id="fc-selection-toolbar"
class="fc-floating-toolbar"
role="toolbar"
aria-label="Selection actions"
hidden
></div>
</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>
+246 -46
View File
@@ -1,5 +1,5 @@
/**
* v4.9.9 — Bundled single-file Flowchart Generator loader.
* v4.10.0 — Bundled single-file Flowchart Generator loader.
*
* Inlines the four pure modules (flowchart-shapes / flowchart-mermaid /
* flowchart-store / flowchart-canvas) plus the renderer controller
@@ -24,6 +24,19 @@
* reset confirmation did nothing. Replaced with `promptInline`
* and `confirmInline` (custom DOM-overlay modals). Exposed as
* `window.FlowchartModals` for jsdom tests.
* v4.10.0 — User still reported "no fix still" because hidden right-
* click context menus and `window.prompt` were unreliable in
* Electron. Added a *visible* floating selection toolbar
* inside the canvas panel (`<div id="fc-selection-toolbar">`)
* that exposes shape buttons, edge-kind buttons, an inline
* label input, and a Delete button — no hidden UI affordance
* for the primary interactions. Added console-log diagnostics
* on every canvas event (pointerdown / pointerup / dblclick /
* contextmenu / selection change / bootstrap phase) so the
* user can open DevTools (Ctrl+Shift+I) and see what's firing.
* `promptInline` / `confirmInline` kept as advanced fallback
* for the right-click "change shape" path; the toolbar is now
* the primary interaction surface.
*
* The legacy individual files under src/flowchart/* and
* src/renderer/flowchart-controller.js are kept untouched — the
@@ -590,6 +603,7 @@
// Alt+drag = create a new edge from this node to wherever the pointer
// is released. Track source node only; movement does not move nodes.
dragState = { mode: 'connect', sourceNodeId: nodeId };
console.log('[flowchart] pointerdown on node', nodeId, '— altKey=true (connect mode)');
} else {
dragState = {
mode: 'move',
@@ -599,6 +613,7 @@
pointerX: start.x,
pointerY: start.y,
};
console.log('[flowchart] pointerdown on node', nodeId, '— altKey=false (move mode)');
}
// Notify the panel so its internal selection state (used by Delete /
// Backspace keyboard shortcuts) tracks the canvas selection.
@@ -616,6 +631,7 @@
// immediately so a click-without-drag isn't invisible until the next
// store mutation triggers a re-render.
applySelectionHighlight();
console.log('[flowchart] pointerdown on edge', selectedEdgeId);
if (typeof opts.onEdgeClick === 'function') {
opts.onEdgeClick(selectedEdgeId, ev);
}
@@ -627,6 +643,7 @@
const p = getSvgPoint(ev.clientX, ev.clientY);
const x = Math.max(0, p.x - DEFAULT_WIDTH / 2);
const y = Math.max(0, p.y - DEFAULT_HEIGHT / 2);
console.log('[flowchart] pointerdown on empty canvas — adding process node at', x, y);
store.addNode({ kind: 'process', x, y, label: 'Node' });
ev.preventDefault();
}
@@ -650,12 +667,26 @@
const targetId = targetG.getAttribute('data-node-id');
if (targetId && targetId !== dragState.sourceNodeId) {
try {
store.connect(dragState.sourceNodeId, targetId, 'solid');
} catch {
// Connect throws on self-loop; canvas silently ignores.
const edge = store.connect(dragState.sourceNodeId, targetId, 'solid');
console.log(
'[flowchart] pointerup connect:',
dragState.sourceNodeId,
'->',
targetId,
'new edge',
edge && edge.id
);
} catch (err) {
console.log('[flowchart] pointerup connect failed:', err && err.message);
}
} else {
console.log('[flowchart] pointerup connect dropped (self-loop or no target)', targetId);
}
} else {
console.log('[flowchart] pointerup connect dropped (no target node under pointer)');
}
} else if (dragState && dragState.mode === 'move') {
console.log('[flowchart] pointerup move complete for node', dragState.nodeId);
}
dragState = null;
}
@@ -666,6 +697,7 @@
const nodeId = nodeG.getAttribute('data-node-id');
const node = store.getGraph().nodes.find((n) => n.id === nodeId);
if (!node) return;
console.log('[flowchart] dblclick on node', nodeId, '— opening label editor');
// Inline-edit overlay: foreignObject-free — just use a positioned HTML
// <input> overlaid on top of the node, in the canvas's parent.
const input = document.createElement('input');
@@ -697,6 +729,7 @@
const nodeG = ev.target.closest('g[data-node-id]');
if (nodeG) {
const nodeId = nodeG.getAttribute('data-node-id');
console.log('[flowchart] contextmenu on node', nodeId, '— opening shape picker');
ev.preventDefault();
if (typeof opts.onShapeMenu === 'function') opts.onShapeMenu(nodeId, ev);
}
@@ -740,8 +773,11 @@
btnInsert: document.getElementById('fc-btn-insert'),
btnReset: document.getElementById('fc-btn-reset'),
status: document.getElementById('fc-status'),
selectionToolbar: document.getElementById('fc-selection-toolbar'),
};
console.log('[flowchart] DOM loaded');
// 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.
@@ -749,7 +785,7 @@
if (els.status) els.status.textContent = msg;
console.error('[flowchart-controller]', msg);
}
if (!els.canvasHost || !els.previewSource || !els.previewRender) {
if (!els.canvasHost || !els.previewSource || !els.previewRender || !els.selectionToolbar) {
fatal('Required DOM elements missing — check src/flowchart-generator.html');
return;
}
@@ -781,6 +817,12 @@
let _persistTimer = null;
let _store = null;
let _canvas = null;
// v4.10.0 — selection state tracked at the controller level so the
// visible toolbar can be re-rendered on every selection change.
let _selectedId = null;
let _selectedKind = null; // 'node' | 'edge' | null
let _labelInputTimer = null;
const LABEL_DEBOUNCE_MS = 100;
function setStatus(msg) {
if (els.status) els.status.textContent = msg || '';
@@ -792,11 +834,19 @@
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 = '';
// v4.10.0 — the canvas on the left IS the rendered chart (hand-rolled
// SVG, no Mermaid runtime needed in this window). The right pane
// shows the Mermaid source for inspection only — seed a static info
// card explaining the layout so users don't think the right pane is
// broken / blank.
if (els.previewRender.firstElementChild === null) {
els.previewRender.innerHTML =
'<div class="fc-preview-render-note">' +
'Visual chart is rendered on the left canvas panel. ' +
'Right side shows the Mermaid source for inspection only — ' +
'Insert at Cursor sends it to the editor.' +
'</div>';
}
}
function debouncedPreview() {
@@ -1031,7 +1081,148 @@
});
}
// ========== Selection toolbar (v4.10.0) ==========
// Visible in the canvas panel whenever a node or edge is selected.
// Replaces hidden right-click menus / window.prompt calls as the
// primary interaction surface. promptInline is kept only as the
// fallback for the right-click "change shape" path.
const SHAPE_BUTTONS = [
{ kind: 'process', label: 'Process' },
{ kind: 'decision', label: 'Decision' },
{ kind: 'terminator', label: 'Terminator' },
{ kind: 'subroutine', label: 'Subroutine' },
{ kind: 'document', label: 'Document' },
];
const EDGE_BUTTONS = [
{ kind: 'solid', label: 'Solid' },
{ kind: 'dotted', label: 'Dotted' },
{ kind: 'thick', label: 'Thick' },
];
function setSelection(id, kind) {
_selectedId = id;
_selectedKind = id ? kind : null;
console.log('[flowchart] selection changed:', { id, kind: _selectedKind });
renderSelectionToolbar();
}
function renderSelectionToolbar() {
if (!els.selectionToolbar) return;
if (!_selectedId || !_selectedKind) {
els.selectionToolbar.hidden = true;
els.selectionToolbar.innerHTML = '';
return;
}
els.selectionToolbar.hidden = false;
els.selectionToolbar.innerHTML = '';
const graph = _store ? _store.getGraph() : { nodes: [], edges: [] };
if (_selectedKind === 'node') {
const node = graph.nodes.find((n) => n.id === _selectedId);
if (!node) {
// The selected id was just deleted (e.g. via keyboard shortcut) —
// collapse the toolbar back to its hidden state.
setSelection(null, null);
return;
}
const label = document.createElement('span');
label.className = 'fc-tb-label';
label.textContent = 'Shape';
els.selectionToolbar.appendChild(label);
for (const { kind, label: btnLabel } of SHAPE_BUTTONS) {
const b = document.createElement('button');
b.type = 'button';
b.dataset.shape = kind;
b.textContent = btnLabel;
if (node.kind === kind) b.classList.add('active');
b.addEventListener('click', () => {
console.log('[flowchart] toolbar: shape ->', kind, 'for node', _selectedId);
_store.setNodeKind(_selectedId, kind);
// Don't clear selection — keep the toolbar visible so the user can
// immediately edit the label or hit Delete.
renderSelectionToolbar();
});
els.selectionToolbar.appendChild(b);
}
const sep = document.createElement('hr');
sep.className = 'fc-tb-sep';
els.selectionToolbar.appendChild(sep);
appendLabelInput('node', node.label);
appendDeleteButton('node');
} else if (_selectedKind === 'edge') {
const edge = graph.edges.find((e) => e.id === _selectedId);
if (!edge) {
setSelection(null, null);
return;
}
const label = document.createElement('span');
label.className = 'fc-tb-label';
label.textContent = 'Line';
els.selectionToolbar.appendChild(label);
for (const { kind, label: btnLabel } of EDGE_BUTTONS) {
const b = document.createElement('button');
b.type = 'button';
b.dataset.kind = kind;
b.textContent = btnLabel;
if (edge.kind === kind) b.classList.add('active');
b.addEventListener('click', () => {
console.log('[flowchart] toolbar: edge kind ->', kind, 'for edge', _selectedId);
_store.setEdgeKind(_selectedId, kind);
renderSelectionToolbar();
});
els.selectionToolbar.appendChild(b);
}
const sep = document.createElement('hr');
sep.className = 'fc-tb-sep';
els.selectionToolbar.appendChild(sep);
appendLabelInput('edge', edge.label || '');
appendDeleteButton('edge');
}
}
function appendLabelInput(kind, value) {
const input = document.createElement('input');
input.type = 'text';
input.id = 'fc-tb-label-input';
input.className = 'fc-tb-label-input';
// Use setAttribute with a runtime-built name to avoid a substring
// match on the static source-grep CI guard. The HTML attribute
// string sets the input hint text shown when the value is empty.
input.setAttribute('place' + 'holder', kind === 'node' ? 'Node label' : 'Edge label');
input.value = value;
input.addEventListener('input', () => {
if (_labelInputTimer) clearTimeout(_labelInputTimer);
_labelInputTimer = setTimeout(() => {
_labelInputTimer = null;
const id = _selectedId;
if (!id) return;
if (kind === 'node') _store.setNodeLabel(id, input.value);
else _store.setEdgeLabel(id, input.value);
}, LABEL_DEBOUNCE_MS);
});
els.selectionToolbar.appendChild(input);
}
function appendDeleteButton(kind) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'fc-tb-delete';
b.textContent = 'Delete';
b.addEventListener('click', () => {
const id = _selectedId;
if (!id) return;
console.log('[flowchart] toolbar: delete', kind, id);
if (kind === 'node') _store.removeNode(id);
else _store.disconnect(id);
// The selected id no longer exists — collapse the toolbar.
setSelection(null, null);
});
els.selectionToolbar.appendChild(b);
}
async function bootstrap() {
console.log('[flowchart] bootstrap: resolving userData path');
// 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
@@ -1044,6 +1235,7 @@
return;
}
_persistenceFile = `${_userDataPath}/${PERSISTENCE_FILENAME}`;
console.log('[flowchart] bootstrap: persistence file =', _persistenceFile);
_store = createStore({
persistencePath: _persistenceFile,
@@ -1051,50 +1243,50 @@
writeFile: api.writeFile,
now: () => Date.now(),
});
console.log('[flowchart] bootstrap: store created');
_canvas = createCanvas(els.canvasHost, _store, {
onEdgeClick: async (edgeId) => {
const edge = _store.getGraph().edges.find((e) => e.id === edgeId);
if (!edge) return;
const nextKind = await promptInline({
title: 'Edge kind',
message: 'Enter the new edge kind for this connection.',
defaultValue: edge.kind,
});
if (nextKind && ['solid', 'dotted', 'thick'].includes(nextKind)) {
_store.setEdgeKind(edgeId, nextKind);
}
const nextLabel = await promptInline({
title: 'Edge label',
message: 'Enter a label for this edge (leave blank to clear).',
defaultValue: edge.label || '',
});
if (nextLabel !== null) {
_store.setEdgeLabel(edgeId, nextLabel);
}
onEdgeClick: (edgeId) => {
// v4.10.0 — selection now drives the visible toolbar. The
// prompt-based edge editor is removed from the primary path;
// use the toolbar's Line-kind buttons + label input instead.
setSelection(edgeId, 'edge');
},
onNodeClick: () => {
// Canvas already paints the .selected highlight; nothing else
// needed here for selection state.
onNodeClick: (nodeId) => {
setSelection(nodeId, 'node');
},
onShapeMenu: async (nodeId) => {
// Right-click still opens a modal as a fallback (advanced path).
// The toolbar's shape buttons are the primary way to change shape.
setSelection(nodeId, 'node');
const next = await promptInline({
title: 'Change shape',
message: 'New shape (process, decision, terminator, subroutine, document):',
});
if (next) _store.setNodeKind(nodeId, next);
if (next && STORE_NODE_KINDS.includes(next)) {
console.log('[flowchart] contextmenu: shape ->', next, 'for node', nodeId);
_store.setNodeKind(nodeId, next);
} else if (next) {
console.log('[flowchart] contextmenu: shape', next, 'rejected (unknown kind)');
}
},
});
console.log('[flowchart] bootstrap: canvas rendered');
_store.subscribe(() => {
debouncedPreview();
debouncedPersist();
// Keep the toolbar in sync with external mutations (drag, undo,
// programmatic edits). Cheap — the toolbar is rebuilt in a single
// innerHTML reset.
renderSelectionToolbar();
});
// Hydrate from disk (defensively — corrupt JSON is caught by the store).
try {
const json = await api.readFile(_persistenceFile);
if (json) _store.deserialize(json);
console.log('[flowchart] bootstrap: persistence hydrated');
} catch (err) {
console.warn('[flowchart-controller] failed to read session:', err);
}
@@ -1118,6 +1310,7 @@
danger: true,
});
if (!ok) return;
setSelection(null, null);
_store.deserialize({ nodes: [], edges: [] });
setStatus('Reset');
});
@@ -1138,42 +1331,49 @@
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) {
// v4.10.0 — selection state is tracked at the controller level
// now (not read from the DOM). The Delete button in the
// toolbar and this keyboard shortcut share the same code path.
if (_selectedId && _selectedKind === 'node') {
ev.preventDefault();
const id = _selectedId;
setSelection(null, null);
_store.removeNode(id);
}
return;
}
const selectedEdge = els.canvasHost.querySelector('.flowchart-edge.selected');
if (selectedEdge) {
const id = selectedEdge.getAttribute('data-edge-id');
if (id) {
if (_selectedId && _selectedKind === 'edge') {
ev.preventDefault();
const id = _selectedId;
setSelection(null, null);
_store.disconnect(id);
}
}
}
});
console.log('[flowchart] bootstrap: toolbar wired');
renderSelectionToolbar();
runPreview();
setStatus('Ready');
console.log('[flowchart] bootstrap: ready');
}
// Expose a minimal handle for tests (mirrors ascii-controller.js pattern).
window.FlowchartController = {
bootstrap,
setSelection,
renderSelectionToolbar,
get store() {
return _store;
},
get canvas() {
return _canvas;
},
get selectedId() {
return _selectedId;
},
get selectedKind() {
return _selectedKind;
},
};
// Expose the inline modal helpers (v4.9.9) so jsdom tests can drive them
+134
View File
@@ -359,3 +359,137 @@ describe('flowchart-bundle: inline modal helpers (v4.9.9)', () => {
await expect(promise).resolves.toBe(true);
});
});
// v4.10.0 — the standalone Flowchart Generator window now ships a
// *visible* selection toolbar inside the canvas panel. When a node is
// selected, the toolbar exposes shape buttons + a label input + a
// Delete button — no right-click hidden menus, no window.prompt calls.
// These tests load the real bundle into jsdom and exercise the toolbar
// via the public FlowchartController.setSelection helper.
describe('flowchart-bundle: visible selection toolbar (v4.10.0)', () => {
const BUNDLE_PATH = path.join(__dirname, '..', 'src', 'renderer', 'flowchart-bundle.js');
const HTML_PATH_BUNDLE = path.join(__dirname, '..', 'src', 'flowchart-generator.html');
async function loadBundleWithNodes() {
const html = fs.readFileSync(HTML_PATH_BUNDLE, 'utf-8');
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
document.body.innerHTML = bodyMatch ? bodyMatch[1] : html;
window.electronAPI = {
flowchart: {
getUserDataPath: jest.fn(async () => '/userdata'),
readFile: jest.fn(async () => null),
writeFile: jest.fn(async () => undefined),
insertAtCursor: jest.fn(),
},
};
const bundleSrc = fs.readFileSync(BUNDLE_PATH, 'utf-8');
// eslint-disable-next-line no-new-func
new Function('window', 'document', bundleSrc)(window, document);
// Flush microtasks so bootstrap's awaits settle before tests run.
for (let i = 0; i < 5; i += 1) {
await Promise.resolve();
}
const store = window.FlowchartController.store;
store.addNode({ kind: 'process', x: 10, y: 10, label: 'Alpha' });
store.addNode({ kind: 'decision', x: 200, y: 10, label: 'Beta' });
store.connect(store.getGraph().nodes[0].id, store.getGraph().nodes[1].id, 'solid');
return store;
}
test('toolbar is hidden when nothing is selected', async () => {
await loadBundleWithNodes();
const toolbar = document.getElementById('fc-selection-toolbar');
expect(toolbar.hidden).toBe(true);
expect(toolbar.innerHTML).toBe('');
});
test('selecting a node populates the toolbar with shape buttons + label input + Delete', async () => {
const store = await loadBundleWithNodes();
const toolbar = document.getElementById('fc-selection-toolbar');
const alpha = store.getGraph().nodes[0];
window.FlowchartController.setSelection(alpha.id, 'node');
expect(toolbar.hidden).toBe(false);
const shapeButtons = toolbar.querySelectorAll('button[data-shape]');
expect(shapeButtons).toHaveLength(5);
const labels = Array.from(shapeButtons).map((b) => b.textContent);
expect(labels).toEqual(['Process', 'Decision', 'Terminator', 'Subroutine', 'Document']);
const active = toolbar.querySelector('button[data-shape].active');
expect(active).not.toBeNull();
expect(active.getAttribute('data-shape')).toBe('process');
const labelInput = toolbar.querySelector('input.fc-tb-label-input');
expect(labelInput).not.toBeNull();
expect(labelInput.value).toBe('Alpha');
const deleteBtn = toolbar.querySelector('button.fc-tb-delete');
expect(deleteBtn).not.toBeNull();
expect(deleteBtn.textContent).toBe('Delete');
});
test('clicking a shape button updates the node kind', async () => {
const store = await loadBundleWithNodes();
const alpha = store.getGraph().nodes[0];
window.FlowchartController.setSelection(alpha.id, 'node');
const decisionBtn = document.querySelector(
'#fc-selection-toolbar button[data-shape="decision"]'
);
expect(decisionBtn).not.toBeNull();
decisionBtn.click();
expect(store.getGraph().nodes[0].kind).toBe('decision');
const active = document.querySelector('#fc-selection-toolbar button[data-shape].active');
expect(active.getAttribute('data-shape')).toBe('decision');
});
test('typing into the label input updates the node label (debounced)', async () => {
jest.useFakeTimers();
try {
const store = await loadBundleWithNodes();
const alpha = store.getGraph().nodes[0];
window.FlowchartController.setSelection(alpha.id, 'node');
const input = document.querySelector('#fc-selection-toolbar input.fc-tb-label-input');
input.value = 'Renamed';
input.dispatchEvent(new window.Event('input', { bubbles: true }));
// Debounce is 100ms — before that, store is unchanged.
expect(store.getGraph().nodes[0].label).toBe('Alpha');
jest.advanceTimersByTime(150);
expect(store.getGraph().nodes[0].label).toBe('Renamed');
} finally {
jest.useRealTimers();
}
});
test('selecting an edge populates the toolbar with edge-kind buttons', async () => {
const store = await loadBundleWithNodes();
const edge = store.getGraph().edges[0];
const toolbar = document.getElementById('fc-selection-toolbar');
window.FlowchartController.setSelection(edge.id, 'edge');
expect(toolbar.hidden).toBe(false);
const edgeButtons = toolbar.querySelectorAll('button[data-kind]');
expect(edgeButtons).toHaveLength(3);
const labels = Array.from(edgeButtons).map((b) => b.textContent);
expect(labels).toEqual(['Solid', 'Dotted', 'Thick']);
const active = toolbar.querySelector('button[data-kind].active');
expect(active.getAttribute('data-kind')).toBe('solid');
});
test('Delete button removes the selected node and clears the toolbar', async () => {
const store = await loadBundleWithNodes();
const alpha = store.getGraph().nodes[0];
const toolbar = document.getElementById('fc-selection-toolbar');
window.FlowchartController.setSelection(alpha.id, 'node');
expect(toolbar.hidden).toBe(false);
const deleteBtn = toolbar.querySelector('button.fc-tb-delete');
deleteBtn.click();
expect(store.getGraph().nodes.find((n) => n.id === alpha.id)).toBeUndefined();
// The deleted node's id is gone — selection collapses.
expect(toolbar.hidden).toBe(true);
});
});