diff --git a/.agents/skills/ofb/SKILL.md b/.agents/skills/ofb/SKILL.md index 4ea0f34..45ab749 100644 --- a/.agents/skills/ofb/SKILL.md +++ b/.agents/skills/ofb/SKILL.md @@ -137,6 +137,8 @@ Live chapters live under `content/{subject}/{slug}/`: **Math equation layout (all subjects):** for chained calculations, **always prefer multi-line KaTeX** and **always align at the equal sign** using `aligned` (e.g. `\begin{aligned} ... &= ... \\ ... \end{aligned}`). Do not write long one-line chains; aligned multi-line steps are the default for readability and mobile layout. +**Dialog/modals in widgets:** when adding a `` in chapter widgets, use the shared modal pattern and classes from `themes/openfreebooks/sass/_contributors.scss` (`contributor-dialog`, `contributor-dialog-panel`, `contributor-dialog-close`) as the base style, keep the dialog centered in modal mode, and only layer minimal widget-specific overrides on top. + ### Interactive demo technology choices Interactive demos can use different technologies based on the learning goal and interaction type: diff --git a/content/math/algebraic-fractions-formulas/_index.md b/content/math/algebraic-fractions-formulas/_index.md index 5ccaa0d..67a7ba2 100644 --- a/content/math/algebraic-fractions-formulas/_index.md +++ b/content/math/algebraic-fractions-formulas/_index.md @@ -59,13 +59,26 @@ strand = "Number & Algebra"

Factorise: \(x^2 - 1 = (x-1)(x+1)\) and \(x^2 + x = x(x+1)\). Cancel the common factor \((x+1)\): - \[ \frac{(x-1)(x+1)}{x(x+1)} = \frac{x-1}{x}, \quad x \neq 0,\; x \neq -1. \] + \[ + \begin{aligned} + \frac{(x-1)(x+1)}{x(x+1)} + &= \frac{x-1}{x}, \\ + x &\neq 0,\; x \neq -1 + \end{aligned} + \] The restrictions matter: at \(x = -1\) the original denominator is zero.

Example. Simplify \(\dfrac{2x^2 - 8}{x^2 - 4}\).

- \[ \frac{2(x^2-4)}{x^2-4} = \frac{2(x-2)(x+2)}{(x-2)(x+2)} = 2, \quad x \neq 2,\; x \neq -2. \] + \[ + \begin{aligned} + \frac{2(x^2-4)}{x^2-4} + &= \frac{2(x-2)(x+2)}{(x-2)(x+2)} \\ + &= 2, \\ + x &\neq 2,\; x \neq -2 + \end{aligned} + \]

Adding and subtracting

@@ -77,13 +90,26 @@ strand = "Number & Algebra"

Example. \(\dfrac{2}{x} + \dfrac{3}{x+1}\).

LCD is \(x(x+1)\): - \[ \frac{2}{x} + \frac{3}{x+1} = \frac{2(x+1) + 3x}{x(x+1)} = \frac{5x+2}{x(x+1)}, \quad x \neq 0,\; x \neq -1. \] + \[ + \begin{aligned} + \frac{2}{x} + \frac{3}{x+1} + &= \frac{2(x+1) + 3x}{x(x+1)} \\ + &= \frac{5x+2}{x(x+1)}, \\ + x &\neq 0,\; x \neq -1 + \end{aligned} + \]

Example. \(\dfrac{1}{x-2} - \dfrac{3}{x^2 - 4}\).

Note \(x^2 - 4 = (x-2)(x+2)\). With LCD \((x-2)(x+2)\): - \[ \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} = \frac{x-1}{(x-2)(x+2)}, \quad x \neq 2,\; x \neq -2. \] + \[ + \begin{aligned} + \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} + &= \frac{x-1}{(x-2)(x+2)}, \\ + x &\neq 2,\; x \neq -2 + \end{aligned} + \]

Multiplying and dividing

@@ -101,12 +127,25 @@ strand = "Number & Algebra"

Example. \(\dfrac{x^2 - 9}{2x} \times \dfrac{4}{x+3}\).

