diff --git a/docs/superpowers/specs/2026-07-22-monospace-font-embedding-and-naming-design.md b/docs/superpowers/specs/2026-07-22-monospace-font-embedding-and-naming-design.md new file mode 100644 index 0000000..cae5d4a --- /dev/null +++ b/docs/superpowers/specs/2026-07-22-monospace-font-embedding-and-naming-design.md @@ -0,0 +1,258 @@ +# Monospace Font Embedding + Naming Differentiation + +**Date:** 2026-07-22 +**Branch:** react-electron +**Goal:** Bring `react-electron` to feature parity with `master` by porting the v4.5.0 "embedded monospace font" feature, and differentiate the dev build identity from the installed `markdown-converter` deb so the two can coexist on the same machine. + +--- + +## Context + +The `react-electron` branch is the active development branch — a major rewrite of the renderer in React 19 + TypeScript + Tailwind/shadcn while keeping the main process in CommonJS. As of commit `bb4e874` it is **ahead** of `master` in nearly every dimension (auto-updater, crash writer, migration runner, command palette, first-run wizard, updates settings, crash report modal, 30+ React modals, large-file mode, scoped CSS). + +A repo-wide diff (`git diff --stat master..react-electron`) shows 301 files changed, +38,293/-26,786 lines. Master is **behind** react-electron. + +The **only** feature present in `master` and absent in `react-electron` is the monospace-font-embedding feature added in master v4.5.0 (commits `3f8679c` through `b218c76`, 19 commits). It bundles JetBrains Mono + Fira Code TTFs and embeds them into PDF/DOCX/EPUB/HTML exports so ASCII art and code blocks render with the exact font the user sees in the preview. + +A second concern: the user has `markdown-converter` installed as a `.deb`. Running `npm run dev` from this repo currently uses the same `appId` (`com.concreteinfo.markdownconverter`) and `productName` (`MarkdownConverter`), which triggers Electron's single-instance lock and confuses file-association routing. We rename this branch's identity to `Markdown Converter React` / `markdown-converter-react` to coexist. + +--- + +## Scope + +### In scope + +1. Port the monospace font embedding feature from `master` v4.5.0 into the react-electron branch: + - All five `src/main/*.js` modules (`MonospaceFontConfig`, `PdfFontHeader`, `DocxFontEmbedder`, `EpubFontEmbedder`, `ExportCss`) + - Settings schema module (`src/main/settings/monospaceSettings.js`) + - Two new IPC handlers (`get-monospace-settings`, `set-monospace-settings`) plus preload allowlist and TypeScript declaration + - Renderer body-class toggle and CSS tokens (`--font-mono-active`, `--font-mono-feature`) + - Bundled font assets: `JetBrainsMono-{Regular,Bold}.ttf`, `FiraCode-{Regular,Bold}.ttf` plus LICENSE files + - `asarUnpack` directive for `assets/fonts/**` + - `download-tools.js` extension to fetch Fira Code + - Wire `buildMonospaceExportCss`, `buildPdfFontHeader`, `DocxFontEmbedder.embed`, `EpubFontEmbedder.embed` into `src/main/index.js` export pipelines (PDF, DOCX, EPUB, HTML paths) + - Print-preview font integration (`src/renderer/components/preview/PrintPreviewFrame.tsx` or equivalent) + - Tests: one test file per new module mirroring master's coverage plus an E2E smoke test + +2. Rename branch identity: + - `package.json` `name` → `markdown-converter-react` + - `package.json` `build.productName` → `Markdown Converter React` + - `package.json` `build.appId` → `com.concreteinfo.markdownconverter.react` + - `package.json` `build.linux.executableName` → `markdown-converter-react` + - Window title prefix → `Markdown Converter — React Dev` in dev mode (use `process.env.VITE_DEV_SERVER_URL` as the gate) + - Settings file path remains `userData/settings.json` (no migration needed) — but add a new key `appVariant: "react"` so the renderer can show the correct title + +### Out of scope + +- React renderer rewrites (already ahead of master) +- Auto-updater, crash, migration (already ahead of master) +- Tauri / Web migration +- Pandoc version bump +- Documentation beyond this spec, the implementation plan, and the changelog entry +- Changes to v4-to-v5 settings migration (we leave the existing runner alone) + +--- + +## Design + +### Module layout (new files) + +``` +src/main/ + MonospaceFontConfig.js # path resolver — finds bundled TTF + PdfFontHeader.js # builds .tex fontspec header for xelatex/lualatex + DocxFontEmbedder.js # injects TTF into pandoc DOCX output (post-process) + EpubFontEmbedder.js # EPUB --epub-embed-font + manifest patch + ExportCss.js # builds CSS string with woff2 embedded as base64 + settings/ + monospaceSettings.js # schema, defaults, family map + ipc/ + monospace-handlers.js # IPC handler registration + utils/ + (existing) # no change + +src/renderer/ + hooks/ + use-monospace-classes.ts # toggles body.mono-{jetbrains,fira}{,-ligatures} classes + components/preview/ + PrintPreviewFrame.tsx # wire print preview to monospace settings via IPC + styles/globals.css # add --font-mono-active/--font-mono-feature declarations + types/electron.d.ts # extend ElectronAPI with monospace.{getSettings,saveSettings} + +src/preload.js # add monospace channels to ALLOWED_SEND_CHANNELS + expose on window.electronAPI.monospace + +assets/fonts/ + JetBrainsMono-Regular.ttf # bundled, ~270 KB + JetBrainsMono-Bold.ttf # bundled, ~278 KB + JetBrainsMono-LICENSE.txt + FiraCode-Regular.ttf # bundled, ~300 KB + FiraCode-Bold.ttf # bundled, ~300 KB + FiraCode-LICENSE.txt + +scripts/download-tools.js # extend to fetch Fira Code + +tests/ + unit/main/monospace/ + MonospaceFontConfig.test.js + monospaceSettings.test.js + PdfFontHeader.test.js + DocxFontEmbedder.test.js + EpubFontEmbedder.test.js + ExportCss.test.js + monospace-handlers.test.js + smoke-e2e-monospace.js # e2e: convert ASCII-art Markdown → PDF/DOCX/EPUB/HTML, assert TTF references appear in output +``` + +### Key interfaces (TypeScript-style for clarity) + +```ts +// src/main/settings/monospaceSettings.js +type MonoFont = 'jetbrains-mono' | 'fira-code'; +const FAMILY_BY_KEY: Record = { + 'jetbrains-mono': 'JetBrains Mono', + 'fira-code': 'Fira Code', +}; +function getActiveMonoFont(settings): string; +function isLigaturesEnabled(settings): boolean; +function safeMonospaceSettings(input): { monospaceFont: MonoFont; monospaceLigatures: boolean }; + +// src/main/MonospaceFontConfig.js +function getMonoFontTtfPath(familyKey, weight = 400): string | null; // null if asset missing +function ligaturesEnabled(settings): boolean; +function getActiveFamily(settings): string; + +// src/main/PdfFontHeader.js +function buildPdfFontHeader(settings, ttfPath, fontFamily): { headerPath: string; familyName: string }; + +// src/main/DocxFontEmbedder.js +async function embedDocxFont(inputDocxPath, outputDocxPath, ttfPath, fontFamily): Promise; +async function buildDocxWithEmbeddedFont(pandocArgs, settings): Promise<{args, postProcess}>; + +// src/main/EpubFontEmbedder.js +async function embedEpubFont(epubPath, ttfPath, fontFamily): Promise; +function withEpubEmbedFontArgs(pandocArgs, ttfPath, fontFamily): string[]; + +// src/main/ExportCss.js +function buildExportCss(settings, { woff2Base64, family, ligatures }): string; +function buildFontFaceBlock(familyKey, woff2Base64): string; + +// src/main/ipc/monospace-handlers.js +function register({ getMainWindow }): void; +// ipcMain.handle('get-monospace-settings', () => ({ monospaceFont, monospaceLigatures })) +// ipcMain.handle('set-monospace-settings', (_e, partial) => safeMonospaceSettings(partial) → persist + broadcast) +``` + +### Renderer integration + +- `use-monospace-classes.ts` hook runs once on mount, calls `electronAPI.monospace.getSettings()`, applies `document.body.classList` of: + - `mono-jetbrains-mono` | `mono-fira-code` + - `mono-ligatures-on` | `mono-ligatures-off` +- CSS in `globals.css`: + ```css + :root { + --font-mono-active: 'JetBrains Mono', 'SF Mono', Monaco, 'Courier New', monospace; + --font-mono-feature: 'liga' 0, 'calt' 0; + } + body.mono-fira-code { --font-mono-active: 'Fira Code', 'JetBrains Mono', 'SF Mono', monospace; } + body.mono-ligatures-on { --font-mono-feature: 'liga' 1, 'calt' 1; } + ``` +- All existing `font-family: monospace` and `font-family: var(--font-mono)` usages continue to work; the new tokens sit above them. + +### Wiring into export pipelines + +In `src/main/index.js` `exportWithPandoc` (PDF branch): +- Read `monospaceFont` + `monospaceLigatures` from settings +- Resolve TTF path via `MonospaceFontConfig.getMonoFontTtfPath` +- If path exists: build font header via `PdfFontHeader.buildPdfFontHeader`, pass `--include-in-header=…` +- If path missing: log warning, fall back to existing `-V monofont=Consolas` behavior + +In DOCX branch: invoke `DocxFontEmbedder.embedDocxFont` post-pandoc. +In EPUB branch: prepend `--epub-embed-font=…` to pandoc args; call `EpubFontEmbedder.embedEpubFont` to patch manifest. +In HTML branch: write CSS via `ExportCss.buildExportCss` (embed woff2 as base64) and reference in `