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

12 KiB
Raw Blame History

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.