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:
2026-09-14 19:01:09 +05:30
parent b1b72f9a60
commit 43c26c6521
3 changed files with 534 additions and 0 deletions
@@ -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.