From 9472a119cc559180c6b0f58ae4e886fff8b904da Mon Sep 17 00:00:00 2001 From: mmann1123 Date: Mon, 6 Apr 2026 15:59:58 -0400 Subject: [PATCH] Add MathLive visual formula editor button to toolbar --- package-lock.json | 36 +++++++++++- package.json | 1 + src/components/MathLiveDialog.jsx | 79 +++++++++++++++++++++++++++ src/components/Toolbar.jsx | 7 ++- src/index.css | 91 +++++++++++++++++++++++++++++++ src/pages/ProjectEditor.jsx | 11 ++++ 6 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 src/components/MathLiveDialog.jsx diff --git a/package-lock.json b/package-lock.json index 2ba34f0..65af443 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "firebase": "^11.1.0", "jszip": "^3.10.1", "lib0": "^0.2.117", + "mathlive": "^0.109.1", "nspell": "^2.1.5", "pdfjs-dist": "^4.9.155", "react": "^18.3.1", @@ -471,6 +472,19 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@cortex-js/compute-engine": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/@cortex-js/compute-engine/-/compute-engine-0.30.2.tgz", + "integrity": "sha512-Zx+iisk9WWdbxjm8EYsneIBszvjfUs7BHNwf1jBtSINIgfWGpHrTTq9vW0J59iGCFt6bOFxbmWyxNMRSmksHMA==", + "dependencies": { + "complex-esm": "^2.1.1-esm1", + "decimal.js": "^10.6.0" + }, + "engines": { + "node": ">=21.7.3", + "npm": ">=10.5.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -2965,6 +2979,15 @@ "node": ">= 0.8" } }, + "node_modules/complex-esm": { + "version": "2.1.1-esm1", + "resolved": "https://registry.npmjs.org/complex-esm/-/complex-esm-2.1.1-esm1.tgz", + "integrity": "sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==", + "engines": { + "node": ">=16.14.2", + "npm": ">=8.5.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3064,7 +3087,6 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -3814,6 +3836,18 @@ "node": ">= 0.4" } }, + "node_modules/mathlive": { + "version": "0.109.1", + "resolved": "https://registry.npmjs.org/mathlive/-/mathlive-0.109.1.tgz", + "integrity": "sha512-TXNkTzdJnk/6SFt0Oezy3bpZkH7aCDriuh2usLhVX8dMS5TMmx/rLd7+T1W2b9VCbEZcXABzhmm6ZMxYr8sFXg==", + "dependencies": { + "@cortex-js/compute-engine": "0.30.2" + }, + "funding": { + "type": "individual", + "url": "https://paypal.me/arnogourdol" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", diff --git a/package.json b/package.json index 331af88..7dff299 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "firebase": "^11.1.0", "jszip": "^3.10.1", "lib0": "^0.2.117", + "mathlive": "^0.109.1", "nspell": "^2.1.5", "pdfjs-dist": "^4.9.155", "react": "^18.3.1", diff --git a/src/components/MathLiveDialog.jsx b/src/components/MathLiveDialog.jsx new file mode 100644 index 0000000..7a1048a --- /dev/null +++ b/src/components/MathLiveDialog.jsx @@ -0,0 +1,79 @@ +import { useRef, useState, useEffect } from 'react'; +import 'mathlive'; + +export default function MathLiveDialog({ onInsert, onClose }) { + const mathFieldRef = useRef(null); + const [mode, setMode] = useState('display'); + + useEffect(() => { + // Focus the math field when dialog opens + const mf = mathFieldRef.current; + if (mf) { + requestAnimationFrame(() => mf.focus()); + } + }, []); + + function handleInsert() { + const mf = mathFieldRef.current; + if (!mf) return; + const latex = mf.value; + if (!latex.trim()) return; + + const wrapped = mode === 'display' + ? `\\[\n${latex}\n\\]` + : `$${latex}$`; + onInsert(wrapped); + onClose(); + } + + function handleKeyDown(e) { + if (e.key === 'Escape') onClose(); + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleInsert(); + } + + return ( +
+
e.stopPropagation()}> + +

Formula Editor

+

+ Type LaTeX or use the virtual keyboard to build your equation. + Press Ctrl+Enter to insert. +

