diff --git a/apps/web/package.json b/apps/web/package.json index ca80e54c..cdb0ca01 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,6 +32,7 @@ "html-to-image": "^1.11.13", "lowlight": "^3.3.0", "lucide-react": "1.21.0", + "mathlive": "0.111.1", "mediabunny": "^1.61.3", "mermaid": "^11.16.0", "motion": "^12.43.0", diff --git a/apps/web/src/components/EditorToolbar.tsx b/apps/web/src/components/EditorToolbar.tsx index 7be84676..fa77cbd1 100644 --- a/apps/web/src/components/EditorToolbar.tsx +++ b/apps/web/src/components/EditorToolbar.tsx @@ -401,6 +401,16 @@ export const EditorToolbar = ({ )} + {showFormattingTools && onPickMathFormula && ( + + + + )} @@ -575,16 +585,6 @@ export const EditorToolbar = ({ > - {onPickMathFormula && ( - - - - )} { - const trimmed = latex.trim(); - if (!trimmed) return ""; - try { - return katex.renderToString(trimmed, { - displayMode: kind === "block", - throwOnError: false, - strict: "warn", - trust: false, - }); - } catch { - return ""; - } -}; +import { MathVisualInput } from "@/components/editor/MathVisualInput"; +import { hasMathPlaceholders, renderMathPreview } from "@/components/editor/math-formula-preview"; export const MathFormulaDialog = ({ open, @@ -41,13 +27,14 @@ export const MathFormulaDialog = ({ onApply: (draft: MathFormulaDraft) => void; onRemove?: () => void; }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const latexId = useId(); const previewId = useId(); const latexInputRef = useRef(null); const [kind, setKind] = useState(draft?.kind ?? "inline"); const [latex, setLatex] = useState(draft?.latex ?? ""); const [error, setError] = useState(null); + const [inputMode, setInputMode] = useState<"visual" | "source">("visual"); const editing = typeof draft?.pos === "number"; useEffect(() => { @@ -55,20 +42,28 @@ export const MathFormulaDialog = ({ setKind(draft?.kind ?? "inline"); setLatex(draft?.latex ?? ""); setError(null); - const timer = window.setTimeout(() => { - latexInputRef.current?.focus(); - latexInputRef.current?.select(); - }, 0); - return () => window.clearTimeout(timer); + setInputMode("visual"); }, [draft, open]); - const previewHtml = useMemo(() => renderPreviewHtml(latex, kind), [kind, latex]); + useEffect(() => { + if (open && inputMode === "source") latexInputRef.current?.focus(); + }, [inputMode, open]); + + const previewHtml = useMemo(() => renderMathPreview(latex, kind), [kind, latex]); + const incomplete = hasMathPlaceholders(latex); const submit = () => { + if (incomplete) return; if (!latex.trim()) { setError(t("mathFormulaDialog.errorEmpty")); return; } + try { + katex.renderToString(latex.trim(), { throwOnError: true, trust: false, strict: "warn" }); + } catch { + setError(t("mathFormulaDialog.previewError")); + return; + } onApply({ kind, latex, @@ -81,15 +76,12 @@ export const MathFormulaDialog = ({ return ( - + {editing ? t("mathFormulaDialog.editTitle") : t("mathFormulaDialog.title")} - - {t("mathFormulaDialog.description")} -
- +
+ + +
+ {inputMode === "visual" ? (open ? ( + { setLatex(value); setError(null); }} + onLoadError={() => { setInputMode("source"); setError(t("mathFormulaDialog.loadError")); }} + /> + ) : null) : (