Files
markdown-converter/docs/superpowers/specs/2026-09-14-theme-registry-design.md
T
amitwh 43c26c6521 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
2026-09-14 19:01:09 +05:30

159 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.