- \[ \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} = \frac{4(x-3)}{2x} = \frac{2(x-3)}{x}, \quad x \neq 0,\; x \neq -3. \] + \[ + \begin{aligned} + \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} + &= \frac{4(x-3)}{2x} \\ + &= \frac{2(x-3)}{x}, \\ + x &\neq 0,\; x \neq -3 + \end{aligned} + \]

Example. \(\dfrac{x^2 - 1}{x+1} \div \dfrac{x-1}{2}\).

- \[ \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} = 2, \quad x \neq -1,\; x \neq 1. \] + \[ + \begin{aligned} + \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} + &= 2, \\ + x &\neq -1,\; x \neq 1 + \end{aligned} + \]

Harder examples

@@ -273,8 +312,16 @@ strand = "Number & Algebra"

Uniform motion

For constant speed, distance \(d\), speed \(v\), and time \(t\) are related by \(v = d/t\). If a - train travels \(240\) km in \(3\) h, then \(v = 240/3 = 80\) km/h. If you know \(v\) and \(t\) - but need distance, rearrange to \(d = vt\). + train travels \(240\) km in \(3\) h, then: +

+

\[ + \begin{aligned} + v &= \frac{240}{3} \\ + &= 80\ \text{km/h} + \end{aligned} + \]

+

+ If you know \(v\) and \(t\) but need distance, rearrange to \(d = vt\).

@@ -282,8 +329,16 @@ strand = "Number & Algebra"

Density

Density \(\rho\) (rho) is mass per unit volume: \(\rho = m/V\). A block of mass \(2.7\) kg with - volume \(0.001\) m³ has \(\rho = 2.7/0.001 = 2700\) kg/m³. To find mass from \(\rho\) and \(V\), - use \(m = \rho V\). + volume \(0.001\) m³ has: +

+

\[ + \begin{aligned} + \rho &= \frac{2.7}{0.001} \\ + &= 2700\ \text{kg/m}^3 + \end{aligned} + \]

+

+ To find mass from \(\rho\) and \(V\), use \(m = \rho V\).

Pressure and electric circuits

@@ -408,7 +463,14 @@ strand = "Number & Algebra"
Make \(t\) the subject of \(v = \dfrac{d}{t}\) and then find \(t\) when \(d = 150\) km and \(v = 75\) km/h.
-

Answer: \(t = d/v = 2\) h.

+

Answer:

+

\[ + \begin{aligned} + t &= \frac{d}{v} \\ + &= \frac{150}{75} \\ + &= 2\ \text{h} + \end{aligned} + \]

Steps: \(vt = d \Rightarrow t = d/v\); substitute values.

@@ -424,7 +486,20 @@ strand = "Number & Algebra"
A formula is \(P = \dfrac{F}{A}\). The force is \(120\) N and the area is \(0.05\) m². Find \(P\), then make \(F\) the subject and verify your force value.
-

Answer: \(P = 120/0.05 = 2400\) Pa; \(F = PA = 2400 \times 0.05 = 120\) N.

+

Answer:

+

\[ + \begin{aligned} + P &= \frac{120}{0.05} \\ + &= 2400\ \text{Pa} + \end{aligned} + \]

+

\[ + \begin{aligned} + F &= PA \\ + &= 2400 \times 0.05 \\ + &= 120\ \text{N} + \end{aligned} + \]

Explanation: Rearranging checks that the formula is consistent.

diff --git a/content/math/algebraic-fractions-formulas/core.html b/content/math/algebraic-fractions-formulas/core.html index 9cfa3a4..2c2e6ae 100644 --- a/content/math/algebraic-fractions-formulas/core.html +++ b/content/math/algebraic-fractions-formulas/core.html @@ -48,13 +48,26 @@

Factorise: \(x^2 - 1 = (x-1)(x+1)\) and \(x^2 + x = x(x+1)\). Cancel the common factor \((x+1)\): - \[ \frac{(x-1)(x+1)}{x(x+1)} = \frac{x-1}{x}, \quad x \neq 0,\; x \neq -1. \] + \[ + \begin{aligned} + \frac{(x-1)(x+1)}{x(x+1)} + &= \frac{x-1}{x}, \\ + x &\neq 0,\; x \neq -1 + \end{aligned} + \] The restrictions matter: at \(x = -1\) the original denominator is zero.

Example. Simplify \(\dfrac{2x^2 - 8}{x^2 - 4}\).

