From 344e6e5447603a79454edd77a33afe135064a8c0 Mon Sep 17 00:00:00 2001 From: Muhammad Tayyab <105964960+mtayyab0182@users.noreply.github.com> Date: Mon, 22 Jun 2026 23:52:31 +0500 Subject: [PATCH 1/2] Final UIUX update --- package-lock.json | 17 -- public/css/styles.css | 162 ++++++++------- src/App.js | 37 +++- src/components/Header.jsx | 10 +- src/components/LatexKeyboard.js | 308 ++++++++++++++++++++++++++++ src/components/Layout.jsx | 19 +- src/components/ThemeToggle.jsx | 14 ++ src/index.css | 23 +++ src/index.js | 2 + src/pages/VolumeCalculator.jsx | 226 +++++++++++---------- src/styles/LatexKeyboard.css | 312 +++++++++++++++++++++++++++++ src/styles/responsive.css | 343 ++++++++++++++++++++++++++++++++ src/styles/theme.css | 255 ++++++++++++++++++++++++ 13 files changed, 1514 insertions(+), 214 deletions(-) create mode 100644 src/components/LatexKeyboard.js create mode 100644 src/components/ThemeToggle.jsx create mode 100644 src/styles/LatexKeyboard.css create mode 100644 src/styles/responsive.css create mode 100644 src/styles/theme.css diff --git a/package-lock.json b/package-lock.json index 213e930..7eb235d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15838,23 +15838,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", 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 6e9867a..6cee749 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import Layout from './components/Layout'; +import { useState, useEffect } from 'react'; // PAGES: import Home from './pages/Home'; @@ -10,16 +11,44 @@ import ContinuityFinder from "./pages/ContinuityFinder"; import ExtremeValueFunction from "./pages/ExtremeValueFinder"; import VolumeCalculator from "./pages/VolumeCalculator"; +function ThemeToggle() { + const [theme, setTheme] = useState(() => + localStorage.getItem('cv-theme') ?? 'auto' + ); + + useEffect(() => { + const root = document.documentElement; + if (theme === 'auto') { + root.removeAttribute('data-theme'); + } else { + root.setAttribute('data-theme', theme); + } + localStorage.setItem('cv-theme', theme); + }, [theme]); + + const cycle = () => + setTheme(t => t === 'auto' ? 'dark' : t === 'dark' ? 'light' : 'auto'); + + const icon = theme === 'dark' ? '๐ŸŒ™' : theme === 'light' ? 'โ˜€๏ธ' : '๐ŸŒ—'; + + return ( + + ); +} + function App() { return ( <> - } />} /> - } />} /> - } />} /> - } />} /> + } body={} />} /> + } body={} />} /> + } body={} />} /> + } body={} />} /> diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 758b34c..76a1cb2 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,8 +1,10 @@ -function Header() { +function Header({ toggle }) { return ( - <> - +
+

CalcVoyager

+ {toggle} +
); } -export default Header; +export default Header; \ No newline at end of file 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 โ”€โ”€ */} +
+