From 61963350e760ec7e9b2a310fb5e6f0f872dd13b2 Mon Sep 17 00:00:00 2001 From: Hananoshika Yomaru Date: Fri, 22 May 2026 00:32:28 -0700 Subject: [PATCH] Enhance math content and widget responsiveness: Add detailed explanations for calculating remainders in written calculation chapters, including examples in _index.md and core.html. Update SKILL.md to include mobile-first demo responsiveness guidelines for all widgets. Revise styles in chapter.scss to improve mobile layout and introduce a mobile toggle for the vertical arithmetic workbench. Update feed.xml and sitemap.xml to reflect recent content additions and modifications. --- .agents/skills/ofb/SKILL.md | 6 ++ content/math/written-calculation/_index.md | 22 ++++- content/math/written-calculation/chapter.scss | 99 +++++++++++++++---- content/math/written-calculation/core.html | 21 ++++ .../math/written-calculation/supplement.html | 1 - .../widgets/lib/math-visualizer.ts | 72 +++++++++++++- .../widgets/vertical-arithmetic-workbench.tsx | 36 ++++--- static/feed.xml | 16 +-- static/sitemap.xml | 2 +- 9 files changed, 231 insertions(+), 44 deletions(-) diff --git a/.agents/skills/ofb/SKILL.md b/.agents/skills/ofb/SKILL.md index 45ab749..e5eb874 100644 --- a/.agents/skills/ofb/SKILL.md +++ b/.agents/skills/ofb/SKILL.md @@ -52,6 +52,7 @@ bun run build # production: JS + Zola + pagefind → public/ | Graph | `graph.edges` must be **acyclic**; builder throws on cycles | | Chart axes (all widgets) | Every coordinate chart must render **x/y axis labels, positive-direction arrowheads, and visible graduations (ticks + scale labels)**. Apply this consistently across all subjects/widgets. | | Accessibility | Avoid decorative characters that may render as emoji on mobile. Prefer broadly supported symbols/text so desktop and mobile stay visually consistent. | +| Demo responsiveness (all widgets) | Mobile-first: keep demo viewport short, reserve stage/canvas height up front to avoid CLS, and never let status/controls overlap or block the interactive area. Use collapsible/toggle controls on small screens when needed. | ## Common tasks → where to edit @@ -139,6 +140,11 @@ Live chapters live under `content/{subject}/{slug}/`: **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. +**Widget layout standards (all subjects):** +- Keep the primary demo stage visible without long pre-scroll on mobile. +- Reserve stage space from first render (`min-height`/fixed aspect) so hydration or assets do not shift nearby content. +- Keep feedback UI (step badges, status, toolbars) outside the canvas interaction area, or anchored so they never occlude controls/content. + ### Interactive demo technology choices Interactive demos can use different technologies based on the learning goal and interaction type: diff --git a/content/math/written-calculation/_index.md b/content/math/written-calculation/_index.md index 010bb60..e3e99c6 100644 --- a/content/math/written-calculation/_index.md +++ b/content/math/written-calculation/_index.md @@ -117,6 +117,27 @@ strand = "Number & Algebra" dividend? Write that digit in the quotient, multiply, subtract, then bring down the next digit. If the division is not exact, the amount left is the remainder.

+

+ To calculate and check a remainder, always use + \( \text{dividend} = \text{divisor} \times \text{quotient} + \text{remainder} \), with + \(0 \le \text{remainder} < \text{divisor}\). For example: +

+

+ \[ + \begin{aligned} + 157 \div 12 &= 13 \text{ remainder } 1 \\ + 157 &= 12 \times 13 + 1 + \end{aligned} + \] +

+

+ The remainder can also be written as a fraction of the divisor: + \[ + \begin{aligned} + 157 \div 12 &= 13 + \frac{1}{12} + \end{aligned} + \] +

Names of the parts

@@ -190,7 +211,6 @@ strand = "Number & Algebra" Subtraction is the inverse of addition: \(c = a - b\) means the same as \(c + b = a\). Borrowing moves one ten from the left so the subtraction in the current column matches that story.

-

Checkpoints

Checkpoint