- \[ \frac{2(x^2-4)}{x^2-4} = \frac{2(x-2)(x+2)}{(x-2)(x+2)} = 2, \quad x \neq 2,\; x \neq -2. \] + \[ + \begin{aligned} + \frac{2(x^2-4)}{x^2-4} + &= \frac{2(x-2)(x+2)}{(x-2)(x+2)} \\ + &= 2, \\ + x &\neq 2,\; x \neq -2 + \end{aligned} + \]

Adding and subtracting

@@ -66,13 +79,26 @@

Example. \(\dfrac{2}{x} + \dfrac{3}{x+1}\).

LCD is \(x(x+1)\): - \[ \frac{2}{x} + \frac{3}{x+1} = \frac{2(x+1) + 3x}{x(x+1)} = \frac{5x+2}{x(x+1)}, \quad x \neq 0,\; x \neq -1. \] + \[ + \begin{aligned} + \frac{2}{x} + \frac{3}{x+1} + &= \frac{2(x+1) + 3x}{x(x+1)} \\ + &= \frac{5x+2}{x(x+1)}, \\ + x &\neq 0,\; x \neq -1 + \end{aligned} + \]

Example. \(\dfrac{1}{x-2} - \dfrac{3}{x^2 - 4}\).

Note \(x^2 - 4 = (x-2)(x+2)\). With LCD \((x-2)(x+2)\): - \[ \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} = \frac{x-1}{(x-2)(x+2)}, \quad x \neq 2,\; x \neq -2. \] + \[ + \begin{aligned} + \frac{x+2}{(x-2)(x+2)} - \frac{3}{(x-2)(x+2)} + &= \frac{x-1}{(x-2)(x+2)}, \\ + x &\neq 2,\; x \neq -2 + \end{aligned} + \]

Multiplying and dividing

@@ -90,12 +116,25 @@

Example. \(\dfrac{x^2 - 9}{2x} \times \dfrac{4}{x+3}\).

- \[ \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} = \frac{4(x-3)}{2x} = \frac{2(x-3)}{x}, \quad x \neq 0,\; x \neq -3. \] + \[ + \begin{aligned} + \frac{(x-3)(x+3)}{2x} \times \frac{4}{x+3} + &= \frac{4(x-3)}{2x} \\ + &= \frac{2(x-3)}{x}, \\ + x &\neq 0,\; x \neq -3 + \end{aligned} + \]

Example. \(\dfrac{x^2 - 1}{x+1} \div \dfrac{x-1}{2}\).

- \[ \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} = 2, \quad x \neq -1,\; x \neq 1. \] + \[ + \begin{aligned} + \frac{(x-1)(x+1)}{x+1} \times \frac{2}{x-1} + &= 2, \\ + x &\neq -1,\; x \neq 1 + \end{aligned} + \]

Harder examples

diff --git a/content/math/algebraic-fractions-formulas/supplement.html b/content/math/algebraic-fractions-formulas/supplement.html index ae76ad9..77aceee 100644 --- a/content/math/algebraic-fractions-formulas/supplement.html +++ b/content/math/algebraic-fractions-formulas/supplement.html @@ -90,8 +90,16 @@

Uniform motion

For constant speed, distance \(d\), speed \(v\), and time \(t\) are related by \(v = d/t\). If a - train travels \(240\) km in \(3\) h, then \(v = 240/3 = 80\) km/h. If you know \(v\) and \(t\) - but need distance, rearrange to \(d = vt\). + train travels \(240\) km in \(3\) h, then: +

+

\[ + \begin{aligned} + v &= \frac{240}{3} \\ + &= 80\ \text{km/h} + \end{aligned} + \]

+

+ If you know \(v\) and \(t\) but need distance, rearrange to \(d = vt\).

@@ -99,8 +107,16 @@

Density

Density \(\rho\) (rho) is mass per unit volume: \(\rho = m/V\). A block of mass \(2.7\) kg with - volume \(0.001\) m³ has \(\rho = 2.7/0.001 = 2700\) kg/m³. To find mass from \(\rho\) and \(V\), - use \(m = \rho V\). + volume \(0.001\) m³ has: +

+

