From 1ae9744b2b03ecae72abdbfcbedb301a1fe6188e Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 17 Aug 2026 11:15:23 -0500 Subject: [PATCH] Replace split textarea/preview editor with a single CodeMirror Markdown editor --- package-lock.json | 239 +++++++++++++++++++++++++++++++++++++ package.json | 7 ++ src/renderer/book.ts | 112 +++-------------- src/renderer/index.css | 132 ++++++-------------- src/renderer/index.html | 12 +- src/renderer/md-editor.ts | 130 ++++++++++++++++++++ tests/selftest-scenario.js | 27 +++++ 7 files changed, 455 insertions(+), 204 deletions(-) create mode 100644 src/renderer/md-editor.ts diff --git a/package-lock.json b/package-lock.json index 4a0b6bd..9967c03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,13 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@codemirror/commands": "^6.11.0", + "@codemirror/lang-markdown": "^6.5.2", + "@codemirror/language": "^6.12.4", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.9", + "@lezer/highlight": "^1.2.3", "archiver": "^8.0.0", "markdown-it": "^15.0.0" }, @@ -22,6 +29,147 @@ "wait-on": "^8.0.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.0.tgz", + "integrity": "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.12", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz", + "integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.2.tgz", + "integrity": "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.9", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.9.tgz", + "integrity": "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -523,6 +671,79 @@ "@hapi/hoek": "^11.0.2" } }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.6.tgz", + "integrity": "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz", + "integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1144,6 +1365,12 @@ "node": ">=18" } }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2502,6 +2729,12 @@ "node": ">=8" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -2635,6 +2868,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/wait-on": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", diff --git a/package.json b/package.json index 399aa32..43df31e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,13 @@ "wait-on": "^8.0.0" }, "dependencies": { + "@codemirror/commands": "^6.11.0", + "@codemirror/lang-markdown": "^6.5.2", + "@codemirror/language": "^6.12.4", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.9", + "@lezer/highlight": "^1.2.3", "archiver": "^8.0.0", "markdown-it": "^15.0.0" } diff --git a/src/renderer/book.ts b/src/renderer/book.ts index 713e175..23d05f6 100644 --- a/src/renderer/book.ts +++ b/src/renderer/book.ts @@ -1,6 +1,7 @@ import type { BookMeta } from "../main/project.js"; -import markdownIt from "markdown-it"; import { alertMessage, confirmMessage, promptInput } from "./dialog.js"; +import { createMdEditor, editorText, setEditorText } from "./md-editor.js"; +import type { EditorView } from "@codemirror/view"; export type FolioAPI = { newBook: () => Promise; @@ -63,30 +64,24 @@ export function initBook(api: FolioAPI) { const editorPlaceholder = $("editorPlaceholder"); const chapterView = $("chapterView"); const chapterTitleEl = $("chapterTitle"); - const sourceArea = $("sourceArea") as HTMLTextAreaElement; - const editorPreview = $("editorPreview"); + const editorHost = $("editorHost"); const wordCountEl = $("wordCount"); const saveStateEl = $("saveState"); - const findBtn = $("findBtn") as HTMLButtonElement; - const findBar = $("findBar"); - const findIn = $("findIn") as HTMLInputElement; - const findPrevBtn = $("findPrev") as HTMLButtonElement; - const findNextBtn = $("findNext") as HTMLButtonElement; - const findCloseBtn = $("findClose") as HTMLButtonElement; const exportBtn = $("exportBtn") as HTMLButtonElement; const exportZipBtn = $("exportZipBtn") as HTMLButtonElement; const addChapterBtn = $("addChapterBtn") as HTMLButtonElement; const projectBar = $("projectBar"); + const editorView: EditorView = createMdEditor(editorHost, { + onChange: (text) => handleEditorChange(text), + }); + function setChapterButtons(enabled: boolean) { addChapterBtn.disabled = !enabled; newChapterBtn.disabled = !enabled; } - - const md = markdownIt({ html: false, linkify: true, typographer: true }); let dirty = false; let loadedContent = ""; - let renderTimer: ReturnType | null = null; let saveTimer: ReturnType | null = null; const bookTitleEl = $("bookTitle"); const bookPathEl = $("bookPath"); @@ -192,8 +187,8 @@ export function initBook(api: FolioAPI) { async function selectChapter(id: string) { if (selectedId && dirty) { - const res = await api.saveChapter(selectedId, sourceArea.value); - if (!(res && "error" in res)) loadedContent = sourceArea.value; + const res = await api.saveChapter(selectedId, editorText(editorView)); + if (!(res && "error" in res)) loadedContent = editorText(editorView); dirty = false; saveStateEl.textContent = "Saved"; saveStateEl.classList.remove("unsaved"); @@ -206,15 +201,14 @@ export function initBook(api: FolioAPI) { const text = typeof content === "string" ? content : ""; const title = (meta && meta.chapters[id]?.title) || id; chapterTitleEl.textContent = title; - sourceArea.value = text; + setEditorText(editorView, text); loadedContent = text; dirty = false; saveStateEl.textContent = "Saved"; saveStateEl.classList.remove("unsaved"); wordCountEl.textContent = `words: ${countWords(text)}`; - renderPreview(text); - sourceArea.focus(); - sourceArea.setSelectionRange(text.length, text.length); + editorView.dispatch({ selection: { anchor: text.length } }); + editorView.focus(); clearTimers(); chapterView.classList.remove("hidden"); editorPlaceholder.classList.add("hidden"); @@ -224,27 +218,16 @@ export function initBook(api: FolioAPI) { } function clearTimers() { - if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; } if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } } - function renderPreview(text: string) { - editorPreview.innerHTML = md.render(text); - // smooth-scroll anchor links open in same preview pane - editorPreview.querySelectorAll("a").forEach((a) => - a.setAttribute("target", "_blank") - ); - } - - async function scheduleSave() { + function handleEditorChange(content: string) { if (!selectedId) return; - const content = sourceArea.value; dirty = content !== loadedContent; saveStateEl.textContent = dirty ? "Unsaved" : "Saved"; saveStateEl.classList.toggle("unsaved", dirty); wordCountEl.textContent = `words: ${countWords(content)}`; clearTimers(); - renderTimer = setTimeout(() => renderPreview(content), 150); if (dirty) { saveTimer = setTimeout(async () => { const res = await api.saveChapter(selectedId!, content); @@ -258,44 +241,6 @@ export function initBook(api: FolioAPI) { } } - // ---- find in editor ---- - function findNext(fromInput = false): boolean { - const needle = findIn.value; - if (!needle) return false; - const ta = sourceArea; - const cur = ta.selectionStart; - const idx = ta.value.toLowerCase().indexOf(needle.toLowerCase(), fromInput ? 0 : cur + 1); - if (idx === -1) return false; - ta.focus(); - ta.setSelectionRange(idx, idx + needle.length); - return true; - } - - function findPrev(): boolean { - const needle = findIn.value; - if (!needle) return false; - const ta = sourceArea; - const cur = ta.selectionStart; - const slice = ta.value.slice(0, cur).toLowerCase(); - const idx = slice.lastIndexOf(needle.toLowerCase()); - if (idx === -1) return false; - ta.focus(); - ta.setSelectionRange(idx, idx + needle.length); - return true; - } - - function openFindBar() { - findBar.classList.remove("hidden"); - findIn.value = ""; - findIn.focus(); - sourceArea.dispatchEvent(new Event("blur")); - } - - function closeFindBar() { - findBar.classList.add("hidden"); - sourceArea.focus(); - } - function status(text: string, ms = 3000) { saveStateEl.textContent = text; saveStateEl.classList.add("unsaved"); @@ -529,35 +474,6 @@ export function initBook(api: FolioAPI) { if (currentBook) void api.revealInFolder(currentBook.path); }); - // ---- editor wiring ---- - sourceArea.addEventListener("input", () => void scheduleSave()); - - findBtn.addEventListener("click", () => openFindBar()); - findNextBtn.addEventListener("click", () => { if (!findNext(false)) findIn.value = ""; }); - findPrevBtn.addEventListener("click", () => findPrev()); - findCloseBtn.addEventListener("click", () => closeFindBar()); - findIn.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - if (e.shiftKey) findPrev(); else findNext(false); - } else if (e.key === "Escape") { - closeFindBar(); - } - }); - - // Ctrl/Cmd+F to open find; Esc to close when open; keep editor focused otherwise - document.addEventListener("keydown", (e) => { - if (e.key === "f" && (e.ctrlKey || e.metaKey) && !findBar.classList.contains("hidden")) { - e.preventDefault(); - } - if (e.key === "F" && (e.ctrlKey || e.metaKey) && findBar.classList.contains("hidden") && selectedId) { - e.preventDefault(); - openFindBar(); - } else if (e.key === "Escape" && !findBar.classList.contains("hidden")) { - closeFindBar(); - } - }); - api.onBookOpened((data) => setCurrentBook(data)); api.onBookClosed(showWelcome); api.onChaptersChanged(() => void refreshMeta()); @@ -570,4 +486,6 @@ export function initBook(api: FolioAPI) { setChapterButtons(false); void renderRecents(); + + (window as any).__folioEditor = editorView; } diff --git a/src/renderer/index.css b/src/renderer/index.css index 0cb1b55..03007ae 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -150,10 +150,8 @@ html, body { .editor { flex: 1; display: flex; - align-items: center; - justify-content: center; background: var(--color-bg); - padding: var(--gap); + overflow: hidden; } .editor-placeholder { @@ -401,17 +399,17 @@ html, body { flex: 1; display: flex; flex-direction: column; - padding: var(--gap); - overflow: auto; + padding: 0; + overflow: hidden; } .chapter-header { display: flex; align-items: baseline; + justify-content: space-between; gap: 12px; - margin-bottom: var(--gap); + padding: 14px 20px; border-bottom: 1px solid var(--color-border); - padding-bottom: var(--gap); } .chapter-title { @@ -427,38 +425,6 @@ html, body { color: var(--color-muted); } -.chapter-content { - flex: 1; - font-family: var(--font-serif); - font-size: 1.05rem; - line-height: 1.7; - color: var(--color-text); - white-space: pre-wrap; - word-break: break-word; - margin: 0; - padding: 0; - outline: none; - max-width: 720px; -} - -/* ---- editor ---- */ -.chapter-view { - flex: 1; - display: flex; - flex-direction: column; - padding: 0; - overflow: hidden; -} - -.chapter-header { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 12px; - padding: 14px 20px; - border-bottom: 1px solid var(--color-border); -} - .editor-actions { display: flex; align-items: center; @@ -473,83 +439,56 @@ html, body { } .editor-panes { + flex: 1; display: flex; - flex: 1; + min-height: 0; overflow: hidden; - gap: 0; } -.source-area { +.cm-editor { flex: 1; min-width: 0; - border: 0; - resize: none; - outline: none; - padding: 24px 20px; - font-family: var(--font-serif); - font-size: 1.05rem; - line-height: 1.7; - color: var(--color-text); + height: 100%; +} + +.cm-editor .cm-scroller { + overflow: auto; +} + +.cm-editor .cm-gutters { background: var(--color-bg); - tab-size: 4; + color: var(--color-muted); + border-right: 1px solid var(--color-border); } -.source-area:focus { - background: var(--color-panel); -} +.cm-h1 { font-size: 1.7rem; font-weight: 700; } +.cm-h2 { font-size: 1.45rem; font-weight: 700; } +.cm-h3 { font-size: 1.25rem; font-weight: 600; } +.cm-h4 { font-size: 1.1rem; font-weight: 600; } +.cm-h5 { font-size: 1.05rem; font-weight: 600; } +.cm-h6 { font-size: 1rem; font-weight: 600; color: var(--color-muted); } -.editor-preview { - flex: 1; - min-width: 0; - overflow-y: auto; - padding: 24px 20px; - font-family: var(--font-serif); - font-size: 1.05rem; - line-height: 1.7; - color: var(--color-text); - background: var(--color-bg); - border-left: 1px solid var(--color-border); -} - -.editor-preview img { - max-width: 100%; - height: auto; -} - -.editor-preview pre { - background: var(--color-panel); - padding: 12px; - border-radius: var(--radius); - overflow-x: auto; -} - -.editor-preview code { +.cm-strong { font-weight: 700; } +.cm-em { font-style: italic; } +.cm-strike { text-decoration: line-through; } +.cm-link { color: var(--color-accent); } +.cm-url { color: var(--color-muted); text-decoration: underline; } +.cm-code { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background: var(--color-panel); padding: 2px 6px; border-radius: 4px; + font-size: 0.9em; } +.cm-quote { color: var(--color-muted); } +.cm-hr { color: var(--color-muted); text-decoration: line-through; } +.cm-comment { color: var(--color-muted); } -.find-bar { - display: flex; - align-items: center; - gap: 4px; - padding: 6px 10px; +.cm-panels { background: var(--color-panel); - border-bottom: 1px solid var(--color-border); -} - -.find-bar input { - flex: 1; - border: 1px solid var(--color-border); - border-radius: var(--radius); - padding: 4px 8px; - font-size: 0.85rem; - background: var(--color-bg); color: var(--color-text); } - /* ---- modal dialog ---- */ .modal-backdrop { position: fixed; @@ -624,3 +563,4 @@ html, body { opacity: 0.5; cursor: not-allowed; } + diff --git a/src/renderer/index.html b/src/renderer/index.html index 35cb8eb..1c44a73 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -46,19 +46,9 @@ words: 0 - -
- -
-
- +