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 | | 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. | | 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. | | 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 ## 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. **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 demo technology choices
Interactive demos can use different technologies based on the learning goal and interaction type: 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 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>. the division is not exact, the amount left is the <strong>remainder</strong>.
</p> </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> <h2 id="terminology" class="book-prose__heading">Names of the parts</h2>
<div class="book-table-wrap"> <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 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. moves one ten from the left so the subtraction in the current column matches that story.
</p> </p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2> <h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<div class="book-callout" role="note"> <div class="book-callout" role="note">
<p class="book-callout__label">Checkpoint</p> <p class="book-callout__label">Checkpoint</p>

View file

@ -39,7 +39,7 @@
.va-workbench__shell { .va-workbench__shell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 28rem; min-height: 24rem;
border: 1px solid var(--va-panel-border); border: 1px solid var(--va-panel-border);
border-radius: 14px; border-radius: 14px;
overflow: hidden; 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 { .va-workbench__sidebar {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
@ -294,37 +308,38 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 16rem; min-height: 18rem;
background: var(--va-stage-bg); background: var(--va-stage-bg);
position: relative; position: relative;
} }
.va-workbench__message-box { .va-workbench__message-box {
position: absolute; position: relative;
top: 1rem; z-index: 1;
left: 50%; width: 100%;
transform: translateX(-50%); padding: 0.75rem 0.9rem;
z-index: 2; border-bottom: 1px solid var(--line);
width: min(92%, 32rem); background: color-mix(in srgb, var(--paper) 80%, var(--card-bg));
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);
} }
.va-workbench__message-box--error .va-workbench__message { .va-workbench__message-box--error .va-workbench__message {
color: var(--va-error); color: var(--va-error);
} }
.va-workbench__message-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
}
.va-workbench__message { .va-workbench__message {
font-size: 1rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
color: var(--ink); color: var(--ink);
margin: 0 0 0.5rem; margin: 0;
line-height: 1.4; line-height: 1.4;
text-align: left;
} }
.va-workbench__progress-track { .va-workbench__progress-track {
@ -342,9 +357,10 @@
} }
.va-workbench__step-counter { .va-workbench__step-counter {
font-size: 0.75rem; font-size: 0.72rem;
color: var(--muted); color: var(--muted);
margin: 0.35rem 0 0; margin: 0;
white-space: nowrap;
} }
.va-workbench__canvas-wrap { .va-workbench__canvas-wrap {
@ -356,6 +372,51 @@
background-size: 20px 20px; 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 { .va-workbench__canvas {
position: absolute; position: absolute;
inset: 0; inset: 0;

View file

@ -106,6 +106,27 @@
dividend? Write that digit in the quotient, multiply, subtract, then bring down the next digit. If 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>. the division is not exact, the amount left is the <strong>remainder</strong>.
</p> </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> <h2 id="terminology" class="book-prose__heading">Names of the parts</h2>
<div class="book-table-wrap"> <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 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. moves one ten from the left so the subtraction in the current column matches that story.
</p> </p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2> <h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<div class="book-callout" role="note"> <div class="book-callout" role="note">
<p class="book-callout__label">Checkpoint</p> <p class="book-callout__label">Checkpoint</p>

View file

@ -129,6 +129,9 @@ export class MathVisualizer {
this.generateAddition(lines); this.generateAddition(lines);
} else { } else {
let current = lines[0]; let current = lines[0];
const initialValue = BigInt(current);
const divisionDivisors: bigint[] = [];
const divisionRemainders: bigint[] = [];
const finalStepFrames: Frame[] = []; const finalStepFrames: Frame[] = [];
for (let i = 1; i < lines.length; i++) { for (let i = 1; i < lines.length; i++) {
@ -159,14 +162,29 @@ export class MathVisualizer {
this.generateMultiplication(current, next); this.generateMultiplication(current, next);
current = (BigInt(current) * BigInt(next)).toString(); current = (BigInt(current) * BigInt(next)).toString();
} else if (this.operation === "/") { } 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); this.generateDivision(current, next);
current = (BigInt(current) / BigInt(next)).toString(); current = (currentValue / nextDivisor).toString();
} }
finalStepFrames.push(this.cloneGrid(this.frames[this.frames.length - 1])); 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) { if (finalStepFrames.length > 1) {
this.frames.push(this.mergeFramesToSummary(finalStepFrames)); this.frames.push(this.mergeFramesToSummary(finalStepFrames));
} }
@ -202,6 +220,49 @@ export class MathVisualizer {
return frame.cells[`${x},${y}`] ?? null; 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 { private mergeFramesToSummary(finalFrames: Frame[]): Frame {
const summaryFrame = this.createEmptyFrame(); const summaryFrame = this.createEmptyFrame();
summaryFrame.message = "Final Summary of All Steps"; summaryFrame.message = "Final Summary of All Steps";
@ -661,7 +722,7 @@ export class MathVisualizer {
qFrame.message = `${div} goes into ${currentVal} exactly ${qDigit} times.`; qFrame.message = `${div} goes into ${currentVal} exactly ${qDigit} times.`;
this.frames.push(qFrame); this.frames.push(qFrame);
if (qDigit > 0 || i === dividendArr.length - 1) { if (qDigit > 0) {
const subVal = qDigit * div; const subVal = qDigit * div;
const subStr = subVal.toString(); const subStr = subVal.toString();
const subFrame = this.cloneGrid(this.frames[this.frames.length - 1]); const subFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
@ -695,9 +756,14 @@ export class MathVisualizer {
} }
resFrame.message = `Remainder is ${currentVal}.`; resFrame.message = `Remainder is ${currentVal}.`;
this.frames.push(resFrame); 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() { render() {

View file

@ -96,6 +96,7 @@ function VerticalArithmeticWorkbench() {
const [playing, setPlaying] = createSignal(false); const [playing, setPlaying] = createSignal(false);
const [speed, setSpeed] = createSignal(800); const [speed, setSpeed] = createSignal(800);
const [soundMuted, setSoundMuted] = createSignal(true); const [soundMuted, setSoundMuted] = createSignal(true);
const [controlsOpen, setControlsOpen] = createSignal(false);
const sound = new WorkbenchSound(); const sound = new WorkbenchSound();
let lastStep = 0; let lastStep = 0;
@ -178,7 +179,21 @@ function VerticalArithmeticWorkbench() {
</h2> </h2>
<div class="va-workbench__shell"> <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"> <div class="va-workbench__brand">
<span class="va-workbench__brand-icon" aria-hidden="true"> <span class="va-workbench__brand-icon" aria-hidden="true">
± ±
@ -309,22 +324,21 @@ function VerticalArithmeticWorkbench() {
</aside> </aside>
<div class="va-workbench__stage"> <div class="va-workbench__stage">
<div <div class="va-workbench__message-box" classList={{ "va-workbench__message-box--error": isError() }}>
class="va-workbench__message-box" <div class="va-workbench__message-head">
classList={{ "va-workbench__message-box--error": isError() }} <p class="va-workbench__message" aria-live="polite">
> {message()}
<p class="va-workbench__message" aria-live="polite"> </p>
{message()} <p class="va-workbench__step-counter">
</p> Step {stepCurrent()} / {stepTotal()}
</p>
</div>
<div class="va-workbench__progress-track"> <div class="va-workbench__progress-track">
<div <div
class="va-workbench__progress-fill" class="va-workbench__progress-fill"
style={{ width: `${progress()}%` }} style={{ width: `${progress()}%` }}
/> />
</div> </div>
<p class="va-workbench__step-counter">
Step {stepCurrent()} / {stepTotal()}
</p>
</div> </div>
<div ref={canvasWrapRef} class="va-workbench__canvas-wrap"> <div ref={canvasWrapRef} class="va-workbench__canvas-wrap">

View file

@ -4,7 +4,14 @@
<title>Open Free Books</title> <title>Open Free Books</title>
<link>https://openfreebooks.org/</link> <link>https://openfreebooks.org/</link>
<description>Recent updates to Open Free Books pages and live chapters.</description> <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> <item>
<title>Algebraic fractions &amp; formulas</title> <title>Algebraic fractions &amp; formulas</title>
<link>https://openfreebooks.org/math/algebraic-fractions-formulas/</link> <link>https://openfreebooks.org/math/algebraic-fractions-formulas/</link>
@ -26,13 +33,6 @@
<pubDate>Fri, 22 May 2026 05:57:59 GMT</pubDate> <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> <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>
<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> <item>
<title>Coordinate geometry foundations</title> <title>Coordinate geometry foundations</title>
<link>https://openfreebooks.org/math/coordinate-geometry-foundations/</link> <link>https://openfreebooks.org/math/coordinate-geometry-foundations/</link>

View file

@ -142,7 +142,7 @@
</url> </url>
<url> <url>
<loc>https://openfreebooks.org/math/pythagoras-theorem/</loc> <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>
<url> <url>
<loc>https://openfreebooks.org/math/quadratic-equations/</loc> <loc>https://openfreebooks.org/math/quadratic-equations/</loc>