diff --git a/content/math/written-calculation/chapter.scss b/content/math/written-calculation/chapter.scss index 56a974a..53d6786 100644 --- a/content/math/written-calculation/chapter.scss +++ b/content/math/written-calculation/chapter.scss @@ -39,7 +39,7 @@ .va-workbench__shell { display: flex; flex-direction: column; - min-height: 28rem; + min-height: 24rem; border: 1px solid var(--va-panel-border); border-radius: 14px; overflow: hidden; @@ -54,6 +54,20 @@ } } +.va-workbench__mobile-toggle { + display: none; + width: 100%; + font: inherit; + font-weight: 600; + font-size: 0.9rem; + padding: 0.75rem 1rem; + border: 0; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--paper) 50%, var(--card-bg)); + color: var(--ink); + cursor: pointer; +} + .va-workbench__sidebar { flex-shrink: 0; display: flex; @@ -294,37 +308,38 @@ flex: 1; display: flex; flex-direction: column; - min-height: 16rem; + min-height: 18rem; background: var(--va-stage-bg); position: relative; } .va-workbench__message-box { - position: absolute; - top: 1rem; - left: 50%; - transform: translateX(-50%); - z-index: 2; - width: min(92%, 32rem); - padding: 0.85rem 1.1rem; - text-align: center; - border: 1px solid var(--line); - border-radius: 0.75rem; - background: color-mix(in srgb, var(--card-bg) 92%, transparent); - backdrop-filter: blur(8px); - box-shadow: 0 8px 24px var(--shadow-soft); + position: relative; + z-index: 1; + width: 100%; + padding: 0.75rem 0.9rem; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--paper) 80%, var(--card-bg)); } .va-workbench__message-box--error .va-workbench__message { color: var(--va-error); } +.va-workbench__message-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; +} + .va-workbench__message { - font-size: 1rem; + font-size: 0.95rem; font-weight: 600; color: var(--ink); - margin: 0 0 0.5rem; + margin: 0; line-height: 1.4; + text-align: left; } .va-workbench__progress-track { @@ -342,9 +357,10 @@ } .va-workbench__step-counter { - font-size: 0.75rem; + font-size: 0.72rem; color: var(--muted); - margin: 0.35rem 0 0; + margin: 0; + white-space: nowrap; } .va-workbench__canvas-wrap { @@ -356,6 +372,51 @@ background-size: 20px 20px; } +@media (max-width: 767px) { + .va-workbench { + width: 100%; + margin-top: 1rem; + } + + .va-workbench__mobile-toggle { + display: block; + } + + .va-workbench__sidebar { + gap: 0; + max-height: 0; + padding: 0 1rem; + border-bottom: 0; + overflow: hidden; + transition: max-height 0.24s ease; + } + + .va-workbench__sidebar--open { + gap: 0.85rem; + max-height: 42rem; + padding: 1rem; + border-bottom: 1px solid var(--line); + overflow-y: auto; + } + + .va-workbench__stage { + min-height: 14rem; + } + + .va-workbench__message-head { + flex-wrap: wrap; + row-gap: 0.2rem; + } + + .va-workbench__message { + font-size: 0.88rem; + } + + .va-workbench__canvas-wrap { + min-height: 12rem; + } +} + .va-workbench__canvas { position: absolute; inset: 0; diff --git a/content/math/written-calculation/core.html b/content/math/written-calculation/core.html index 55f0704..d144554 100644 --- a/content/math/written-calculation/core.html +++ b/content/math/written-calculation/core.html @@ -106,6 +106,27 @@ dividend? Write that digit in the quotient, multiply, subtract, then bring down the next digit. If the division is not exact, the amount left is the remainder.

+

+ To calculate and check a remainder, always use + \( \text{dividend} = \text{divisor} \times \text{quotient} + \text{remainder} \), with + \(0 \le \text{remainder} < \text{divisor}\). For example: +

+

+ \[ + \begin{aligned} + 157 \div 12 &= 13 \text{ remainder } 1 \\ + 157 &= 12 \times 13 + 1 + \end{aligned} + \] +

+

+ The remainder can also be written as a fraction of the divisor: + \[ + \begin{aligned} + 157 \div 12 &= 13 + \frac{1}{12} + \end{aligned} + \] +

Names of the parts

diff --git a/content/math/written-calculation/supplement.html b/content/math/written-calculation/supplement.html index 96656ba..4af66d6 100644 --- a/content/math/written-calculation/supplement.html +++ b/content/math/written-calculation/supplement.html @@ -26,7 +26,6 @@ Subtraction is the inverse of addition: \(c = a - b\) means the same as \(c + b = a\). Borrowing moves one ten from the left so the subtraction in the current column matches that story.

-

Checkpoints

Checkpoint

diff --git a/content/math/written-calculation/widgets/lib/math-visualizer.ts b/content/math/written-calculation/widgets/lib/math-visualizer.ts index fe40572..6e7854e 100644 --- a/content/math/written-calculation/widgets/lib/math-visualizer.ts +++ b/content/math/written-calculation/widgets/lib/math-visualizer.ts @@ -129,6 +129,9 @@ export class MathVisualizer { this.generateAddition(lines); } else { let current = lines[0]; + const initialValue = BigInt(current); + const divisionDivisors: bigint[] = []; + const divisionRemainders: bigint[] = []; const finalStepFrames: Frame[] = []; for (let i = 1; i < lines.length; i++) { @@ -159,14 +162,29 @@ export class MathVisualizer { this.generateMultiplication(current, next); current = (BigInt(current) * BigInt(next)).toString(); } else if (this.operation === "/") { - if (BigInt(next) === 0n) throw new Error("Cannot divide by zero."); + const nextDivisor = BigInt(next); + if (nextDivisor === 0n) throw new Error("Cannot divide by zero."); + const currentValue = BigInt(current); + divisionDivisors.push(nextDivisor); + divisionRemainders.push(currentValue % nextDivisor); this.generateDivision(current, next); - current = (BigInt(current) / BigInt(next)).toString(); + current = (currentValue / nextDivisor).toString(); } finalStepFrames.push(this.cloneGrid(this.frames[this.frames.length - 1])); } + if (this.operation === "/" && divisionDivisors.length > 0 && this.frames.length > 0) { + const resultFrame = this.cloneGrid(this.frames[this.frames.length - 1]); + resultFrame.message = this.buildDivisionResultMessage( + initialValue, + BigInt(current), + divisionDivisors, + divisionRemainders + ); + this.frames.push(resultFrame); + } + if (finalStepFrames.length > 1) { this.frames.push(this.mergeFramesToSummary(finalStepFrames)); } @@ -202,6 +220,49 @@ export class MathVisualizer { return frame.cells[`${x},${y}`] ?? null; } + private rationalToDecimalString(numerator: bigint, denominator: bigint, maxDigits = 9) { + const integerPart = numerator / denominator; + let remainder = numerator % denominator; + if (remainder === 0n) return integerPart.toString(); + + let fractional = ""; + for (let i = 0; i < maxDigits && remainder !== 0n; i++) { + remainder *= 10n; + fractional += (remainder / denominator).toString(); + remainder %= denominator; + } + if (remainder !== 0n) fractional += "..."; + return `${integerPart.toString()}.${fractional}`; + } + + private buildDivisionResultMessage( + originalDividend: bigint, + finalQuotient: bigint, + divisors: bigint[], + remainders: bigint[] + ) { + const totalDivisor = divisors.reduce((acc, d) => acc * d, 1n); + const decimalValue = this.rationalToDecimalString(originalDividend, totalDivisor); + + if (remainders.every((r) => r === 0n)) { + return `Result: ${finalQuotient.toString()} (exact division).`; + } + + const terms: string[] = []; + for (let i = remainders.length - 1; i >= 0; i--) { + const remainder = remainders[i]; + const denomFactors = divisors + .slice(i) + .map((d) => d.toString()) + .reverse(); + const denominatorText = + denomFactors.length === 1 ? denomFactors[0] : denomFactors.join(" x "); + terms.push(`${remainder.toString()}/${denominatorText}`); + } + + return `Result: ${finalQuotient.toString()} + ${terms.join(" + ")} = ${decimalValue}`; + } + private mergeFramesToSummary(finalFrames: Frame[]): Frame { const summaryFrame = this.createEmptyFrame(); summaryFrame.message = "Final Summary of All Steps"; @@ -661,7 +722,7 @@ export class MathVisualizer { qFrame.message = `${div} goes into ${currentVal} exactly ${qDigit} times.`; this.frames.push(qFrame); - if (qDigit > 0 || i === dividendArr.length - 1) { + if (qDigit > 0) { const subVal = qDigit * div; const subStr = subVal.toString(); const subFrame = this.cloneGrid(this.frames[this.frames.length - 1]); @@ -695,9 +756,14 @@ export class MathVisualizer { } resFrame.message = `Remainder is ${currentVal}.`; this.frames.push(resFrame); + } else if (i === dividendArr.length - 1) { + const zeroFrame = this.cloneGrid(this.frames[this.frames.length - 1]); + zeroFrame.message = `${div} does not fit into ${currentVal}. Quotient digit is 0; remainder stays ${currentVal}.`; + this.frames.push(zeroFrame); } } } + } render() { diff --git a/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx index a38a9e8..6162226 100644 --- a/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx +++ b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx @@ -96,6 +96,7 @@ function VerticalArithmeticWorkbench() { const [playing, setPlaying] = createSignal(false); const [speed, setSpeed] = createSignal(800); const [soundMuted, setSoundMuted] = createSignal(true); + const [controlsOpen, setControlsOpen] = createSignal(false); const sound = new WorkbenchSound(); let lastStep = 0; @@ -178,7 +179,21 @@ function VerticalArithmeticWorkbench() {
-