+ +
+
+ + +
+
+ + +
+
+
+
+ ); +} diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index dd4d770..9f434fc 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -30,7 +30,7 @@ const NUMBERED_SNIPPET = `\\begin{enumerate} \\item \\end{enumerate}`; -export default function Toolbar({ onInsert, onUndo, onRedo }) { +export default function Toolbar({ onInsert, onUndo, onRedo, onFormulaEditor }) { const [figureOpen, setFigureOpen] = useState(false); const [tableOpen, setTableOpen] = useState(false); @@ -67,6 +67,11 @@ export default function Toolbar({ onInsert, onUndo, onRedo }) { + {onFormulaEditor && ( + + )} {/* Figure dropdown */} diff --git a/src/index.css b/src/index.css index 36cd1ac..4526df5 100644 --- a/src/index.css +++ b/src/index.css @@ -1798,6 +1798,97 @@ body { } } +/* ===== MathLive Formula Dialog ===== */ +.mathlive-dialog { + background: #2a2a3d; + border-radius: 10px; + width: 600px; + max-width: 95vw; + padding: 24px; + position: relative; + color: #e0e0e0; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); +} +.mathlive-dialog h3 { + margin: 0 0 4px; + font-size: 18px; + color: #fff; +} +.mathlive-hint { + font-size: 13px; + color: #888; + margin-bottom: 16px; +} +.mathlive-field { + display: block; + width: 100%; + min-height: 60px; + font-size: 22px; + background: #1e1e2e; + color: #fff; + border: 1px solid #3a3a50; + border-radius: 6px; + padding: 12px; + --selection-background-color: rgba(76, 175, 80, 0.3); +} +.mathlive-field:focus-within { + border-color: #4caf50; +} +.mathlive-controls { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16px; + flex-wrap: wrap; + gap: 12px; +} +.mathlive-mode-toggle { + display: flex; + gap: 16px; + font-size: 13px; + color: #bbb; +} +.mathlive-mode-toggle label { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; +} +.mathlive-mode-toggle code { + font-size: 11px; + color: #888; +} +.mathlive-actions { + display: flex; + gap: 8px; +} +.mathlive-btn-cancel { + background: transparent; + color: #999; + border: 1px solid #555; + padding: 8px 20px; + border-radius: 4px; + font-size: 13px; + cursor: pointer; +} +.mathlive-btn-cancel:hover { + border-color: #888; + color: #ccc; +} +.mathlive-btn-insert { + background: #4caf50; + color: #fff; + border: none; + padding: 8px 20px; + border-radius: 4px; + font-size: 13px; + font-weight: 600; + cursor: pointer; +} +.mathlive-btn-insert:hover { + background: #43a047; +} + /* ===== Collaborator Avatars ===== */ .collaborator-avatars { display: flex; diff --git a/src/pages/ProjectEditor.jsx b/src/pages/ProjectEditor.jsx index fe20e46..e3826ca 100644 --- a/src/pages/ProjectEditor.jsx +++ b/src/pages/ProjectEditor.jsx @@ -21,6 +21,7 @@ import CompileLog from '../components/CompileLog.jsx'; import FileTree from '../components/FileTree.jsx'; import DocumentOutline from '../components/DocumentOutline.jsx'; import ShareDialog from '../components/ShareDialog.jsx'; +import MathLiveDialog from '../components/MathLiveDialog.jsx'; import CollaboratorAvatars from '../components/CollaboratorAvatars.jsx'; import FilePreview from '../components/FilePreview.jsx'; @@ -40,6 +41,7 @@ export default function ProjectEditor() { const [titleValue, setTitleValue] = useState(''); const [filesMenuOpen, setFilesMenuOpen] = useState(false); const [shareDialogOpen, setShareDialogOpen] = useState(false); + const [mathDialogOpen, setMathDialogOpen] = useState(false); const [comments, setComments] = useState([]); // Resizable pane state @@ -755,6 +757,7 @@ export default function ProjectEditor() { onInsert={handleInsertSnippet} onUndo={() => editorUndoRedoRef.current?.undo()} onRedo={() => editorUndoRedoRef.current?.redo()} + onFormulaEditor={canEdit ? () => setMathDialogOpen(true) : undefined} /> setShareDialogOpen(false)} /> )} + + {/* MathLive Formula Editor */} + {mathDialogOpen && ( + setMathDialogOpen(false)} + /> + )} ); }