\[ + \begin{aligned} + \rho &= \frac{2.7}{0.001} \\ + &= 2700\ \text{kg/m}^3 + \end{aligned} + \]

+

+ To find mass from \(\rho\) and \(V\), use \(m = \rho V\).

Pressure and electric circuits

@@ -225,7 +241,14 @@
Make \(t\) the subject of \(v = \dfrac{d}{t}\) and then find \(t\) when \(d = 150\) km and \(v = 75\) km/h.
-

Answer: \(t = d/v = 2\) h.

+

Answer:

+

\[ + \begin{aligned} + t &= \frac{d}{v} \\ + &= \frac{150}{75} \\ + &= 2\ \text{h} + \end{aligned} + \]

Steps: \(vt = d \Rightarrow t = d/v\); substitute values.

@@ -241,7 +264,20 @@
A formula is \(P = \dfrac{F}{A}\). The force is \(120\) N and the area is \(0.05\) m². Find \(P\), then make \(F\) the subject and verify your force value.
-

Answer: \(P = 120/0.05 = 2400\) Pa; \(F = PA = 2400 \times 0.05 = 120\) N.

+

Answer:

+

\[ + \begin{aligned} + P &= \frac{120}{0.05} \\ + &= 2400\ \text{Pa} + \end{aligned} + \]

+

\[ + \begin{aligned} + F &= PA \\ + &= 2400 \times 0.05 \\ + &= 120\ \text{N} + \end{aligned} + \]

Explanation: Rearranging checks that the formula is consistent.

