diff --git a/package.json b/package.json index 43df31e..5dcc329 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev": "concurrently -k \"npm run build:watch\" \"wait-on dist/main.js && npm run electron\"", "start": "npm run build && npm run electron", "lint": "tsc --noEmit", - "test": "node tests/run-project-test.mjs && node tests/run-chapters-test.mjs && node tests/run-wikilinks-test.mjs", + "test": "node tests/run-project-test.mjs && node tests/run-chapters-test.mjs && node tests/run-wikilinks-test.mjs && node tests/run-format-test.mjs", "package": "electron-builder", "test:e2e": "sh tests/run-e2e.sh" }, diff --git a/src/renderer/book.ts b/src/renderer/book.ts index 23d05f6..d66365e 100644 --- a/src/renderer/book.ts +++ b/src/renderer/book.ts @@ -2,6 +2,21 @@ import type { BookMeta } from "../main/project.js"; import { alertMessage, confirmMessage, promptInput } from "./dialog.js"; import { createMdEditor, editorText, setEditorText } from "./md-editor.js"; import type { EditorView } from "@codemirror/view"; +import { redo, undo } from "@codemirror/commands"; +import { + clearFormatting, + headingLevelAt, + insertHorizontalRule, + setHeading, + toggleBlockquote, + toggleBold, + toggleBulletList, + toggleInlineCode, + toggleItalic, + toggleLink, + toggleOrderedList, + toggleStrikethrough, +} from "./md-format.js"; export type FolioAPI = { newBook: () => Promise; @@ -74,6 +89,45 @@ export function initBook(api: FolioAPI) { const editorView: EditorView = createMdEditor(editorHost, { onChange: (text) => handleEditorChange(text), + onSelection: (view) => { + headingSelect.value = String(headingLevelAt(view.state, view.state.selection.main.head)); + }, + }); + + const toolbar = $("editorToolbar"); + const headingSelect = $("headingSelect") as HTMLSelectElement; + const formatCommands: Record boolean> = { + undo, + redo, + bold: toggleBold, + italic: toggleItalic, + strike: toggleStrikethrough, + code: toggleInlineCode, + link: toggleLink, + bullet: toggleBulletList, + number: toggleOrderedList, + quote: toggleBlockquote, + hr: insertHorizontalRule, + clear: clearFormatting, + }; + + toolbar.addEventListener("pointerdown", (e) => { + if ((e.target as HTMLElement).closest("button, select")) e.preventDefault(); + }); + + toolbar.addEventListener("click", (e) => { + const btn = (e.target as HTMLElement).closest("button[data-cmd]") as HTMLButtonElement | null; + if (!btn) return; + const cmd = formatCommands[btn.dataset.cmd ?? ""]; + if (cmd) { + cmd(editorView); + editorView.focus(); + } + }); + + headingSelect.addEventListener("change", () => { + setHeading(Number(headingSelect.value))(editorView); + editorView.focus(); }); function setChapterButtons(enabled: boolean) { diff --git a/src/renderer/index.css b/src/renderer/index.css index 03007ae..c1356f0 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -445,6 +445,86 @@ html, body { overflow: hidden; } +/* ---- editor toolbar ---- */ +.editor-toolbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + padding: 6px 12px; + border-bottom: 1px solid var(--color-border); + background: var(--color-panel); + user-select: none; + -webkit-user-select: none; +} + +.toolbar-group { + display: flex; + align-items: center; + gap: 2px; + padding-right: 8px; + margin-right: 8px; + border-right: 1px solid var(--color-border); +} + +.toolbar-group:last-child { + border-right: none; + margin-right: 0; + padding-right: 0; +} + +.toolbar-btn { + min-width: 30px; + height: 28px; + padding: 0 7px; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--color-text); + font-family: var(--font-sans); + font-size: 0.85rem; + line-height: 1; + cursor: pointer; + transition: background 0.12s ease, border-color 0.12s ease; +} + +.toolbar-btn:hover { + background: var(--color-bg); + border-color: var(--color-border); +} + +.toolbar-btn:active { + background: var(--color-accent); + color: #fff; + border-color: var(--color-accent); +} + +.toolbar-btn.b { font-weight: 700; } +.toolbar-btn.i { font-style: italic; } +.toolbar-btn.s { text-decoration: line-through; } +.toolbar-btn.code { + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.75rem; +} + +.toolbar-select { + height: 28px; + max-width: 130px; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-sans); + font-size: 0.82rem; + padding: 0 6px; + cursor: pointer; + outline: none; +} + +.toolbar-select:focus { + border-color: var(--color-accent); +} + .cm-editor { flex: 1; min-width: 0; diff --git a/src/renderer/index.html b/src/renderer/index.html index 1c44a73..b625eeb 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -48,6 +48,37 @@ +
+
+ + +
+
+ +
+
+ + + + + +
+
+ + + + + +
+