diff --git a/public/css/styles.css b/public/css/styles.css index c5ffbdb..09ea627 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -7,9 +7,9 @@ } .app-container { - background: white; + background: var(--cv-bg-surface); border-radius: 20px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + box-shadow: var(--cv-shadow-lg); padding: 40px; max-width: 1400px; margin: 0 auto; @@ -18,7 +18,7 @@ /* Header */ .app-title { - color: #333; + color: var(--cv-text-primary); margin-bottom: 10px; font-size: 32px; text-align: center; @@ -26,14 +26,14 @@ .app-subtitle { text-align: center; - color: #666; + color: var(--cv-text-secondary); margin-bottom: 30px; font-size: 16px; } /* Demo Section */ .demo-section { - background: #f8f9fa; + background: var(--cv-bg-sunken); padding: 20px; border-radius: 12px; margin-bottom: 20px; @@ -41,7 +41,7 @@ .demo-title { font-weight: 700; - color: #667eea; + color: var(--cv-accent); margin-bottom: 15px; font-size: 16px; } @@ -56,7 +56,7 @@ grid-column: 1 / -1; margin-top: 15px; margin-bottom: 10px; - color: #667eea; + color: var(--cv-accent); font-size: 14px; font-weight: 700; text-transform: uppercase; @@ -64,24 +64,24 @@ } .demo-item { - background: white; + background: var(--cv-bg-surface); padding: 15px; border-radius: 8px; - border: 2px solid #ddd; + border: 2px solid var(--cv-border); cursor: pointer; transition: all 0.3s ease; } .demo-item:hover { - border-color: #667eea; + border-color: var(--cv-accent); transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2); + box-shadow: var(--cv-shadow-md); } .demo-number { font-size: 12px; font-weight: 700; - color: #667eea; + color: var(--cv-accent); margin-bottom: 8px; } @@ -93,11 +93,12 @@ display: flex; align-items: center; justify-content: center; + color: var(--cv-text-primary); } /* Input Section */ .input-section { - background: #f8f9fa; + background: var(--cv-bg-sunken); padding: 25px; border-radius: 12px; margin-bottom: 20px; @@ -105,16 +106,16 @@ .section-title { font-weight: 700; - color: #333; + color: var(--cv-text-primary); margin-bottom: 20px; font-size: 18px; } .limit-builder { - background: white; + background: var(--cv-bg-surface); padding: 25px; border-radius: 12px; - border: 3px solid #667eea; + border: 3px solid var(--cv-accent); margin-bottom: 20px; } @@ -135,13 +136,13 @@ .limit-label { font-weight: 700; - color: #667eea; + color: var(--cv-accent); font-size: 28px; } .limit-arrow { font-size: 28px; - color: #667eea; + color: var(--cv-accent); } /* Multi-variable container */ @@ -156,57 +157,60 @@ display: flex; align-items: center; gap: 10px; - background: #f8f9fa; + background: var(--cv-bg-sunken); padding: 10px 15px; border-radius: 8px; - border: 2px solid #e0e0e0; + border: 2px solid var(--cv-border); transition: all 0.3s ease; } .variable-row:hover { - border-color: #667eea; - background: white; + border-color: var(--cv-accent); + background: var(--cv-bg-surface); } .variable-name-input { padding: 8px 12px; - border: 2px solid #667eea; + border: 2px solid var(--cv-accent); border-radius: 6px; font-size: 18px; width: 60px; text-align: center; font-weight: 700; - color: #667eea; + color: var(--cv-accent); + background: var(--cv-input-bg); } .variable-name-input:focus { outline: none; - border-color: #764ba2; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + border-color: var(--cv-accent-hover); + box-shadow: 0 0 0 3px var(--cv-accent-light); } .limit-input-inline { padding: 8px 12px; - border: 2px solid #667eea; + border: 2px solid var(--cv-accent); border-radius: 6px; font-size: 18px; width: 140px; text-align: center; font-weight: 600; + background: var(--cv-input-bg); + color: var(--cv-text-primary); } .limit-input-inline:focus { outline: none; - border-color: #764ba2; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + border-color: var(--cv-accent-hover); + box-shadow: 0 0 0 3px var(--cv-accent-light); } .add-var-btn, .remove-var-btn { padding: 6px 12px; - border: 2px solid #667eea; - background: white; - color: #667eea; + border: 2px solid var(--cv-accent); + background: var(--cv-bg-surface); + color: var(--cv-accent); border-radius: 6px; cursor: pointer; font-size: 20px; @@ -216,42 +220,42 @@ } .add-var-btn:hover { - background: #667eea; - color: white; + background: var(--cv-accent); + color: var(--cv-text-inverse); transform: scale(1.1); } .remove-var-btn { - border-color: #dc3545; - color: #dc3545; + border-color: var(--cv-error); + color: var(--cv-error); } .remove-var-btn:hover { - background: #dc3545; + background: var(--cv-error); color: white; transform: scale(1.1); } .direction-select { padding: 8px 12px; - border: 2px solid #667eea; + border: 2px solid var(--cv-accent); border-radius: 6px; font-size: 16px; font-weight: 600; - color: #667eea; - background: white; + color: var(--cv-accent); + background: var(--cv-input-bg); cursor: pointer; margin-left: auto; } .direction-select:focus { outline: none; - border-color: #764ba2; + border-color: var(--cv-accent-hover); } .function-label { font-weight: 600; - color: #333; + color: var(--cv-text-primary); margin-bottom: 10px; font-size: 14px; } @@ -259,17 +263,18 @@ .function-field { min-height: 80px; padding: 20px; - border: 3px solid #667eea; + border: 3px solid var(--cv-accent); border-radius: 12px; font-size: 24px; - background: white; + background: var(--cv-input-bg); + color: var(--cv-text-primary); cursor: text; transition: all 0.3s ease; } .function-field:focus-within { - border-color: #764ba2; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + border-color: var(--cv-accent-hover); + box-shadow: 0 0 0 3px var(--cv-accent-light); } /* Toolbar */ @@ -279,7 +284,7 @@ .toolbar-title { font-weight: 700; - color: #667eea; + color: var(--cv-accent); margin-bottom: 10px; font-size: 13px; text-transform: uppercase; @@ -291,15 +296,15 @@ grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); gap: 8px; padding: 15px; - background: #f8f9fa; + background: var(--cv-bg-sunken); border-radius: 12px; } .toolbar-button { padding: 10px; - border: 2px solid #667eea; - background: white; - color: #667eea; + border: 2px solid var(--cv-accent); + background: var(--cv-bg-surface); + color: var(--cv-accent); border-radius: 8px; cursor: pointer; font-size: 14px; @@ -308,8 +313,8 @@ } .toolbar-button:hover { - background: #667eea; - color: white; + background: var(--cv-accent); + color: var(--cv-text-inverse); transform: translateY(-2px); } @@ -326,20 +331,21 @@ .calculate-button { padding: 18px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; + background: var(--cv-btn-eq-bg); + color: var(--cv-btn-eq-text); border: none; border-radius: 12px; font-size: 18px; font-weight: 700; cursor: pointer; transition: all 0.3s ease; - box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + box-shadow: var(--cv-shadow-md); } .calculate-button:hover { + background: var(--cv-btn-eq-hover); transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); + box-shadow: var(--cv-shadow-lg); } .calculate-button:active { @@ -348,9 +354,9 @@ .clear-button { padding: 18px; - background: white; - color: #667eea; - border: 3px solid #667eea; + background: var(--cv-btn-clear-bg); + color: var(--cv-btn-clear-text); + border: 3px solid var(--cv-btn-clear-text); border-radius: 12px; font-size: 18px; font-weight: 700; @@ -359,8 +365,9 @@ } .clear-button:hover { - background: #667eea; + background: var(--cv-error); color: white; + border-color: var(--cv-error); transform: translateY(-2px); } @@ -374,7 +381,8 @@ } .answer-box { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--cv-accent-light); + border: 2px solid var(--cv-accent); padding: 30px; border-radius: 12px; margin-bottom: 20px; @@ -382,20 +390,20 @@ } .answer-label { - color: rgba(255, 255, 255, 0.9); + color: var(--cv-accent-text); font-size: 18px; font-weight: 600; margin-bottom: 10px; } .answer-value { - color: white; + color: var(--cv-accent); font-size: 36px; font-weight: 700; } .steps-box { - background: #f8f9fa; + background: var(--cv-bg-sunken); padding: 25px; border-radius: 12px; } @@ -403,7 +411,7 @@ .steps-title { font-size: 20px; font-weight: 700; - color: #333; + color: var(--cv-text-primary); margin-bottom: 20px; } @@ -414,33 +422,34 @@ } .step-item { - background: white; + background: var(--cv-bg-surface); padding: 20px; border-radius: 12px; - border: 2px solid #e0e0e0; + border: 2px solid var(--cv-border); transition: all 0.3s ease; } .step-item:hover { - border-color: #667eea; - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); + border-color: var(--cv-accent); + box-shadow: var(--cv-shadow-sm); } .step-number { font-size: 16px; font-weight: 700; - color: #667eea; + color: var(--cv-accent); margin-bottom: 10px; } .step-content { - color: #555; + color: var(--cv-text-secondary); line-height: 1.6; margin-bottom: 10px; } .step-math { - background: #f8f9fa; + background: var(--cv-bg-sunken); + color: var(--cv-text-primary); padding: 15px; border-radius: 8px; margin-top: 10px; @@ -452,13 +461,14 @@ position: fixed; bottom: 30px; right: 30px; - background: #333; - color: white; + background: var(--cv-bg-raised); + color: var(--cv-text-primary); + border: 1px solid var(--cv-border-strong); padding: 15px 25px; border-radius: 8px; font-size: 16px; font-weight: 600; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + box-shadow: var(--cv-shadow-lg); opacity: 0; transform: translateY(20px); transition: all 0.3s ease; diff --git a/src/App.js b/src/App.js index a3c10f8..c48d44c 100644 --- a/src/App.js +++ b/src/App.js @@ -3,19 +3,17 @@ import { AuthProvider } from "./context/AuthContext"; import { ProgressProvider } from "./context/ProgressContext"; import Layout from "./components/Layout"; import ScrollToTop from "./utils/ScrollToTop"; -import IntegralsPart1 from "./pages/IntegralsPart1"; -import IntegralsPart2 from "./pages/IntegralsPart2"; +import ErrorBoundary from "./components/ErrorBoundary"; -// Pages import Home from "./pages/Home"; import Login from "./pages/Login"; import Signup from "./pages/Signup"; import Dashboard from "./pages/Dashboard"; import AISolver from "./pages/AISolver"; import NotFound from "./pages/NotFound"; -import ErrorBoundary from "./components/ErrorBoundary"; -// Guide parts +import IntegralsPart1 from "./pages/IntegralsPart1"; +import IntegralsPart2 from "./pages/IntegralsPart2"; import PartialPart1 from "./pages/PartialPart1"; import PartialPart2 from "./pages/PartialPart2"; import VectorPart1 from "./pages/VectorPart1"; @@ -23,7 +21,6 @@ import VectorPart2 from "./pages/VectorPart2"; import LimitsPart1 from "./pages/LimitsPart1"; import LimitsPart2 from "./pages/LimitsPart2"; -// Tools import ContinuityFinder from "./pages/ContinuityFinder"; import ExtremeValueFunction from "./pages/ExtremeValueFinder"; import VolumeCalculator from "./pages/VolumeCalculator"; @@ -37,6 +34,7 @@ function App() { + {/* Home */} } />} /> {/* Auth */} @@ -62,22 +60,21 @@ function App() { } />} /> } />} /> - {/* Multiple Integrals */} + {/* Multiple Integrals */} } /> } />} /> } />} /> - {/* Tools */} + {/* Tools */} } />} /> } />} /> } />} /> - } /> } />} /> - {/* Catch-all route */} + {/* Catch-all */} } />} /> - + diff --git a/src/components/Header.jsx b/src/components/Header.jsx index aa034d2..3d828d1 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -3,14 +3,14 @@ import { NavLink, Link } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; const navLinks = [ - { to: "/simple-concepts", label: "Concepts", type: "Guide" }, - { to: "/partial-derivatives/1", label: "Partials", type: "Guide" }, - { to: "/vector-calculus/1", label: "Vectors", type: "Guide" }, - { to: "/test", label: "Continuity", type: "Tool" }, - { to: "/extreme", label: "Extrema", type: "Tool" }, - { to: "/volumecalculator", label: "Integrals", type: "Tool" }, - { to: "/taylorx", label: "TaylorX", type: "Tool" }, - { to: "/ai-solver", label: "AI Solver", type: "Tool" }, + { to: "/simple-concepts", label: "Concepts", type: "Guide" }, + { to: "/partial-derivatives/1",label: "Partials", type: "Guide" }, + { to: "/vector-calculus/1", label: "Vectors", type: "Guide" }, + { to: "/test", label: "Continuity",type: "Tool" }, + { to: "/extreme", label: "Extrema", type: "Tool" }, + { to: "/volumecalculator", label: "Integrals", type: "Tool" }, + { to: "/taylorx", label: "TaylorX", type: "Tool" }, + { to: "/ai-solver", label: "AI Solver", type: "Tool" }, ]; function Header({ darkMode, onToggleDark }) { @@ -18,26 +18,37 @@ function Header({ darkMode, onToggleDark }) { const [menuOpen, setMenuOpen] = useState(false); const headerRef = useRef(null); + // Close menu on outside click OR Escape key โ€” combined into one effect useEffect(() => { - const handler = (e) => { + const handleClick = (e) => { if (headerRef.current && !headerRef.current.contains(e.target)) { setMenuOpen(false); } }; - document.addEventListener("mousedown", handler); - return () => document.removeEventListener("mousedown", handler); - }, []); - - useEffect(() => { - const handler = (e) => { + const handleKey = (e) => { if (e.key === "Escape") setMenuOpen(false); }; - document.addEventListener("keydown", handler); - return () => document.removeEventListener("keydown", handler); + document.addEventListener("mousedown", handleClick); + document.addEventListener("keydown", handleKey); + return () => { + document.removeEventListener("mousedown", handleClick); + document.removeEventListener("keydown", handleKey); + }; }, []); + const handleLogout = () => { + try { + logout(); + } catch (err) { + console.error("Logout failed:", err); + } + setMenuOpen(false); + }; + return (
+ + {/* Brand */} setMenuOpen(false)}> @@ -46,6 +57,7 @@ function Header({ darkMode, onToggleDark }) { + {/* Desktop nav */} + {/* Controls: theme toggle + auth + hamburger */}
{user ? (
- {user.username[0].toUpperCase()} + {user.username?.[0]?.toUpperCase() ?? "U"}
) : (
- Sign in + Sign in Sign up
)} @@ -89,6 +102,7 @@ function Header({ darkMode, onToggleDark }) {
+ {/* Mobile nav */} +
); } diff --git a/src/components/LatexKeyboard.js b/src/components/LatexKeyboard.js new file mode 100644 index 0000000..3235671 --- /dev/null +++ b/src/components/LatexKeyboard.js @@ -0,0 +1,308 @@ +import React, { useState, useCallback, useRef } from 'react'; +import katex from 'katex'; +import 'katex/dist/katex.min.css'; +import './LatexKeyboard.css'; +import LatexKeyboard from '../components/LatexKeyboard'; + +/* ============================================================ + LaTeX Math Keyboard + Props: + value {string} โ€” controlled LaTeX string + onChange {function} โ€” called with new LaTeX string + onInsert {function} โ€” called when user inserts from keyboard + placeholder {string} โ€” placeholder text when empty + compact {boolean} โ€” single-row tab strip instead of full panel + ============================================================ */ + +const KEY_GROUPS = [ + { + id: 'basic', + label: 'Basic', + icon: 'ยฑ', + keys: [ + { display: 'xยฒ', latex: '^{2}', label: 'squared' }, + { display: 'xโฟ', latex: '^{}', label: 'power', cursor: -1 }, + { display: 'โˆš', latex: '\\sqrt{}', label: 'square root', cursor: -1 }, + { display: 'โฟโˆš', latex: '\\sqrt[n]{}', label: 'nth root', cursor: -1 }, + { display: '|x|', latex: '\\left|\\right|', label: 'absolute value' }, + { display: 'a/b', latex: '\\frac{}{}', label: 'fraction', cursor: -1 }, + { display: 'ยฑ', latex: '\\pm', label: 'plus minus' }, + { display: 'โ‰ ', latex: '\\neq', label: 'not equal' }, + { display: 'โ‰ค', latex: '\\leq', label: 'less or equal' }, + { display: 'โ‰ฅ', latex: '\\geq', label: 'greater or equal' }, + { display: 'โ‰ˆ', latex: '\\approx', label: 'approx' }, + { display: 'โˆž', latex: '\\infty', label: 'infinity' }, + { display: '%', latex: '\\%', label: 'percent' }, + { display: 'ยท', latex: '\\cdot', label: 'dot product' }, + { display: 'ร—', latex: '\\times', label: 'times' }, + { display: 'รท', latex: '\\div', label: 'divide' }, + ], + }, + { + id: 'calculus', + label: 'Calculus', + icon: 'โˆซ', + keys: [ + { display: 'd/dx', latex: '\\frac{d}{dx}', label: 'derivative' }, + { display: 'dยฒ/dxยฒ',latex: '\\frac{d^2}{dx^2}', label: '2nd derivative' }, + { display: 'โˆ‚/โˆ‚x', latex: '\\frac{\\partial}{\\partial x}', label: 'partial deriv.' }, + { display: 'โˆซ', latex: '\\int', label: 'integral' }, + { display: 'โˆซab', latex: '\\int_{}^{}', label: 'definite integral', cursor: -1 }, + { display: 'โˆฌ', latex: '\\iint', label: 'double integral' }, + { display: 'โˆญ', latex: '\\iiint', label: 'triple integral' }, + { display: 'โˆฎ', latex: '\\oint', label: 'contour integral' }, + { display: 'lim', latex: '\\lim_{ \\to }', label: 'limit' }, + { display: 'ฮฃ', latex: '\\sum_{}^{}', label: 'sum', cursor: -1 }, + { display: 'ฮ ', latex: '\\prod_{}^{}', label: 'product', cursor: -1 }, + { display: 'โˆ‡', latex: '\\nabla', label: 'nabla / gradient' }, + ], + }, + { + id: 'trig', + label: 'Trig', + icon: 'โˆฟ', + keys: [ + { display: 'sin', latex: '\\sin', label: 'sine' }, + { display: 'cos', latex: '\\cos', label: 'cosine' }, + { display: 'tan', latex: '\\tan', label: 'tangent' }, + { display: 'csc', latex: '\\csc', label: 'cosecant' }, + { display: 'sec', latex: '\\sec', label: 'secant' }, + { display: 'cot', latex: '\\cot', label: 'cotangent' }, + { display: 'sinโปยน', latex: '\\arcsin', label: 'arcsine' }, + { display: 'cosโปยน', latex: '\\arccos', label: 'arccosine' }, + { display: 'tanโปยน', latex: '\\arctan', label: 'arctangent' }, + { display: 'sinh', latex: '\\sinh', label: 'hyperbolic sin' }, + { display: 'cosh', latex: '\\cosh', label: 'hyperbolic cos' }, + { display: 'tanh', latex: '\\tanh', label: 'hyperbolic tan' }, + { display: 'log', latex: '\\log', label: 'log base 10' }, + { display: 'ln', latex: '\\ln', label: 'natural log' }, + { display: 'logโ‚™', latex: '\\log_{}', label: 'log base n', cursor: -1 }, + { display: 'eหฃ', latex: 'e^{}', label: 'exponential', cursor: -1 }, + ], + }, + { + id: 'greek', + label: 'Greek', + icon: 'ฮฑ', + keys: [ + { display: 'ฮฑ', latex: '\\alpha', label: 'alpha' }, + { display: 'ฮฒ', latex: '\\beta', label: 'beta' }, + { display: 'ฮณ', latex: '\\gamma', label: 'gamma' }, + { display: 'ฮ“', latex: '\\Gamma', label: 'Gamma (upper)' }, + { display: 'ฮด', latex: '\\delta', label: 'delta' }, + { display: 'ฮ”', latex: '\\Delta', label: 'Delta (upper)' }, + { display: 'ฮต', latex: '\\epsilon', label: 'epsilon' }, + { display: 'ฮถ', latex: '\\zeta', label: 'zeta' }, + { display: 'ฮท', latex: '\\eta', label: 'eta' }, + { display: 'ฮธ', latex: '\\theta', label: 'theta' }, + { display: 'ฮ˜', latex: '\\Theta', label: 'Theta (upper)' }, + { display: 'ฮป', latex: '\\lambda', label: 'lambda' }, + { display: 'ฮ›', latex: '\\Lambda', label: 'Lambda (upper)' }, + { display: 'ฮผ', latex: '\\mu', label: 'mu' }, + { display: 'ฮฝ', latex: '\\nu', label: 'nu' }, + { display: 'ฯ€', latex: '\\pi', label: 'pi' }, + { display: 'ฮ ', latex: '\\Pi', label: 'Pi (upper)' }, + { display: 'ฯ', latex: '\\rho', label: 'rho' }, + { display: 'ฯƒ', latex: '\\sigma', label: 'sigma' }, + { display: 'ฮฃ', latex: '\\Sigma', label: 'Sigma (upper)' }, + { display: 'ฯ„', latex: '\\tau', label: 'tau' }, + { display: 'ฯ†', latex: '\\phi', label: 'phi' }, + { display: 'ฮฆ', latex: '\\Phi', label: 'Phi (upper)' }, + { display: 'ฯ‡', latex: '\\chi', label: 'chi' }, + { display: 'ฯˆ', latex: '\\psi', label: 'psi' }, + { display: 'ฮจ', latex: '\\Psi', label: 'Psi (upper)' }, + { display: 'ฯ‰', latex: '\\omega', label: 'omega' }, + { display: 'ฮฉ', latex: '\\Omega', label: 'Omega (upper)' }, + ], + }, + { + id: 'symbols', + label: 'Symbols', + icon: 'โˆˆ', + keys: [ + { display: 'โˆˆ', latex: '\\in', label: 'element of' }, + { display: 'โˆ‰', latex: '\\notin', label: 'not element of' }, + { display: 'โŠ‚', latex: '\\subset', label: 'subset' }, + { display: 'โІ', latex: '\\subseteq', label: 'subset or equal' }, + { display: 'โŠƒ', latex: '\\supset', label: 'superset' }, + { display: 'โˆช', latex: '\\cup', label: 'union' }, + { display: 'โˆฉ', latex: '\\cap', label: 'intersection' }, + { display: 'โˆ…', latex: '\\emptyset', label: 'empty set' }, + { display: 'โ„', latex: '\\mathbb{R}', label: 'real numbers' }, + { display: 'โ„ค', latex: '\\mathbb{Z}', label: 'integers' }, + { display: 'โ„•', latex: '\\mathbb{N}', label: 'naturals' }, + { display: 'โ„š', latex: '\\mathbb{Q}', label: 'rationals' }, + { display: 'โˆ€', latex: '\\forall', label: 'for all' }, + { display: 'โˆƒ', latex: '\\exists', label: 'there exists' }, + { display: 'ยฌ', latex: '\\neg', label: 'negation' }, + { display: 'โˆง', latex: '\\land', label: 'and' }, + { display: 'โˆจ', latex: '\\lor', label: 'or' }, + { display: 'โ†’', latex: '\\rightarrow', label: 'right arrow' }, + { display: 'โ†”', latex: '\\leftrightarrow', label: 'biconditional' }, + { display: 'โ‡’', latex: '\\Rightarrow', label: 'implies' }, + { display: 'โŸน', latex: '\\implies', label: 'implies (long)' }, + { display: 'โŸบ', latex: '\\iff', label: 'iff' }, + ], + }, +]; + +/* Renders a LaTeX string safely. Returns null if katex throws. */ +function renderLatex(latex) { + if (!latex || !latex.trim()) return null; + try { + return katex.renderToString(latex, { + throwOnError: false, + displayMode: true, + output: 'html', + }); + } catch { + return null; + } +} + +export default function LatexKeyboard({ + value = '', + onChange, + onInsert, + placeholder = 'Type or tap keys to build a math expressionโ€ฆ', + compact = false, +}) { + const [activeGroup, setActiveGroup] = useState('basic'); + const [inputValue, setInputValue] = useState(value); + const inputRef = useRef(null); + + /* Sync internal state if parent changes value prop */ + React.useEffect(() => { + setInputValue(value); + }, [value]); + + const handleInputChange = useCallback((e) => { + const v = e.target.value; + setInputValue(v); + onChange?.(v); + }, [onChange]); + + const insertLatex = useCallback((latex, cursorOffset) => { + const el = inputRef.current; + if (!el) { + const next = inputValue + latex; + setInputValue(next); + onChange?.(next); + onInsert?.(latex); + return; + } + + const start = el.selectionStart ?? inputValue.length; + const end = el.selectionEnd ?? inputValue.length; + const next = inputValue.slice(0, start) + latex + inputValue.slice(end); + setInputValue(next); + onChange?.(next); + onInsert?.(latex); + + /* Restore focus and position cursor inside braces when cursorOffset is set */ + requestAnimationFrame(() => { + el.focus(); + const pos = cursorOffset !== undefined + ? start + latex.length + cursorOffset + : start + latex.length; + el.setSelectionRange(pos, pos); + }); + }, [inputValue, onChange, onInsert]); + + const currentGroup = KEY_GROUPS.find(g => g.id === activeGroup) ?? KEY_GROUPS[0]; + const rendered = renderLatex(inputValue); + + return ( +
+ + {/* โ”€โ”€ Input row โ”€โ”€ */} +
+