mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
docs(specs): add theme-registry + ascii-art-upgrade + flowchart-editor designs
Three new specs for the v4.8.0+ feature wave: * Theme registry: replace hardcoded 25-theme menu in main.js with a pure ThemeRegistry module + per-theme CSS file convention. Add 12 new themes (Catppuccin x4, One Light, Tokyo Night Storm, Synthwave '84, Outrun, Winter is Coming Light+Dark, Solarized Dark HC, Spring Light). * ASCII art upgrade: consolidate dual implementations (standalone window vs dead in-app modal) into a single path; add 12 hand-coded fonts + figlet npm library for 400+ fonts; add copy/save/insert output destinations; comprehensive tests for the previously-zero-coverage textToASCII/createASCIIBox/getASCIITemplate machinery. * Flow chart editor: sidebar panel with hand-rolled SVG canvas, node-graph data model, drag/drop editing, live Mermaid source preview, undo/redo, session persistence. Emits Mermaid which the existing preview pane already renders natively. Amit Haridas
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# MarkdownConverter — ASCII Art Generator Upgrade Design
|
||||
|
||||
**Date:** 2026-09-14
|
||||
**Status:** Draft — pending review
|
||||
**Author:** Amit Haridas
|
||||
|
||||
## Overview
|
||||
|
||||
Promote the ASCII Art Generator from "works but untested, dual-implemented, 5 fonts" to a full-fledged feature with 17 hand-coded fonts + 400+ FIGlet fonts, single source of truth, comprehensive test coverage, and three output paths (insert at cursor, copy to clipboard, save to file). Consolidate the dead-code in-app modal in favor of the existing standalone `BrowserWindow`.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Single implementation path.** Delete the dead `#ascii-art-dialog` modal markup, its `ModalManager` instance, the `showASCIIGenerator*` dead channels in `preload.js`, and the corresponding renderer controller at `renderer.js:5942-6727`. The standalone `ascii-generator.html` window remains the only path.
|
||||
2. **Many more fonts.** Add 12 hand-coded fonts (Big, Small, Lean, Slant, Isometric1-4, 3-D, 3x5, ANSI Shadow, Calvin S) on top of the 5 existing (standard, banner, block, bubble, digital). Plus all `figlet` npm fonts (400+) accessible through a searchable picker.
|
||||
3. **Three output destinations.** Insert at cursor (existing — wraps result in a fenced code block), Copy to clipboard (new), Save to `.txt` file (new).
|
||||
4. **Comprehensive test coverage.** The current `textToASCII`, `createASCIIBox`, `getASCIITemplate`, and the new `figlet` adapter all have ZERO tests today. Add full coverage — snapshot tests for known outputs across all fonts, integration tests for copy/save/insert.
|
||||
5. **Pure-module architecture.** Move all algorithm code from `renderer.js` into `src/main/AsciiArt.js` (pure module, IPC-coupled via `main.js`, mirroring the `DocQA`/`DailyNotes` pattern).
|
||||
|
||||
## Non-Goals (v1)
|
||||
|
||||
- No animated ASCII / motion ASCII.
|
||||
- No color / ANSI escape codes (would break the markdown fenced code block contract).
|
||||
- No font upload — only shipped fonts (hand-coded + `figlet` standard library).
|
||||
- No image export (PNG/SVG of ASCII art) — text only.
|
||||
- No undo history inside the modal — single-shot generation.
|
||||
- No in-modal text editing of the generated ASCII (the user can paste into the editor instead).
|
||||
|
||||
## Decisions Locked
|
||||
|
||||
| Decision | Choice | Reason |
|
||||
|---|---|---|
|
||||
| Library | `figlet` npm package (latest, ~400 KB unpacked) | Industry-standard JS port of the original C library; 400+ fonts; small footprint |
|
||||
| Hand-coded fonts | Add 12 more, keep existing 5 as `legacy/` set | Hand-coded fonts have a distinct aesthetic (`figlet` doesn't 1:1 replicate all of them) and load with zero I/O |
|
||||
| Font loading | Hand-coded: synchronous. `figlet`: lazy-loaded on font-picker open (async) | Avoids 400 KB of synchronous font loading at app startup |
|
||||
| Module split | `src/main/AsciiArt.js` (pure), `src/renderer/ascii-controller.js` (UI), `src/ascii-generator.html` (already exists) | Matches established renderer-controller + main-module split |
|
||||
| Single source of truth | Standalone window only; in-app modal deleted | Dead code is technical debt; consolidation is required for testability |
|
||||
| Output destinations | Insert at cursor (existing) + Copy clipboard + Save to file | Covers all "I want this ASCII art in my markdown" use cases |
|
||||
| Insert wrapper | Result wrapped in `\n\`\`\`\n…\n\`\`\`\n` (existing behaviour) | Preserves current markdown-fence contract |
|
||||
| Persistence | None — the modal is stateless. Last-used font remembered via `electron-store` `ascii:lastFont` key (new). | Small UX win without adding persistence layer |
|
||||
| Tests | jsdom + Jest, snapshot tests for known font outputs | Same pattern as existing `DocQA.test.js` |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
Renderer (UI): │ src/renderer/ │
|
||||
- ascii-generator │ ascii-controller.js │
|
||||
.html │ - font-picker │
|
||||
- ascii- │ - input/options │
|
||||
controller.js │ - preview │
|
||||
│ - 3 action buttons │
|
||||
└────────┬────────────────┘
|
||||
│ IPC: ascii:generate, ascii:list-fonts,
|
||||
│ ascii:last-font, ascii:save
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
Main (pure): │ src/main/AsciiArt.js │
|
||||
│ generate(text, font, │
|
||||
│ options) │
|
||||
│ listFonts() │
|
||||
│ getFontMeta(id) │
|
||||
└────────┬────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Hand-coded │ │ figlet │ │ Templates │
|
||||
│ (sync) │ │ (lazy async) │ │ (19 existing)│
|
||||
│ 17 fonts │ │ 400+ fonts │ │ │
|
||||
└─────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
## New Modules
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/main/AsciiArt.js` | Pure module. `generate({ text, font, options })` returns the ASCII string. `listFonts()` returns `{ id, label, kind: 'hand-coded'\|'figlet'\|'template', sample }[]`. `getFontMeta(id)` returns `{ kind, height, supportedChars }`. Lazy-loads `figlet` on first `figlet:*` font request; caches font list. |
|
||||
| `src/main/AsciiArt.fonts.js` | Hand-coded font tables (5 existing + 12 new) extracted from `renderer.js:6005-6474`. Exports `HAND_CODED_FONTS` as `{ id → { height, chars: { 'A' → string[height], … } } }`. |
|
||||
| `src/renderer/ascii-controller.js` | Renderer-side controller. Wires input/options/preview/action-buttons in the standalone window. Calls `window.api.ascii.generate(...)` etc. (new preload bindings). |
|
||||
| `tests/ascii-art.test.js` | Unit tests for `AsciiArt.generate()` across hand-coded fonts (snapshot), `listFonts()` shape, `getFontMeta()`, figlet adapter (mocked), template adapter. |
|
||||
| `tests/ascii-art.fonts.test.js` | Per-hand-coded-font known-output snapshot tests for the word "HELLO" (and a few edge cases: empty string, single char, mixed case, digits). |
|
||||
| `tests/ascii-art.templates.test.js` | Per-template known-output snapshot for `getASCIITemplate('arrow-right', {})` etc. |
|
||||
| `tests/preload-ascii.test.js` | Asserts the new `ascii:*` IPC channels are declared in `preload.js` allow-list. |
|
||||
|
||||
## Modified Modules
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/ascii-generator.html` | Add: (a) searchable font picker dropdown (debounced 100 ms input filter over `listFonts()`), (b) two new buttons: "Copy to Clipboard" + "Save to File", (c) call `window.api.ascii.generate(...)` instead of running `textToASCII` inline. Layout otherwise unchanged. |
|
||||
| `src/main.js:5584-5611` (`openAsciiGenerator`) | No structural change. The IPC channel name stays `open-ascii-generator`. |
|
||||
| `src/main.js` (new IPC handlers, after line 5611) | Register: `ipcMain.handle('ascii:generate', …)`, `ipcMain.handle('ascii:list-fonts', …)`, `ipcMain.handle('ascii:get-font-meta', …)`, `ipcMain.handle('ascii:save', …)` (writes user-selected path), `ipcMain.handle('ascii:last-font', …)` (read + write via `electron-store`). |
|
||||
| `src/main.js` (require block) | Add `const AsciiArt = require('./main/AsciiArt')` near `main.js:4005-4010`. |
|
||||
| `src/preload.js:98-99,294-296` | **Delete** the dead `show-ascii-generator` and `show-ascii-generator-window` channels. Add new `ascii:generate`, `ascii:list-fonts`, `ascii:get-font-meta`, `ascii:save`, `ascii:last-font` to `validInvokeChannels`. Add the `generators.ascii` namespace helpers. |
|
||||
| `src/renderer.js:2188` | **Delete** the `new ModalManager('#ascii-art-dialog')` instantiation. |
|
||||
| `src/renderer.js:5942-6727` | **Delete** the entire in-app ASCII modal block: `showASCIIGenerator`, `hideASCIIGenerator`, `generateASCIIPreview`, `insertASCIIArt`, `createASCIIBox`, `getASCIITemplate`, `textToASCII`, all 5 font tables, the 19 template strings. (Total ~786 lines.) |
|
||||
| `src/index.html:814-…` (`#ascii-art-dialog` markup) | **Delete** the entire modal markup block. |
|
||||
| `src/styles.css` + other stylesheets | Remove any CSS tied only to the deleted modal. |
|
||||
| `package.json` dependencies | Add `"figlet": "^1.8.0"` (or current latest). |
|
||||
| `README.md:55-57` (ASCII Art Generator row) | Update to mention "17 hand-coded fonts + 400+ FIGlet fonts, copy/save/insert". |
|
||||
| `README.md:124` (shortcut) | No change — `Ctrl+Shift+A` keeps working (existing standalone window path). |
|
||||
| `electron-builder.config.js` | No change (pure-JS dep). |
|
||||
|
||||
## Hand-Coded Fonts (12 new)
|
||||
|
||||
Each is a `{ height, chars: { 'A': [...], 'B': [...], ... '0'..'9', ' ' } }` table. Heights vary 4–8 rows.
|
||||
|
||||
| id | label | height | source inspiration |
|
||||
|---|---|---|---|
|
||||
| `big` | Big | 8 | `figlet` "Big" |
|
||||
| `small` | Small | 5 | `figlet` "Small" |
|
||||
| `lean` | Lean | 6 | `figlet` "Lean" |
|
||||
| `slant` | Slant | 6 | `figlet` "Slant" (skewed) |
|
||||
| `isometric1` | Isometric 1 | 6 | `figlet` "Isometric1" |
|
||||
| `isometric2` | Isometric 2 | 6 | `figlet` "Isometric2" |
|
||||
| `isometric3` | Isometric 3 | 6 | `figlet` "Isometric3" |
|
||||
| `isometric4` | Isometric 4 | 6 | `figlet` "Isometric4" |
|
||||
| `three-d` | 3-D | 7 | `figlet` "3-D" |
|
||||
| `three-x-five` | 3x5 | 5 | `figlet` "3x5" |
|
||||
| `ansi-shadow` | ANSI Shadow | 8 | `figlet` "ANSI Shadow" |
|
||||
| `calvin-s` | Calvin S | 7 | `figlet` "Calvin S" |
|
||||
|
||||
Plus the 5 existing (standard, banner, block, bubble, digital). Total 17 hand-coded.
|
||||
|
||||
## New UI Features
|
||||
|
||||
1. **Searchable font picker.** Dropdown with text input. Lists all `listFonts()` results (17 hand-coded + 400+ figlet = ~417). Fuzzy-match on label, debounced 100 ms. Selecting a font renders a 5-char preview (`"HELLO"`) inline.
|
||||
2. **Copy to Clipboard button.** Copies the rendered ASCII art to the OS clipboard. Uses Electron's `clipboard.writeText()` (already available via `require('electron').clipboard` in main process — exposed as `window.api.ascii.copy(text)` via new preload helper).
|
||||
3. **Save to File button.** Opens a `dialog.showSaveDialog` with default `.txt` extension, writes the rendered ASCII art. Returns the saved path on success.
|
||||
4. **Last-used font memory.** New `electron-store` key `ascii:lastFont`. On window open, preselect the last-used font.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. User triggers `Ctrl+Shift+A` (existing) or Tools → ASCII Art Generator menu (existing).
|
||||
2. `main.js:5584` `openAsciiGenerator()` creates/ focuses the standalone `BrowserWindow` (existing behaviour).
|
||||
3. Renderer loads `src/ascii-generator.html`. The page calls `window.api.ascii.listFonts()` on mount, populates the font picker.
|
||||
4. User types text, selects font, chooses options (box style, padding) → the preview pane debounces (200 ms) and calls `window.api.ascii.generate(...)`.
|
||||
5. Click "Insert": renderer emits existing `insert-generated-content` IPC → main forwards to `mainWindow.webContents.send('insert-content', …)` → editor's `tabManager.insertAtCursor` runs, wrapped in fenced code block.
|
||||
6. Click "Copy": renderer calls `window.api.ascii.copy(text)` → main process writes to clipboard via `clipboard.writeText`.
|
||||
7. Click "Save": renderer calls `window.api.ascii.save(text)` → main shows save dialog → writes file → returns path. UI shows success toast.
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Handling |
|
||||
|---|---|
|
||||
| `figlet` fails to lazy-load (dep missing, native binding error) | `AsciiArt.listFonts()` returns hand-coded fonts only; logs a warning. Picker shows "FIGlet fonts unavailable" notice. |
|
||||
| `figlet` font file missing (corrupt install) | `AsciiArt.generate({ font: 'figlet:Big' })` throws a structured error → controller shows toast + falls back to last working font. |
|
||||
| User picks a font with unsupported characters (e.g. non-ASCII) | `generate()` substitutes `?` per character; controller shows a one-line warning above the preview. |
|
||||
| Clipboard write fails (rare, OS lock) | `clipboard.writeText` returns synchronously and almost never throws; if it does, the controller shows a toast and falls back to "Save to File". |
|
||||
| Save dialog cancelled | Returns `{ canceled: true }`; UI shows no error. |
|
||||
| IPC channel not allowed | `preload.js` is the gate; renderer can only call declared channels. Lint catches new channel declarations missing from allow-list. |
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Unit (`tests/ascii-art.test.js`)** — `generate({ text: 'HELLO', font: 'big', options: {} })` returns the expected ASCII string (snapshot); `listFonts()` returns `{ id, label, kind, sample }[]` with hand-coded first; `getFontMeta('big')` returns `{ kind: 'hand-coded', height: 8 }`.
|
||||
2. **Snapshot (`tests/ascii-art.fonts.test.js`)** — One snapshot per hand-coded font for `generate({ text: 'HELLO', font: '<id>' })`. Snapshots are checked into `tests/__snapshots__/ascii-art.fonts.test.js.snap`. Edge cases: empty string, single char, mixed case, digits.
|
||||
3. **Snapshot (`tests/ascii-art.templates.test.js`)** — One snapshot per existing template (`arrow-right`, `flowchart`, etc.).
|
||||
4. **Adapter (`tests/ascii-art.figlet-adapter.test.js`)** — `figlet.generate` is mocked; assert the adapter calls through with correct options, handles the promise, handles errors.
|
||||
5. **Integration (`tests/ascii-art.electron.test.js`)** — Spawn the actual `ascii-generator.html` window via `electron` binary in headless mode and assert the window loads without console errors. Skip if no display available.
|
||||
6. **Preload (`tests/preload-ascii.test.js`)** — Asserts all new `ascii:*` channels are in `validInvokeChannels` allow-list; old `show-ascii-generator*` channels are gone.
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| Hand-coded font tables are tedious to author and error-prone | Medium | Snapshot tests catch drift; visual review for first 4; the rest follow the pattern. |
|
||||
| `figlet` npm package has unexpected transitive deps or native bindings | Low | `figlet` is pure JS in v1.x; no native deps. Verify with `npm ls figlet` after install. |
|
||||
| Adding `figlet` increases bundle size (~400 KB unpacked, ~150 KB gzipped in asar) | Low | Acceptable: ASCII art is a featured function. Document in `THIRD-PARTY-NOTICES.md` (MIT license). |
|
||||
| Deleting 786 lines from `renderer.js` might break something unrelated | Low | The deleted block is self-contained. `git grep "showASCIIGenerator\|textToASCII\|createASCIIBox\|getASCIITemplate"` after deletion should return only the new module location. |
|
||||
| Snapshot tests for fonts become noisy (whitespace, line endings) | Medium | Normalize line endings to `\n`; trim trailing whitespace; commit a stable snapshot per font after manual review. |
|
||||
| Standalone window cannot share state with the editor (last-used font, theme sync) | Low | The standalone window is its own `BrowserWindow`; use `electron-store` (already used app-wide) for cross-window state. Theme sync is out of scope (standalone window has its own theme via `body.theme-<id>` from Spec 1). |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `src/main/AsciiArt.js` exists with `generate`, `listFonts`, `getFontMeta` API.
|
||||
- [ ] 17 hand-coded fonts (5 existing + 12 new) registered, each renders the test word "HELLO" correctly.
|
||||
- [ ] `figlet` library loads lazily on first `figlet:*` font request; lists ~400+ fonts via `listFonts()`.
|
||||
- [ ] In-app modal (`#ascii-art-dialog`, `showASCIIGenerator*`, `preload.js:294-296`) is fully deleted; `git grep` returns zero hits for these symbols outside the deletion commit.
|
||||
- [ ] Standalone window has Copy to Clipboard, Save to File, and Insert buttons. All three work end-to-end.
|
||||
- [ ] Last-used font is remembered across window opens via `electron-store` `ascii:lastFont`.
|
||||
- [ ] `npm test` passes with the new test files added.
|
||||
- [ ] `npm run lint` clean, `npm run format:check` clean.
|
||||
- [ ] `npm run build:linux` succeeds (no native-binding surprises from `figlet`).
|
||||
- [ ] README updated to mention the expanded font catalog and the new output destinations.
|
||||
@@ -0,0 +1,193 @@
|
||||
# MarkdownConverter — Flow Chart Editor Design
|
||||
|
||||
**Date:** 2026-09-14
|
||||
**Status:** Draft — pending review
|
||||
**Author:** Amit Haridas
|
||||
|
||||
## Overview
|
||||
|
||||
Add a sidebar-panel flow chart editor that lets users build flowcharts visually (drag nodes, connect edges, edit labels in-place) without writing Mermaid syntax by hand. The editor produces Mermaid `flowchart` source, which the existing preview pane already renders natively. Insert at cursor writes a ```` ```mermaid ```` fenced block. The feature is purely renderer-side — no main-process changes needed because Mermaid is already a bundled dependency.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Visual node editor.** Click to add nodes (5 shapes: process, decision, terminator, subroutine, document). Drag to move. Double-click to edit label inline. Right-click for shape submenu.
|
||||
2. **Edge editor.** Drag from node edge handle to another node to connect. Click edge to set type (solid arrow, dotted arrow, thick arrow) and add a label.
|
||||
3. **Live Mermaid preview.** Right pane of the sidebar panel shows the generated Mermaid source and re-renders the actual SVG on every change (debounced 250 ms). User can copy the source or insert it at the cursor.
|
||||
4. **Undo / redo.** `Ctrl+Z` / `Ctrl+Shift+Z` within the panel. Snapshot-based, bounded depth (50 steps).
|
||||
5. **Persistence.** The current flowchart is auto-saved to `<userData>/flowchart-session.json` so reopening the panel restores the user's work.
|
||||
6. **Comprehensive tests.** Pure-data module (`flowchart-store.js`) and the Mermaid translator (`flowchart-mermaid.js`) are fully unit-tested. The canvas (`flowchart-canvas.js`) is DOM-tested with jsdom pointer events.
|
||||
|
||||
## Non-Goals (v1)
|
||||
|
||||
- No swimlanes / subgraphs (Mermaid supports them; deferring for v2).
|
||||
- No collaboration / multi-user editing.
|
||||
- No export to PNG/SVG (the preview pane already renders; "Save as SVG" is a thin wrapper that can come later).
|
||||
- No import of existing Mermaid source (parse, validate, place into canvas) — defer to v2.
|
||||
- No theme sync (the SVG renders in the preview pane, which already respects the active theme).
|
||||
- No infinite canvas / zoom-pan — fixed viewport sized to the panel.
|
||||
- No copy/paste of nodes between editor instances.
|
||||
|
||||
## Decisions Locked
|
||||
|
||||
| Decision | Choice | Reason |
|
||||
|---|---|---|
|
||||
| Side location | New sidebar panel `flowchart` | Matches the established `src/sidebar/<name>-panel.js` pattern (`search-panel`, `daily-notes-panel`, `git-panel`) |
|
||||
| Canvas technology | SVG (no D3, no Konva, no library) | Hand-rolled SVG keeps the bundle small; the canvas is bounded (~100 nodes max); drag/drop is straightforward with pointer events |
|
||||
| Graph model | Pure data structure (nodes + edges) stored in a single `flowchart-store.js` module | Matches the codebase's "pure module + injectable IO" pattern; trivially testable |
|
||||
| Node shapes | 5 SVG shape templates per type, sized to text width | Mermaid supports many shapes; 5 covers 95% of real flowcharts |
|
||||
| Edge routing | Straight lines between node centers (no orthogonal/Manhattan routing) | Simpler; orthogonal routing can come in v2; the visual is still clear |
|
||||
| Undo/redo | Snapshot stack with bounded depth 50 | Predictable memory; matches editor undo conventions |
|
||||
| Persistence | Auto-save to `<userData>/flowchart-session.json` on every change (debounced 500 ms) | Survives app restart; user doesn't lose work |
|
||||
| Mermaid emission | Always `flowchart TD` (top-down) for v1 | Top-down is the most common flowchart direction; horizontal can be a per-node option in v2 |
|
||||
| Debouncing | Preview re-render 250 ms; persistence 500 ms | Responsive but not jittery |
|
||||
| Testing | Pure store/mermaid unit tests; canvas DOM tests with jsdom | No real-browser testing needed |
|
||||
| Accessibility | Keyboard shortcuts for add/delete/undo; visible focus rings on nodes; ARIA labels on the SVG | The panel is mouse-first but keyboard-accessible |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ src/sidebar/flowchart-panel.js │
|
||||
│ - mounts the panel │
|
||||
│ - splits left (canvas) | right (preview)│
|
||||
│ - wires pointer events to canvas │
|
||||
│ - subscribes to store changes │
|
||||
└──────────┬────────────────┬───────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌─────────────────────┐
|
||||
│ flowchart- │ │ flowchart-mermaid.js│
|
||||
│ canvas.js │ │ - toMermaid(graph) │
|
||||
│ - renders SVG │ │ - validate(shape) │
|
||||
│ - pointer events │ └─────────────────────┘
|
||||
│ - hit-testing │
|
||||
└────────┬─────────┘
|
||||
│ reads/writes
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ flowchart-store.js (pure module) │
|
||||
│ - graph { nodes, edges } │
|
||||
│ - addNode / moveNode / removeNode │
|
||||
│ - connect / disconnect / setEdgeType │
|
||||
│ - undo / redo (snapshot stack) │
|
||||
│ - subscribe(fn) → emits on change │
|
||||
│ - serialize() / deserialize(json) │
|
||||
│ - persistence IO injected │
|
||||
└──────────────────────────────────────────┘
|
||||
│
|
||||
│ debounced 500 ms write
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ <userData>/flowchart-session.json │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## New Modules
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/sidebar/flowchart-panel.js` | Renderer-only panel. Mounts the canvas + preview; registers keyboard shortcuts; wires the "Insert at Cursor" button. |
|
||||
| `src/flowchart/flowchart-store.js` | Pure data module. The graph is `{ nodes: Node[], edges: Edge[] }`. Node: `{ id, kind, x, y, label }`. Edge: `{ id, fromNodeId, toNodeId, kind: 'solid'\|'dotted'\|'thick', label? }`. Exports `create()`, `addNode`, `moveNode`, `setNodeLabel`, `setNodeKind`, `removeNode`, `connect`, `disconnect`, `setEdgeKind`, `setEdgeLabel`, `undo`, `redo`, `subscribe`, `serialize`, `deserialize`, `toJSON`. Constructor takes injected IO `{ persistencePath, readFile, writeFile, now }`. |
|
||||
| `src/flowchart/flowchart-canvas.js` | Renderer-only SVG canvas. Owns an `<svg>` element. Renders nodes as `<g>` containing shape `<path>`/`<rect>`/`<ellipse>` and a `<text>` label. Renders edges as `<line>` or `<polyline>`. Listens for pointer events: drag to move, double-click to edit label, right-click for shape menu. Hit-testing walks the DOM in reverse z-order. |
|
||||
| `src/flowchart/flowchart-mermaid.js` | Pure translator. `toMermaid(graph)` returns the Mermaid `flowchart TD` source string. Validates each shape against the 5 supported kinds; throws on unknown kind. Handles label escaping (`"`, newlines → `\n`). |
|
||||
| `src/flowchart/flowchart-shapes.js` | Pure module: `shapeSvg(kind, x, y, width, height) → string`. Defines the 5 SVG shape templates (process=rect, decision=diamond, terminator=stadium, subroutine=rect-with-double-border, document=parallelogram-approx). Pure functions; unit-tested. |
|
||||
| `tests/flowchart-store.test.js` | Pure store tests: add/move/connect/disconnect/delete; undo/redo round-trip; subscribe fires once per change; serialize/deserialize round-trip; throws on invalid input. |
|
||||
| `tests/flowchart-mermaid.test.js` | Translation tests: each node kind emits the correct Mermaid syntax (`[]`, `{}`, `(())`, `[[]]`, `[]/]`); edge kinds (`-->`, `-.->`, `==>`); label escaping. Snapshot tests for representative graphs. |
|
||||
| `tests/flowchart-shapes.test.js` | Each shape function returns SVG that matches the expected viewBox and contains the expected primitive. |
|
||||
| `tests/flowchart-canvas.test.js` | jsdom tests: mount canvas with a 3-node graph; assert SVG structure; simulate a `pointerdown` + `pointermove` + `pointerup`; assert store updated with new position. |
|
||||
| `tests/flowchart-panel.test.js` | jsdom test: mount the panel; assert canvas + preview panes exist; assert preview re-renders on store change. |
|
||||
| `tests/preload-flowchart.test.js` | (No new preload channels — feature is renderer-only.) |
|
||||
|
||||
## Modified Modules
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/renderer.js:2300` (sidebar registration) | Add `sidebarManager.registerPanel('flowchart', { title: 'Flow Chart', render: renderFlowChartPanel, icon: '<svg>…flowchart icon…</svg>' })`. |
|
||||
| `src/index.html` | Add a sidebar rail button with `data-panel="flowchart"` and the matching icon. |
|
||||
| `src/styles/sidebar.css` (or wherever sidebar styles live) | Add minimal layout for the panel's split: `display: flex; flex: 1;` with left pane ~70% (canvas) and right pane ~30% (preview). |
|
||||
| `README.md:54-70` (Advanced Features) | Add row: "**Visual flow chart editor** — Build Mermaid flowcharts visually; drag nodes, connect edges, live preview. Insert at cursor." |
|
||||
| `README.md:120-128` (Keyboard Shortcuts) | Add row: `\| Add Flow Chart Node \| Insert (when panel focused) \|`, `\| Undo \| Ctrl+Z \|`, `\| Redo \| Ctrl+Shift+Z \|` (panel-scoped). |
|
||||
|
||||
## Node Shapes
|
||||
|
||||
| kind | Mermaid syntax | SVG shape | Use case |
|
||||
|---|---|---|---|
|
||||
| `process` | `A[Label]` | `<rect>` | Generic step |
|
||||
| `decision` | `A{Label}` | `<polygon points="…">` (diamond) | Yes/no, branch |
|
||||
| `terminator` | `A([Label])` | `<rect rx=…>` (stadium) | Start/end |
|
||||
| `subroutine` | `A[[Label]]` | `<rect>` with double border | Named subroutine call |
|
||||
| `document` | `A[/Label/]` | `<polygon>` (parallelogram-approx) | Document/file reference |
|
||||
|
||||
Sizes auto-grow to fit the label text (measure with `getComputedTextLength()` on the `<text>` after first render).
|
||||
|
||||
## Edge Kinds
|
||||
|
||||
| kind | Mermaid syntax | SVG style |
|
||||
|---|---|---|
|
||||
| `solid` | `A --> B` | Solid `<line>` with marker arrow |
|
||||
| `dotted` | `A -.-> B` | `stroke-dasharray="4,4"` |
|
||||
| `thick` | `A ==> B` | `stroke-width="3"` |
|
||||
|
||||
Edge labels render as `<text>` at the midpoint of the edge with a small white background rect for legibility.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. User opens the panel via sidebar rail button (`data-panel="flowchart"`).
|
||||
2. `flowchart-panel.js` constructs a `flowchart-store.js` instance, calls `deserialize()` with whatever is in `<userData>/flowchart-session.json` (empty graph if absent).
|
||||
3. Panel mounts the canvas SVG and the preview pane side by side.
|
||||
4. Canvas subscribes to store changes; on every change, re-renders the SVG and notifies the panel.
|
||||
5. Panel debounces (250 ms) and calls `flowchart-mermaid.toMermaid(graph)`; updates the preview pane.
|
||||
6. Panel debounces (500 ms) and calls `store.serialize()` → writes to `<userData>/flowchart-session.json` via injected `writeFile`.
|
||||
7. User clicks "Insert at Cursor": panel emits `insert-content` IPC with the Mermaid source wrapped in a fenced code block (same pattern as ASCII art's insert).
|
||||
8. User triggers undo/redo: panel sends `Ctrl+Z` / `Ctrl+Shift+Z` to the canvas's keyboard handler when the panel is focused (the editor's existing undo is left alone — undo only fires when the panel has focus).
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Handling |
|
||||
|---|---|
|
||||
| Persistence file is corrupt JSON | `deserialize()` catches, logs warning, returns empty graph. User loses prior session but can start fresh. |
|
||||
| Persistence write fails (disk full, perms) | `writeFile` rejects; the panel logs the error but continues functioning. The next save attempt retries. |
|
||||
| `toMermaid()` encounters unknown node kind | Throws a typed error; the preview pane shows "Invalid graph: <message>"; the canvas still renders. User must fix the kind (e.g. via right-click menu). |
|
||||
| `toMermaid()` label contains newlines | Escapes `\n` to literal `\n` (Mermaid's escape). |
|
||||
| Canvas hit-test misidentifies a node | Defensive: the canvas uses `data-node-id` attribute on every `<g>`; lookup by id is O(1). |
|
||||
| User adds a node with empty label | Allowed; rendered as a single space. Mermaid will accept it. |
|
||||
| User tries to connect a node to itself | `connect()` throws `TypeError`; canvas shows toast. |
|
||||
| Undo stack overflow (>50 entries) | Oldest snapshot dropped; no memory growth. |
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Unit (`tests/flowchart-store.test.js`)** — Pure store operations. 30+ tests across add/move/connect/disconnect/delete/undo/redo/subscribe/serialize/deserialize. Includes invalid-input rejection.
|
||||
2. **Unit (`tests/flowchart-mermaid.test.js`)** — Translation for each shape kind × each edge kind × with-label × without-label. Snapshot tests for 5 representative graphs (linear chain, decision diamond, parallel branches, cycle, large 20-node graph).
|
||||
3. **Unit (`tests/flowchart-shapes.test.js`)** — `shapeSvg('process', 10, 10, 100, 50)` returns SVG containing a `<rect>` at the right coordinates.
|
||||
4. **DOM (`tests/flowchart-canvas.test.js`)** — Mount with 3-node graph; assert `<svg>` contains 3 `<g data-node-id="…">`. Simulate `pointerdown` on node 1's center, `pointermove` +100px, `pointerup`; assert store's `moveNode` was called with the new position.
|
||||
5. **DOM (`tests/flowchart-panel.test.js`)** — Mount panel; assert two panes exist; trigger a store change; assert preview re-renders within 300 ms (jest fake timers).
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| Drag/drop in jsdom is fragile (no real layout) | Medium | Use `getBoundingClientRect()` mocks; canvas tests focus on store wiring, not pixel-perfect rendering. |
|
||||
| Hand-rolled SVG shapes look amateurish next to Mermaid's rendered output | Medium | Use the same color tokens as the active theme via CSS variables; reuse `--accent`, `--text-primary` from the theme CSS. |
|
||||
| Undo/redo memory growth on large graphs | Low | Bounded depth (50 snapshots) + snapshot diff-based compression (only store changed nodes). |
|
||||
| Persistence path conflicts on multi-window future | Low | Use `<userData>/flowchart-session.json` — single user, single app instance. |
|
||||
| Mermaid preview re-render flickers on every keystroke | Medium | Debounce 250 ms; show a "Rendering…" status indicator during re-render. |
|
||||
| Edge routing looks ugly on dense graphs | Medium | Document as a known limitation in the panel tooltip; v2 orthogonal routing is on the roadmap. |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `src/sidebar/flowchart-panel.js` exists and is registered via `sidebarManager.registerPanel('flowchart', …)`.
|
||||
- [ ] Sidebar rail button with `data-panel="flowchart"` added to `src/index.html`; clicking it opens the panel.
|
||||
- [ ] User can add a node by clicking the canvas (default: process shape at click position).
|
||||
- [ ] User can drag a node to a new position; the store's `moveNode` is called with correct coordinates.
|
||||
- [ ] User can double-click a node to edit its label inline; on blur or Enter, the store is updated.
|
||||
- [ ] User can right-click a node to change its shape (5 options: process, decision, terminator, subroutine, document).
|
||||
- [ ] User can drag from a node's edge handle to another node to create an edge.
|
||||
- [ ] User can click an edge to change its type (3 options: solid, dotted, thick) and add an optional label.
|
||||
- [ ] `Ctrl+Z` / `Ctrl+Shift+Z` (when panel is focused) undoes / redoes the last change.
|
||||
- [ ] The right preview pane shows the current Mermaid source as text AND renders the SVG via the existing Mermaid render path.
|
||||
- [ ] "Insert at Cursor" wraps the Mermaid source in a ```` ```mermaid ```` fenced code block and inserts it at the editor cursor.
|
||||
- [ ] Closing and reopening the app restores the last graph from `<userData>/flowchart-session.json`.
|
||||
- [ ] `npm test` passes with the new test files added (target +30 new tests across 5 files).
|
||||
- [ ] `npm run lint` clean, `npm run format:check` clean.
|
||||
- [ ] `npm run build:linux` succeeds.
|
||||
- [ ] README updated to mention the visual flow chart editor.
|
||||
@@ -0,0 +1,158 @@
|
||||
# MarkdownConverter — Editor Theme Registry Design
|
||||
|
||||
**Date:** 2026-09-14
|
||||
**Status:** Draft — pending review
|
||||
**Author:** Amit Haridas
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the hardcoded editor-theme menu array in `main.js` (25 inline items at `src/main.js:1137-1245`) and the scattered CSS (`body.theme-<id>` selector blocks across `styles.css`, `styles-modern.css`, `styles-concreteinfo.css`) with a single `ThemeRegistry` module and a per-theme CSS file convention. Add 12 new themes spanning popular-requested, high-contrast, and seasonal categories. Future theme additions become a one-line `register()` call plus one CSS file.
|
||||
|
||||
## Goals
|
||||
|
||||
1. New themes can be added by changing one JS file and adding one CSS file — no menu edits.
|
||||
2. Add 12 new themes spanning popular-requested (Catppuccin palette), high-contrast (Solarized Dark HC), and seasonal categories.
|
||||
3. Theme application stays sub-100ms on theme switch (current behaviour).
|
||||
4. Existing 25 themes keep working — pure refactor for them, additive for the 12 new ones.
|
||||
5. Theme persistence model unchanged — `electron-store` `theme` key, default `atomonelight`.
|
||||
|
||||
## Non-Goals (v1)
|
||||
|
||||
- No user-defined custom themes (theme editor) — only shipped themes.
|
||||
- No per-syntax-token color customization beyond the per-theme palette.
|
||||
- No theme auto-switching based on system light/dark mode.
|
||||
- No export-theme changes — `ExportThemes.js` is a separate system with its own registry (already exists).
|
||||
|
||||
## Decisions Locked
|
||||
|
||||
| Decision | Choice | Reason |
|
||||
|---|---|---|
|
||||
| Module location | `src/main/ThemeRegistry.js` (pure, no Electron) | Matches the established `DocQA`/`DailyNotes`/`WorkspaceSearch` pattern |
|
||||
| Persistence | Existing `electron-store` `theme` key | Already wired; no migration needed |
|
||||
| CSS organisation | One file per theme at `src/styles/themes/<id>.css` | Convention beats scattered selector blocks |
|
||||
| CSS loading | Preload ALL theme `<link>` tags in `index.html`; toggle `disabled` on theme switch | Avoids link-swap network roundtrip; < 5 KB per theme × 37 = ~185 KB total CSS overhead |
|
||||
| Menu generation | `main.js` calls `ThemeRegistry.list()` + `categories()` to build menu items | Removes the 109-line hardcoded block |
|
||||
| Theme id format | kebab-case, lowercase, no spaces (`catppuccin-mocha`, `winter-is-coming-light`) | Stable contract for storage + CSS file naming |
|
||||
| New themes | 12 new: 4× Catppuccin, 1× One Light, 1× Tokyo Night Storm, 2× Synthwave/Outrun, 2× Winter is Coming, 1× Solarized Dark HC, 1× Solarized Light (seasonal Spring variant) | Highest demand per GitHub issues + aesthetic completeness |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
electron-store ───► │ src/main/ThemeRegistry │ ◄── register() at startup
|
||||
(theme = "x") │ .js (pure module) │
|
||||
│ list() │
|
||||
│ get(id) │
|
||||
│ categories() │
|
||||
└──────────┬──────────────┘
|
||||
│ list()
|
||||
▼
|
||||
main.js menu builder
|
||||
│
|
||||
┌──────────┴───────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
"Light Themes" "Dark Themes"
|
||||
"High-Contrast" "Seasonal"
|
||||
│ │
|
||||
└──────────┬───────────┘
|
||||
│ IPC: theme-changed
|
||||
▼
|
||||
renderer.js applyTheme(id)
|
||||
│
|
||||
▼
|
||||
index.html has 37 <link rel="stylesheet"
|
||||
href="styles/themes/<id>.css" disabled
|
||||
(all themes preloaded)
|
||||
│
|
||||
▼
|
||||
applyTheme() flips `disabled` on the
|
||||
matching <link>, removes from others
|
||||
```
|
||||
|
||||
## New Modules
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/main/ThemeRegistry.js` | Pure module: `register(theme)`, `unregister(id)`, `list()`, `get(id)`, `categories()`, `lightThemes()`, `darkThemes()`. A theme is `{ id, label, category: 'light'\|'dark'\|'high-contrast'\|'seasonal', isDark: boolean }`. No Electron imports. |
|
||||
| `src/main/ThemeRegistry.bootstrap.js` | Calls `register()` for all 37 themes (25 existing + 12 new). Imported by `main.js` at startup. |
|
||||
| `src/styles/themes/<id>.css` × 37 | Per-theme CSS rules previously scattered. Each file's body selector is `body.theme-<id>`. Naming convention: `<id>.css`. |
|
||||
| `tests/theme-registry.test.js` | Register / unregister / list / get / categories; lightThemes/darkThemes filtering; throws on duplicate id. |
|
||||
| `tests/theme-menu-builder.test.js` | Generates MenuItem[] from `list()` + `categories()`: each item has `{ id, label, type: 'radio', checked, click: () => setTheme(id) }`. |
|
||||
|
||||
## Modified Modules
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/main.js:1137-1245` | Delete the 109-line hardcoded theme menu array. Replace with: `const themeMenu = buildThemeMenu(store, mainWindow)` (new helper, ~25 lines, lives next to the menu builder). |
|
||||
| `src/main.js:3952-3955` | `setTheme(id)` already validates against a hardcoded set — replace hardcoded list with `ThemeRegistry.get(id) ? id : 'atomonelight'`. |
|
||||
| `src/main.js` startup | Add `require('./main/ThemeRegistry.bootstrap')` once. |
|
||||
| `src/index.html` | Replace the bare `<link rel="stylesheet" href="styles.css">` block with: preload ALL theme CSS as `<link id="theme-<id>" rel="stylesheet" href="styles/themes/<id>.css" disabled>` (one per theme). The base `styles.css` link stays as the structural (non-themed) layer. |
|
||||
| `src/renderer.js:2900` (`get-theme` IPC) | No change — handler already returns stored theme. |
|
||||
| `src/renderer.js:3005-3007` (apply theme) | Replace `document.body.className = 'theme-<id>'` with: find the matching `<link id="theme-<id>">`, set `disabled = false`; find the previously active `<link>`, set `disabled = true`. Keep the `theme-<id>` body className for any rule that depends on it (we keep the convention). |
|
||||
| `src/styles.css` + `styles-modern.css` + `styles-concreteinfo.css` | Remove the inlined `body.theme-<id> { … }` blocks (one per existing 25 themes). Move them to `src/styles/themes/<id>.css`. Pure code-motion — no rules change. |
|
||||
| `README.md:129-159` (Themes section) | Update list to 37 themes, mention new categories. |
|
||||
|
||||
## New Themes (12)
|
||||
|
||||
| id | label | category | notes |
|
||||
|---|---|---|---|
|
||||
| `catppuccin-latte` | Catppuccin Latte | light | Warm pastel |
|
||||
| `catppuccin-frappe` | Catppuccin Frappé | dark | Muted pastels |
|
||||
| `catppuccin-macchiato` | Catppuccin Macchiato | dark | Medium contrast |
|
||||
| `catppuccin-mocha` | Catppuccin Mocha | dark | High contrast pastels |
|
||||
| `one-light` | One Light | light | Atom One Light sibling |
|
||||
| `tokyo-night-storm` | Tokyo Night Storm | dark | Variant of existing Tokyo Night |
|
||||
| `synthwave-84` | Synthwave '84 | dark | Neon-on-dark |
|
||||
| `outrun` | Outrun | dark | Magenta/cyan |
|
||||
| `winter-is-coming-light` | Winter is Coming (Light) | light | Light variant of existing dark |
|
||||
| `winter-is-coming-dark` | Winter is Coming (Dark) | dark | New dark variant |
|
||||
| `solarized-dark-hc` | Solarized Dark (High Contrast) | high-contrast | Accessibility-focused |
|
||||
| `spring-light` | Spring Light | seasonal | First seasonal theme; same palette as Solarized Light but with green accents |
|
||||
|
||||
Categories are independent from `isDark`; e.g. `winter-is-coming-light` has `isDark: false` and `category: 'light'` but conceptually belongs to the "winter is coming" family. Future seasonal themes can share a `family: 'winter-is-coming'` field without changing the public API.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. App starts → `main.js` requires `ThemeRegistry.bootstrap.js` → calls `register()` for each of the 37 themes.
|
||||
2. `main.js` menu builder reads `ThemeRegistry.list()` + `categories()`, generates the View → Theme submenu programmatically.
|
||||
3. Renderer on init sends `get-theme` IPC → `main.js` replies with stored theme id.
|
||||
4. `renderer.js:3005-3007` `applyTheme(id)` toggles `disabled` on the matching `<link id="theme-<id>">`; sets `body.className = 'theme-<id>'` for legacy rule compatibility.
|
||||
5. User picks a new theme in menu → main process `setTheme(id)` → persists via `electron-store` → broadcasts `theme-changed` IPC → renderer `applyTheme(id)` re-toggles.
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Handling |
|
||||
|---|---|
|
||||
| Stored theme id no longer exists (e.g. after downgrade) | `setTheme()` falls back to `atomonelight`; logs a warning. Renderer also re-applies defensively. |
|
||||
| Missing theme CSS file (e.g. partial install) | `<link disabled>` simply doesn't activate; no JS error. App continues with previous active theme. |
|
||||
| `register()` called with duplicate id | Throws synchronously — startup fails loudly rather than silently shadowing an existing theme. |
|
||||
| Theme CSS file fails to parse (syntax error) | Browser logs error to console; the disabled `<link>` is never activated so no visual breakage. |
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Unit (`tests/theme-registry.test.js`)** — `register` adds to list, `unregister` removes, `get` returns the theme, `categories()` returns unique categories in registration order, `lightThemes()`/`darkThemes()` filter correctly. Throws on duplicate id. Throws on missing id in `get()`.
|
||||
2. **Unit (`tests/theme-menu-builder.test.js`)** — Given a list of 5 sample themes (2 light, 2 dark, 1 hc), `buildThemeMenu` returns a `MenuItem[]` with the correct structure: submenu labels, accelerators, `type: 'radio'`, `checked` matching current selection, `click` handlers wired.
|
||||
3. **Snapshot (`tests/theme-registry-bootstrap.test.js`)** — At startup, `ThemeRegistry.list()` returns exactly 37 themes with the expected ids in the expected order.
|
||||
4. **Existing tests** — `tests/project-meta.test.js` already checks `v${version}` in README (now 4.8.0); unchanged.
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| Per-theme CSS files duplicate common rules across 37 files | Medium | Factor the shared editor-surface rules (caret, selection, scrollbar) into a `styles/themes/_base.css`; per-theme files only carry palette tokens. |
|
||||
| Preloading 37 `<link>` tags delays first paint | Low | Total CSS ≈ 185 KB; gzip ≈ 30 KB; below the 100 ms first-paint budget on Electron's bundled Chromium. Measure with `webContents.getPrintersAsync`-style timing if needed. |
|
||||
| Body className + disabled-link double-state could drift | Low | Single function `applyTheme(id)` is the only mutation path; unit-tested with both states asserted. |
|
||||
| README Themes list grows unwieldy at 37 | Low | Group by category in the README; collapse to one-liners per theme with a table. |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `src/main/ThemeRegistry.js` exists with the public API documented.
|
||||
- [ ] 37 themes registered at startup; `list().length === 37`.
|
||||
- [ ] `main.js` menu builder produces the View → Theme submenu from the registry (no hardcoded theme names).
|
||||
- [ ] Renderer switches themes by toggling `<link disabled>`; no full page reload.
|
||||
- [ ] Theme persists across restarts via `electron-store`.
|
||||
- [ ] All existing 25 themes look identical to v4.8.0 (visual regression by inspection — themes are CSS, no logic change).
|
||||
- [ ] `npm test` passes with the new theme-registry + menu-builder test files added.
|
||||
- [ ] `npm run lint` clean, `npm run format:check` clean.
|
||||
- [ ] README updated to list all 37 themes by category.
|
||||
Reference in New Issue
Block a user