diff --git a/content/math/counting-and-numbers/chapter.scss b/content/math/counting-and-numbers/chapter.scss index a3cc682..f37b7c4 100644 --- a/content/math/counting-and-numbers/chapter.scss +++ b/content/math/counting-and-numbers/chapter.scss @@ -235,6 +235,33 @@ margin-top: 12px; } +@media (max-width: 640px) { + .addition-groups__piles { + gap: 12px; + } + + .addition-groups__pile { + gap: 6px; + } + + .addition-groups__dots { + width: 96px; + min-height: 78px; + padding: 8px; + gap: 4px; + } + + .addition-groups__dot { + width: 10px; + height: 10px; + } + + .addition-groups__plus, + .addition-groups__equals { + font-size: 1.2rem; + } +} + .multiplication-array__grid { display: grid; gap: 6px; @@ -587,11 +614,6 @@ width: 100%; height: 100%; display: block; - cursor: grab; -} - -.big-numbers-scale__canvas:active { - cursor: grabbing; } .big-numbers-scale__status { @@ -619,6 +641,36 @@ gap: 10px; } +.big-numbers-scale__controls-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.1rem; + padding: 7px 9px; +} + +.big-numbers-scale__controls-dialog { + margin: auto; + padding: 0; + border: 0; + background: transparent; + color: inherit; +} + +.big-numbers-scale__controls-dialog__sheet { + width: 100%; +} + +.big-numbers-scale__controls-dialog__header { + display: none; +} + +.big-numbers-scale__controls-dialog__title { + margin: 0; + font-size: 0.9rem; + color: var(--soft-ink); +} + .big-numbers-scale__section { border: 1px solid var(--line); border-radius: 10px; @@ -710,4 +762,63 @@ .big-numbers-scale__hud { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .big-numbers-scale__controls-toggle { + display: none; + } + .big-numbers-scale__controls-dialog { + display: block; + position: static; + width: auto; + max-width: none; + min-width: 0; + margin: 0; + background: transparent; + border: 0; + } + .big-numbers-scale__controls-dialog__sheet { + padding: 0; + border: 0; + background: transparent; + box-shadow: none; + } +} + +@media (max-width: 959px) { + .big-numbers-scale__layout { + grid-template-columns: 1fr; + } + + .big-numbers-scale__controls-dialog { + width: min(92vw, 420px); + max-width: none; + margin: auto; + border: 0; + } + + .big-numbers-scale__controls-dialog__sheet { + padding: 18px 18px 14px; + opacity: 0; + transform: translateY(12px); + transition: opacity 200ms ease, transform 200ms ease; + } + + .big-numbers-scale__controls-dialog[open] .big-numbers-scale__controls-dialog__sheet { + opacity: 1; + transform: translateY(0); + } + + .big-numbers-scale__controls-dialog__header { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0 0 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--line); + } + + .big-numbers-scale__controls { + max-height: 70vh; + overflow: auto; + padding-right: 2px; + } } diff --git a/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx b/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx index 36570cf..a513584 100644 --- a/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx +++ b/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx @@ -27,6 +27,8 @@ function BigNumbersScale() { const soundToggle = byId("soundToggle"); const soundIcon = byId("soundIcon"); const soundText = byId("soundText"); + const controlsToggleBtn = byId("controlsToggleBtn"); + const controlsDialog = byId("controlsDialog"); const presetStepsContainer = byId("presetStepsContainer"); if ( @@ -49,6 +51,8 @@ function BigNumbersScale() { !soundToggle || !soundIcon || !soundText || + !controlsToggleBtn || + !controlsDialog || !presetStepsContainer ) { return; @@ -121,11 +125,6 @@ function BigNumbersScale() { currentTierIdx: 0, visualMode: "NORMAL" as VisualMode, transitionProgress: 0, - panX: 0, - panY: 0, - isDragging: false, - startX: 0, - startY: 0, particles: [] as Particle[], ambientStars: [] as CosmicStar[], shockwaves: [] as Shockwave[], @@ -555,8 +554,8 @@ function BigNumbersScale() { ctx.fillStyle = canvasBg; ctx.fillRect(0, 0, width, height); - const cx = width / 2 + state.panX; - const cy = height / 2 + state.panY; + const cx = width / 2; + const cy = height / 2; state.ambientStars.forEach((s) => s.draw(cx, cy, starColor)); @@ -566,8 +565,8 @@ function BigNumbersScale() { const gridSpacing = 40; const gridCols = Math.ceil(width / gridSpacing); const gridRows = Math.ceil(height / gridSpacing); - const startX = state.panX % gridSpacing; - const startY = state.panY % gridSpacing; + const startX = 0; + const startY = 0; for (let i = 0; i < gridCols; i += 1) { ctx.beginPath(); ctx.moveTo(startX + i * gridSpacing, 0); @@ -711,47 +710,33 @@ function BigNumbersScale() { } }); - const toLocal = (evtX: number, evtY: number) => { - const rect = canvas.getBoundingClientRect(); - return { x: evtX - rect.left, y: evtY - rect.top }; + const isMobileControlsMode = () => window.matchMedia("(max-width: 959px)").matches; + + const onControlsToggleClick = () => { + if (!isMobileControlsMode()) return; + if (!controlsDialog.open) { + controlsDialog.showModal(); + } }; - canvas.addEventListener("mousedown", (e) => { - state.isDragging = true; - const p = toLocal(e.clientX, e.clientY); - state.startX = p.x - state.panX; - state.startY = p.y - state.panY; - }); + const onControlsDialogClick = (event: MouseEvent) => { + if (event.target === controlsDialog) { + controlsDialog.close(); + return; + } + if (!isMobileControlsMode()) return; + const target = event.target; + if (!(target instanceof HTMLElement)) return; + const clickedButton = target.closest("button"); + if (!clickedButton) return; + if (clickedButton.id === "controlsToggleBtn") return; + requestAnimationFrame(() => { + if (controlsDialog.open) controlsDialog.close(); + }); + }; - canvas.addEventListener("mousemove", (e) => { - if (!state.isDragging) return; - const p = toLocal(e.clientX, e.clientY); - state.panX = p.x - state.startX; - state.panY = p.y - state.startY; - }); - - window.addEventListener("mouseup", () => { - state.isDragging = false; - }); - - canvas.addEventListener("touchstart", (e) => { - if (e.touches.length !== 1) return; - state.isDragging = true; - const p = toLocal(e.touches[0]!.clientX, e.touches[0]!.clientY); - state.startX = p.x - state.panX; - state.startY = p.y - state.panY; - }); - - canvas.addEventListener("touchmove", (e) => { - if (!state.isDragging || e.touches.length !== 1) return; - const p = toLocal(e.touches[0]!.clientX, e.touches[0]!.clientY); - state.panX = p.x - state.startX; - state.panY = p.y - state.startY; - }); - - canvas.addEventListener("touchend", () => { - state.isDragging = false; - }); + controlsToggleBtn.addEventListener("click", onControlsToggleClick); + controlsDialog.addEventListener("click", onControlsDialogClick); let rafId = 0; let lastTime = performance.now(); @@ -786,6 +771,8 @@ function BigNumbersScale() { onCleanup(() => { cancelAnimationFrame(rafId); window.removeEventListener("resize", resizeCanvas); + controlsToggleBtn.removeEventListener("click", onControlsToggleClick); + controlsDialog.removeEventListener("click", onControlsDialogClick); }); }); @@ -801,6 +788,16 @@ function BigNumbersScale() { 🔇 Muted + Speed: Normal @@ -840,49 +837,64 @@ function BigNumbersScale() { -
-
-

Mathematical milestones

-
-
-
-

Motion control

-
- - -
-
- - - + +
+
+

Mathematical milestones

+
+
+
+

Motion control

+
+ + + +
+
+ + + +
+
+
+

Time to count individually

+

+ Counting to 1 at one number per second takes: +

+
+ 1 Second +
+

+ "A single heartbeat. The primary unit of all structures." +

+
-
-

Time to count individually

-

- Counting to 1 at one number per second takes: -

-
- 1 Second -
-

- "A single heartbeat. The primary unit of all structures." -

-
-
+
); diff --git a/frontend/src/lib/katex.ts b/frontend/src/lib/katex.ts index 474227b..d98efce 100644 --- a/frontend/src/lib/katex.ts +++ b/frontend/src/lib/katex.ts @@ -102,14 +102,45 @@ export function observeMathAutoRender(root: ParentNode = document) { let scheduled = false; let rendering = false; + const pendingTargets = new Set(); + + const queueTargetFromNode = (node: Node) => { + if (nodeInsideKatex(node)) return; + + if (node instanceof HTMLElement) { + if (node.matches(AUTO_RENDER_TARGETS)) { + pendingTargets.add(node); + } + node.querySelectorAll(AUTO_RENDER_TARGETS).forEach((el) => { + pendingTargets.add(el); + }); + return; + } + + if (node instanceof DocumentFragment) { + node.querySelectorAll(AUTO_RENDER_TARGETS).forEach((el) => { + pendingTargets.add(el); + }); + return; + } + + const parent = node.parentElement; + if (!parent || nodeInsideKatex(parent)) return; + const host = parent.closest(AUTO_RENDER_TARGETS); + if (host) pendingTargets.add(host); + }; const scheduleRender = () => { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { scheduled = false; + if (pendingTargets.size === 0) return; + rendering = true; - renderBookMath(root, { force: true }); + const targets = Array.from(pendingTargets); + pendingTargets.clear(); + targets.forEach((el) => renderMathInContainer(el)); rendering = false; }); }; @@ -118,11 +149,19 @@ export function observeMathAutoRender(root: ParentNode = document) { if (rendering) return; const hasExternalChange = mutations.some((mutation) => { if (nodeInsideKatex(mutation.target)) return false; - if (mutation.type === "characterData") return true; - if (mutation.addedNodes.length === 0 && mutation.removedNodes.length === 0) return false; - const addedInsideKatex = Array.from(mutation.addedNodes).every(nodeInsideKatex); - const removedInsideKatex = Array.from(mutation.removedNodes).every(nodeInsideKatex); - return !(addedInsideKatex && removedInsideKatex); + + if (mutation.type === "characterData") { + const parent = mutation.target.parentElement; + if (!parent || nodeInsideKatex(parent)) return false; + const host = parent.closest(AUTO_RENDER_TARGETS); + if (!host) return false; + delete host.dataset.katexRendered; + pendingTargets.add(host); + return true; + } + + mutation.addedNodes.forEach((node) => queueTargetFromNode(node)); + return mutation.addedNodes.length > 0; }); if (hasExternalChange) scheduleRender(); }); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ecd67b9..6798395 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -66,7 +66,7 @@ let stopMathObserver: (() => void) | null = null; function scheduleBookMath() { void import("./lib/katex").then(({ observeMathAutoRender, renderBookMath }) => { requestAnimationFrame(() => { - renderBookMath(document, { force: true }); + renderBookMath(document); if (!stopMathObserver) { stopMathObserver = observeMathAutoRender(document); }