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.

This commit is contained in:
Hananoshika Yomaru 2026-05-22 00:32:28 -07:00
parent 30b4f4cf53
commit 61963350e7
9 changed files with 231 additions and 44 deletions

View file

@ -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 `<dialog>` 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:

View file

@ -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 <strong>remainder</strong>.
</p>
<p>
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:
</p>
<p>
\[
\begin{aligned}
157 \div 12 &= 13 \text{ remainder } 1 \\
157 &= 12 \times 13 + 1
\end{aligned}
\]
</p>
<p>
The remainder can also be written as a fraction of the divisor:
\[
\begin{aligned}
157 \div 12 &= 13 + \frac{1}{12}
\end{aligned}
\]
</p>
<h2 id="terminology" class="book-prose__heading">Names of the parts</h2>
<div class="book-table-wrap">
@ -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.
</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<div class="book-callout" role="note">
<p class="book-callout__label">Checkpoint</p>

View file

@ -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;

View file

@ -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 <strong>remainder</strong>.
</p>
<p>
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:
</p>
<p>
\[
\begin{aligned}
157 \div 12 &= 13 \text{ remainder } 1 \\
157 &= 12 \times 13 + 1
\end{aligned}
\]
</p>
<p>
The remainder can also be written as a fraction of the divisor:
\[
\begin{aligned}
157 \div 12 &= 13 + \frac{1}{12}
\end{aligned}
\]
</p>
<h2 id="terminology" class="book-prose__heading">Names of the parts</h2>
<div class="book-table-wrap">

View file

@ -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.
</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<div class="book-callout" role="note">
<p class="book-callout__label">Checkpoint</p>

View file

@ -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() {

View file

@ -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() {
</h2>
<div class="va-workbench__shell">
<aside class="va-workbench__sidebar">
<button
type="button"
class="va-workbench__mobile-toggle"
aria-expanded={controlsOpen()}
aria-controls="va-workbench-controls"
onClick={() => setControlsOpen((open) => !open)}
>
{controlsOpen() ? "Hide controls" : "Show controls"}
</button>
<aside
id="va-workbench-controls"
class="va-workbench__sidebar"
classList={{ "va-workbench__sidebar--open": controlsOpen() }}
>
<div class="va-workbench__brand">
<span class="va-workbench__brand-icon" aria-hidden="true">
±
@ -309,22 +324,21 @@ function VerticalArithmeticWorkbench() {
</aside>
<div class="va-workbench__stage">
<div
class="va-workbench__message-box"
classList={{ "va-workbench__message-box--error": isError() }}
>
<p class="va-workbench__message" aria-live="polite">
{message()}
</p>
<div class="va-workbench__message-box" classList={{ "va-workbench__message-box--error": isError() }}>
<div class="va-workbench__message-head">
<p class="va-workbench__message" aria-live="polite">
{message()}
</p>
<p class="va-workbench__step-counter">
Step {stepCurrent()} / {stepTotal()}
</p>
</div>
<div class="va-workbench__progress-track">
<div
class="va-workbench__progress-fill"
style={{ width: `${progress()}%` }}
/>
</div>
<p class="va-workbench__step-counter">
Step {stepCurrent()} / {stepTotal()}
</p>
</div>
<div ref={canvasWrapRef} class="va-workbench__canvas-wrap">

View file

@ -4,7 +4,14 @@
<title>Open Free Books</title>
<link>https://openfreebooks.org/</link>
<description>Recent updates to Open Free Books pages and live chapters.</description>
<lastBuildDate>Fri, 22 May 2026 06:25:20 GMT</lastBuildDate>
<lastBuildDate>Fri, 22 May 2026 06:55:33 GMT</lastBuildDate>
<item>
<title>Pythagoras&apos; theorem</title>
<link>https://openfreebooks.org/math/pythagoras-theorem/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/pythagoras-theorem/</guid>
<pubDate>Fri, 22 May 2026 06:55:33 GMT</pubDate>
<description>Use Pythagoras&apos; theorem and its converse in 2D problems and coordinate settings.</description>
</item>
<item>
<title>Algebraic fractions &amp; formulas</title>
<link>https://openfreebooks.org/math/algebraic-fractions-formulas/</link>
@ -26,13 +33,6 @@
<pubDate>Fri, 22 May 2026 05:57:59 GMT</pubDate>
<description>0 is nothing; 1 is one. Learn to tell them apart — the same skill as telling false from true — before counting larger amounts.</description>
</item>
<item>
<title>Pythagoras&apos; theorem</title>
<link>https://openfreebooks.org/math/pythagoras-theorem/</link>
<guid isPermaLink="true">https://openfreebooks.org/math/pythagoras-theorem/</guid>
<pubDate>Fri, 22 May 2026 05:22:44 GMT</pubDate>
<description>Use Pythagoras&apos; theorem and its converse in 2D problems and coordinate settings.</description>
</item>
<item>
<title>Coordinate geometry foundations</title>
<link>https://openfreebooks.org/math/coordinate-geometry-foundations/</link>

View file

@ -142,7 +142,7 @@
</url>
<url>
<loc>https://openfreebooks.org/math/pythagoras-theorem/</loc>
<lastmod>2026-05-22T05:22:44.000Z</lastmod>
<lastmod>2026-05-22T06:55:33.000Z</lastmod>
</url>
<url>
<loc>https://openfreebooks.org/math/quadratic-equations/</loc>