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)}
+ />
+ )}
);
}