@fnix/lexxy-mathjax 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @fnix/lexxy-mathjax
2
+
3
+ LaTeX equations for [Lexxy](https://lexxy.dev) (the rich text editor for Rails), rendered with [MathJax](https://www.mathjax.org).
4
+
5
+ - Toolbar button (and `Cmd/Ctrl+Shift+E`) opens a dialog with a LaTeX input and live preview
6
+ - Click any equation to edit it
7
+ - Inline (`e^{i\pi} + 1 = 0` in a sentence) and display/block modes
8
+ - Saved HTML stores the raw LaTeX in a `data-latex` attribute, so content round-trips cleanly and is typeset again on display pages
9
+
10
+ ## How it works
11
+
12
+ Equations are stored in the document (and in what ActionText persists) as:
13
+
14
+ ```html
15
+ <span class="lexxy-math" data-latex="\frac{a}{b}">\frac{a}{b}</span> <!-- inline -->
16
+ <div class="lexxy-math" data-latex="\int_0^1 x\,dx">...</div> <!-- display/block -->
17
+ ```
18
+
19
+ Inside the editor the extension typesets these with the page's MathJax. **MathJax is host-provided, not bundled**: load MathJax v3 (`tex-chtml`) on any page that uses the editor or displays saved content.
20
+
21
+ ## Installation
22
+
23
+ ### With importmap-rails (no build)
24
+
25
+ Vendor this package (or pin it) and pin lexxy's bare specifier so the extension resolves it:
26
+
27
+ ```ruby
28
+ # config/importmap.rb
29
+ pin "lexxy" # from the lexxy gem, per lexxy's install docs
30
+ pin "@37signals/lexxy", to: "lexxy.js" # alias used by @fnix/lexxy-mathjax
31
+ pin "@fnix/lexxy-mathjax", to: "lexxy-mathjax/index.js" # vendored src/ of this package
32
+ # ...pin the files under src/ as well if vendoring, e.g. via pin_all_from
33
+ ```
34
+
35
+ ### With a JS bundler (esbuild / vite / webpack)
36
+
37
+ ```bash
38
+ npm install @fnix/lexxy-mathjax @37signals/lexxy
39
+ ```
40
+
41
+ ### Register the extension
42
+
43
+ ```js
44
+ import { configure } from "@37signals/lexxy"
45
+ import MathjaxExtension from "@fnix/lexxy-mathjax"
46
+
47
+ configure({
48
+ global: { extensions: [ MathjaxExtension ] }
49
+ })
50
+ ```
51
+
52
+ Include the stylesheet (`@fnix/lexxy-mathjax/styles`, or copy `styles/lexxy-mathjax.css` into your assets).
53
+
54
+ ### Load MathJax
55
+
56
+ ```html
57
+ <script>
58
+ window.MathJax = { chtml: { displayAlign: "center" } }
59
+ </script>
60
+ <script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
61
+ ```
62
+
63
+ If MathJax is missing, the editor still works and equations show their raw LaTeX.
64
+
65
+ ## Rails / ActionText integration
66
+
67
+ **1. Allow `data-latex` through the server-side sanitizer** (ActionText strips unknown attributes when rendering):
68
+
69
+ ```ruby
70
+ # config/initializers/lexxy_mathjax.rb
71
+ ActiveSupport.on_load(:action_text_content) do
72
+ ActionText::ContentHelper.allowed_attributes += [ "data-latex" ]
73
+ end
74
+ ```
75
+
76
+ **2. Typeset saved content on display pages.** Load MathJax (as above) plus a small script that renders `[data-latex]` elements, Turbo-compatible:
77
+
78
+ ```js
79
+ function typesetEquations() {
80
+ const elements = document.querySelectorAll(".trix-content [data-latex], .lexxy-content [data-latex]")
81
+ if (elements.length === 0 || !window.MathJax?.typesetPromise) return
82
+
83
+ elements.forEach((el) => { el.textContent = el.getAttribute("data-latex") })
84
+ window.MathJax.typesetPromise([ ...elements ])
85
+ }
86
+
87
+ document.addEventListener("turbo:load", typesetEquations)
88
+ document.addEventListener("DOMContentLoaded", typesetEquations)
89
+ ```
90
+
91
+ Note: `typesetPromise` scans for `\(...\)` delimiters by default; the snippet above instead sets each element's content from `data-latex` and typesets just those elements. If you prefer delimiter scanning, configure `tex.inlineMath` accordingly.
92
+
93
+ ## Demo
94
+
95
+ Try it online: https://fnix.github.io/lexxy-mathjax/
96
+
97
+ Or serve the repo statically and open the demo locally:
98
+
99
+ ```bash
100
+ npx serve .
101
+ # then visit http://localhost:3000/demo/
102
+ ```
103
+
104
+ ## Tests
105
+
106
+ ```bash
107
+ npm install
108
+ npm test
109
+ ```
110
+
111
+ ## Development notes
112
+
113
+ The code follows lexxy's own conventions: plain ES modules, vanilla JS, no build step. Structure:
114
+
115
+ - `src/nodes/math_node.js` — `DecoratorNode` storing `{ latex, display }`; `createDOM` typesets with MathJax, `exportDOM`/`importDOM` handle the `data-latex` HTML format
116
+ - `src/extensions/mathjax_extension.js` — the `Lexxy.Extension`: registers the node, the `insertMath` command, the toolbar button, and click-to-edit
117
+ - `src/elements/math_editor_dialog.js` — `<lexxy-math-editor>` dialog with live preview
118
+ - `src/helpers/mathjax_helper.js` — serialized MathJax typesetting with graceful degradation
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@fnix/lexxy-mathjax",
3
+ "version": "0.1.0",
4
+ "description": "MathJax LaTeX equation extension for Lexxy, the rich text editor for Rails.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./styles": "./styles/lexxy-mathjax.css"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "styles"
14
+ ],
15
+ "keywords": [
16
+ "lexxy",
17
+ "lexical",
18
+ "mathjax",
19
+ "latex",
20
+ "rails",
21
+ "actiontext"
22
+ ],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/fnix/lexxy-mathjax.git"
27
+ },
28
+ "homepage": "https://github.com/fnix/lexxy-mathjax#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/fnix/lexxy-mathjax/issues"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "peerDependencies": {
36
+ "@37signals/lexxy": ">= 0.9.0"
37
+ },
38
+ "scripts": {
39
+ "test": "vitest run",
40
+ "demo": "npx serve ."
41
+ },
42
+ "devDependencies": {
43
+ "@37signals/lexxy": "^0.9.24",
44
+ "jsdom": "^27.0.0",
45
+ "vitest": "^4.0.0"
46
+ }
47
+ }
@@ -0,0 +1,117 @@
1
+ import { typesetInto } from "../helpers/mathjax_helper.js"
2
+
3
+ const PREVIEW_DEBOUNCE_INTERVAL = 150
4
+
5
+ export class MathEditorDialogElement extends HTMLElement {
6
+ #resolve = null
7
+ #previewTimer = null
8
+
9
+ connectedCallback() {
10
+ this.innerHTML = this.constructor.template
11
+ this.dialog = this.querySelector("dialog")
12
+ this.textarea = this.querySelector("textarea")
13
+ this.displayCheckbox = this.querySelector("input[name='display']")
14
+ this.preview = this.querySelector(".lexxy-math-editor__preview")
15
+
16
+ this.querySelector("form").addEventListener("submit", this.#handleSubmit)
17
+ this.dialog.addEventListener("close", this.#handleClose)
18
+ this.textarea.addEventListener("input", this.#schedulePreview)
19
+ this.textarea.addEventListener("keydown", this.#handleTextareaKeydown)
20
+ this.displayCheckbox.addEventListener("change", this.#refreshPreview)
21
+ this.querySelector("[data-behavior='cancel']").addEventListener("click", () => this.dialog.close())
22
+ }
23
+
24
+ disconnectedCallback() {
25
+ clearTimeout(this.#previewTimer)
26
+ this.#settle(null)
27
+ }
28
+
29
+ // Shows the dialog and resolves with { latex, display } on confirm, null on cancel.
30
+ open({ latex = "", display = false } = {}) {
31
+ this.textarea.value = latex
32
+ this.displayCheckbox.checked = display
33
+ this.#refreshPreview()
34
+
35
+ this.dialog.showModal()
36
+ this.textarea.focus()
37
+ this.textarea.select()
38
+
39
+ return new Promise((resolve) => {
40
+ this.#resolve = resolve
41
+ })
42
+ }
43
+
44
+ #handleSubmit = (event) => {
45
+ event.preventDefault()
46
+
47
+ const latex = this.textarea.value.trim()
48
+ this.#settle(latex ? { latex, display: this.displayCheckbox.checked } : null)
49
+ this.dialog.close()
50
+ }
51
+
52
+ #handleClose = () => {
53
+ this.#settle(null)
54
+ }
55
+
56
+ #handleTextareaKeydown = (event) => {
57
+ if (event.key === "Enter" && !event.shiftKey) {
58
+ event.preventDefault()
59
+ this.querySelector("form").requestSubmit()
60
+ }
61
+ }
62
+
63
+ #schedulePreview = () => {
64
+ clearTimeout(this.#previewTimer)
65
+ this.#previewTimer = setTimeout(this.#refreshPreview, PREVIEW_DEBOUNCE_INTERVAL)
66
+ }
67
+
68
+ #refreshPreview = () => {
69
+ const latex = this.textarea.value.trim()
70
+
71
+ if (latex) {
72
+ typesetInto(this.preview, latex, { display: this.displayCheckbox.checked })
73
+ } else {
74
+ this.preview.textContent = ""
75
+ }
76
+ }
77
+
78
+ #settle(result) {
79
+ if (this.#resolve) {
80
+ const resolve = this.#resolve
81
+ this.#resolve = null
82
+ resolve(result)
83
+ }
84
+ }
85
+
86
+ static get template() {
87
+ return `
88
+ <dialog class="lexxy-math-editor">
89
+ <form method="dialog" novalidate>
90
+ <label class="lexxy-math-editor__label">
91
+ LaTeX
92
+ <textarea name="latex" rows="3" spellcheck="false" autocomplete="off" placeholder="\\frac{a}{b}"></textarea>
93
+ </label>
94
+
95
+ <div class="lexxy-math-editor__preview" aria-live="polite"></div>
96
+
97
+ <div class="lexxy-math-editor__footer">
98
+ <label class="lexxy-math-editor__display-toggle">
99
+ <input type="checkbox" name="display"> Display as block
100
+ </label>
101
+
102
+ <div class="lexxy-math-editor__actions">
103
+ <button type="button" data-behavior="cancel">Cancel</button>
104
+ <button type="submit" data-behavior="confirm">Save</button>
105
+ </div>
106
+ </div>
107
+ </form>
108
+ </dialog>
109
+ `
110
+ }
111
+ }
112
+
113
+ export function defineMathEditorDialogElement() {
114
+ if (!customElements.get("lexxy-math-editor")) {
115
+ customElements.define("lexxy-math-editor", MathEditorDialogElement)
116
+ }
117
+ }
@@ -0,0 +1,154 @@
1
+ import { Extension, Lexical } from "@37signals/lexxy"
2
+ import { MathNode, $createMathNode, $isMathNode } from "../nodes/math_node.js"
3
+ import { defineMathEditorDialogElement } from "../elements/math_editor_dialog.js"
4
+
5
+ const {
6
+ $getNearestNodeFromDOMNode,
7
+ $getNodeByKey,
8
+ $insertNodes,
9
+ COMMAND_PRIORITY_NORMAL,
10
+ createCommand
11
+ } = Lexical
12
+
13
+ export const INSERT_MATH_COMMAND = createCommand("INSERT_MATH_COMMAND")
14
+
15
+ // Square root of x, drawn to match lexxy's 18x18 filled toolbar icons
16
+ const MATH_ICON = `
17
+ <svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
18
+ <path d="M17 3.5V5.3H10.1L6.7 15.5H5.2L3.3 10.9H1.5V9.1H4.5L5.9 12.5L8.8 3.5Z"/>
19
+ <path d="M10.4 7.9L14.6 13.1M14.6 7.9L10.4 13.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" fill="none"/>
20
+ </svg>
21
+ `
22
+
23
+ export class MathjaxExtension extends Extension {
24
+ #dialog = null
25
+ #editor = null
26
+
27
+ get enabled() {
28
+ return this.editorElement.supportsRichText
29
+ }
30
+
31
+ get allowedElements() {
32
+ return [
33
+ { tag: "span", attributes: [ "data-latex" ] },
34
+ { tag: "div", attributes: [ "data-latex" ] }
35
+ ]
36
+ }
37
+
38
+ get lexicalExtension() {
39
+ const extension = this
40
+
41
+ return this.defineExtension({
42
+ name: "lexxy/mathjax",
43
+ nodes: [ MathNode ],
44
+ register(editor) {
45
+ extension.#editor = editor
46
+
47
+ const unregisterInsert = editor.registerCommand(INSERT_MATH_COMMAND, () => {
48
+ extension.#promptNewEquation()
49
+ return true
50
+ }, COMMAND_PRIORITY_NORMAL)
51
+
52
+ // String command so toolbar buttons can use data-command="insertMath"
53
+ const unregisterToolbarInsert = editor.registerCommand("insertMath", () => {
54
+ extension.#promptNewEquation()
55
+ return true
56
+ }, COMMAND_PRIORITY_NORMAL)
57
+
58
+ const unregisterClicks = editor.registerRootListener((rootElement, previousRootElement) => {
59
+ previousRootElement?.removeEventListener("click", extension.#handleEquationClicked)
60
+ rootElement?.addEventListener("click", extension.#handleEquationClicked)
61
+ })
62
+
63
+ return () => {
64
+ unregisterInsert()
65
+ unregisterToolbarInsert()
66
+ unregisterClicks()
67
+ }
68
+ }
69
+ })
70
+ }
71
+
72
+ initializeToolbar(toolbar) {
73
+ const button = document.createElement("button")
74
+ button.type = "button"
75
+ button.name = "math"
76
+ button.className = "lexxy-editor__toolbar-button"
77
+ button.title = "Insert equation"
78
+ button.setAttribute("data-command", "insertMath")
79
+ button.setAttribute("data-hotkey", "cmd+shift+e ctrl+shift+e")
80
+ button.innerHTML = MATH_ICON
81
+
82
+ toolbar.appendChild(button)
83
+ toolbar.requestOverflowRefresh?.()
84
+ }
85
+
86
+ dispose() {
87
+ this.#dialog?.remove()
88
+ this.#dialog = null
89
+ this.#editor = null
90
+ }
91
+
92
+ async #promptNewEquation() {
93
+ const result = await this.#openDialog()
94
+ if (!result) return
95
+
96
+ this.#editor.update(() => {
97
+ $insertNodes([ $createMathNode(result.latex, result.display) ])
98
+ })
99
+ this.#editor.focus()
100
+ }
101
+
102
+ #handleEquationClicked = async (event) => {
103
+ const element = event.target.closest(".lexxy-math")
104
+ if (!element) return
105
+
106
+ let nodeKey = null
107
+ this.#editor.read(() => {
108
+ const node = $getNearestNodeFromDOMNode(element)
109
+ if ($isMathNode(node)) nodeKey = node.getKey()
110
+ })
111
+ if (nodeKey === null) return
112
+
113
+ event.preventDefault()
114
+
115
+ let currentValues = null
116
+ this.#editor.read(() => {
117
+ const node = $getNodeByKey(nodeKey)
118
+ if ($isMathNode(node)) {
119
+ currentValues = { latex: node.getLatex(), display: node.isDisplay() }
120
+ }
121
+ })
122
+ if (!currentValues) return
123
+
124
+ const result = await this.#openDialog(currentValues)
125
+ if (!result) return
126
+
127
+ this.#editor.update(() => {
128
+ const node = $getNodeByKey(nodeKey)
129
+ if (!$isMathNode(node)) return
130
+
131
+ if (node.isDisplay() === result.display) {
132
+ node.setLatex(result.latex)
133
+ } else {
134
+ // Inline vs block affects where the node may live in the tree, so
135
+ // swap in a fresh node instead of mutating in place.
136
+ node.replace($createMathNode(result.latex, result.display))
137
+ }
138
+ })
139
+ this.#editor.focus()
140
+ }
141
+
142
+ #openDialog(values = {}) {
143
+ return this.#findOrCreateDialog().open(values)
144
+ }
145
+
146
+ #findOrCreateDialog() {
147
+ if (!this.#dialog || !this.#dialog.isConnected) {
148
+ defineMathEditorDialogElement()
149
+ this.#dialog = document.createElement("lexxy-math-editor")
150
+ document.body.appendChild(this.#dialog)
151
+ }
152
+ return this.#dialog
153
+ }
154
+ }
@@ -0,0 +1,62 @@
1
+ const MATHJAX_WAIT_ATTEMPTS = 40
2
+ const MATHJAX_WAIT_INTERVAL = 250
3
+
4
+ let warnedAboutMissingMathJax = false
5
+ let renderQueue = Promise.resolve()
6
+
7
+ // Resolves with window.MathJax once its startup has finished, or null if
8
+ // MathJax never shows up (the host page is responsible for loading it).
9
+ export async function findMathJax() {
10
+ for (let attempt = 0; attempt < MATHJAX_WAIT_ATTEMPTS; attempt++) {
11
+ if (window.MathJax?.startup?.promise) {
12
+ await window.MathJax.startup.promise
13
+ return window.MathJax
14
+ }
15
+ await sleep(MATHJAX_WAIT_INTERVAL)
16
+ }
17
+
18
+ if (!warnedAboutMissingMathJax) {
19
+ warnedAboutMissingMathJax = true
20
+ console.warn("@fnix/lexxy-mathjax: window.MathJax not found. Equations will show raw LaTeX. " +
21
+ "Load MathJax v3 (tex-chtml) on pages that use the editor.")
22
+ }
23
+
24
+ return null
25
+ }
26
+
27
+ // Typesets LaTeX into the given element, replacing its content. Falls back to
28
+ // showing the raw LaTeX when MathJax is unavailable or the LaTeX is invalid.
29
+ // Calls are serialized because MathJax's conversion pipeline is not reentrant.
30
+ export function typesetInto(element, latex, { display = false } = {}) {
31
+ element.textContent = latex
32
+
33
+ renderQueue = renderQueue.then(async () => {
34
+ const mathjax = await findMathJax()
35
+ if (!mathjax) return
36
+
37
+ try {
38
+ const rendered = await mathjax.tex2chtmlPromise(latex, {
39
+ display,
40
+ em: 16,
41
+ ex: 8,
42
+ containerWidth: element.clientWidth || 780
43
+ })
44
+ element.replaceChildren(rendered)
45
+ element.classList.remove("lexxy-math--error")
46
+
47
+ // Refresh MathJax's global stylesheet so glyphs used by this equation render.
48
+ mathjax.startup.document.clear()
49
+ mathjax.startup.document.updateDocument()
50
+ } catch (error) {
51
+ element.textContent = latex
52
+ element.classList.add("lexxy-math--error")
53
+ element.title = `${error}`
54
+ }
55
+ })
56
+
57
+ return renderQueue
58
+ }
59
+
60
+ function sleep(milliseconds) {
61
+ return new Promise((resolve) => setTimeout(resolve, milliseconds))
62
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { MathjaxExtension, INSERT_MATH_COMMAND } from "./extensions/mathjax_extension.js"
2
+ export { MathNode, $createMathNode, $isMathNode } from "./nodes/math_node.js"
3
+ export { MathjaxExtension as default } from "./extensions/mathjax_extension.js"
@@ -0,0 +1,115 @@
1
+ import { Lexical } from "@37signals/lexxy"
2
+ import { typesetInto } from "../helpers/mathjax_helper.js"
3
+
4
+ const { DecoratorNode } = Lexical
5
+
6
+ export class MathNode extends DecoratorNode {
7
+ static getType() {
8
+ return "lexxy-math"
9
+ }
10
+
11
+ static clone(node) {
12
+ return new MathNode(node.__latex, node.__display, node.__key)
13
+ }
14
+
15
+ static importJSON(serializedNode) {
16
+ return $createMathNode(serializedNode.latex, serializedNode.display)
17
+ }
18
+
19
+ static importDOM() {
20
+ return {
21
+ span: (element) => $mathConversionFor(element, false),
22
+ div: (element) => $mathConversionFor(element, true)
23
+ }
24
+ }
25
+
26
+ constructor(latex = "", display = false, key) {
27
+ super(key)
28
+ this.__latex = latex
29
+ this.__display = display
30
+ }
31
+
32
+ getLatex() {
33
+ return this.__latex
34
+ }
35
+
36
+ setLatex(latex) {
37
+ this.getWritable().__latex = latex
38
+ }
39
+
40
+ isDisplay() {
41
+ return this.__display
42
+ }
43
+
44
+ setDisplay(display) {
45
+ this.getWritable().__display = display
46
+ }
47
+
48
+ isInline() {
49
+ return !this.__display
50
+ }
51
+
52
+ isKeyboardSelectable() {
53
+ return true
54
+ }
55
+
56
+ getTextContent() {
57
+ return this.__latex
58
+ }
59
+
60
+ createDOM() {
61
+ const element = document.createElement(this.__display ? "div" : "span")
62
+ element.className = this.__display ? "lexxy-math lexxy-math--display" : "lexxy-math"
63
+ element.setAttribute("role", "math")
64
+ element.setAttribute("aria-label", `Equation: ${this.__latex}`)
65
+ element.setAttribute("data-latex", this.__latex)
66
+
67
+ typesetInto(element, this.__latex, { display: this.__display })
68
+
69
+ return element
70
+ }
71
+
72
+ updateDOM() {
73
+ return true
74
+ }
75
+
76
+ exportDOM() {
77
+ const element = document.createElement(this.__display ? "div" : "span")
78
+ element.className = "lexxy-math"
79
+ element.setAttribute("data-latex", this.__latex)
80
+ element.textContent = this.__latex
81
+ return { element }
82
+ }
83
+
84
+ exportJSON() {
85
+ return {
86
+ type: "lexxy-math",
87
+ version: 1,
88
+ latex: this.__latex,
89
+ display: this.__display
90
+ }
91
+ }
92
+
93
+ decorate() {
94
+ return null
95
+ }
96
+ }
97
+
98
+ export function $createMathNode(latex = "", display = false) {
99
+ return new MathNode(latex, display)
100
+ }
101
+
102
+ export function $isMathNode(node) {
103
+ return node instanceof MathNode
104
+ }
105
+
106
+ function $mathConversionFor(element, display) {
107
+ if (!element.hasAttribute("data-latex")) return null
108
+
109
+ return {
110
+ conversion: (domNode) => ({
111
+ node: $createMathNode(domNode.getAttribute("data-latex"), display)
112
+ }),
113
+ priority: 2
114
+ }
115
+ }
@@ -0,0 +1,111 @@
1
+ /* Equations inside the editor and in rendered content */
2
+
3
+ .lexxy-math {
4
+ display: inline-block;
5
+ padding: 0 0.1em;
6
+ border-radius: 0.2em;
7
+ cursor: pointer;
8
+ }
9
+
10
+ .lexxy-math--display {
11
+ display: block;
12
+ text-align: center;
13
+ margin: 0.5em 0;
14
+ }
15
+
16
+ lexxy-editor .lexxy-math:hover {
17
+ background-color: color-mix(in srgb, currentColor 8%, transparent);
18
+ outline: 1px solid color-mix(in srgb, currentColor 20%, transparent);
19
+ }
20
+
21
+ lexxy-editor .lexxy-math.selected,
22
+ lexxy-editor [data-lexical-decorator="true"]:focus .lexxy-math {
23
+ outline: 2px solid var(--lexxy-focus-color, #2563eb);
24
+ }
25
+
26
+ .lexxy-math--error {
27
+ color: #b91c1c;
28
+ font-family: monospace;
29
+ outline: 1px dashed #b91c1c;
30
+ }
31
+
32
+ /* Equation editor dialog */
33
+
34
+ .lexxy-math-editor::backdrop {
35
+ background: rgba(0, 0, 0, 0.35);
36
+ }
37
+
38
+ .lexxy-math-editor {
39
+ min-width: min(28rem, 90vw);
40
+ border: 1px solid #d4d4d8;
41
+ border-radius: 0.5rem;
42
+ padding: 1rem;
43
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
44
+ }
45
+
46
+ .lexxy-math-editor form {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 0.75rem;
50
+ }
51
+
52
+ .lexxy-math-editor__label {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 0.25rem;
56
+ font-size: 0.85rem;
57
+ color: #52525b;
58
+ }
59
+
60
+ .lexxy-math-editor textarea {
61
+ font-family: ui-monospace, monospace;
62
+ font-size: 0.95rem;
63
+ padding: 0.5rem;
64
+ border: 1px solid #d4d4d8;
65
+ border-radius: 0.375rem;
66
+ resize: vertical;
67
+ }
68
+
69
+ .lexxy-math-editor__preview {
70
+ min-height: 3rem;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ padding: 0.5rem;
75
+ border: 1px dashed #d4d4d8;
76
+ border-radius: 0.375rem;
77
+ overflow-x: auto;
78
+ }
79
+
80
+ .lexxy-math-editor__footer {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ gap: 1rem;
85
+ }
86
+
87
+ .lexxy-math-editor__display-toggle {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.35rem;
91
+ font-size: 0.85rem;
92
+ }
93
+
94
+ .lexxy-math-editor__actions {
95
+ display: flex;
96
+ gap: 0.5rem;
97
+ }
98
+
99
+ .lexxy-math-editor__actions button {
100
+ padding: 0.35rem 0.9rem;
101
+ border-radius: 0.375rem;
102
+ border: 1px solid #d4d4d8;
103
+ background: white;
104
+ cursor: pointer;
105
+ }
106
+
107
+ .lexxy-math-editor__actions button[data-behavior="confirm"] {
108
+ background: var(--lexxy-focus-color, #2563eb);
109
+ border-color: var(--lexxy-focus-color, #2563eb);
110
+ color: white;
111
+ }