mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
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:
parent
30b4f4cf53
commit
61963350e7
9 changed files with 231 additions and 44 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() }}
|
||||
>
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -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' 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' theorem and its converse in 2D problems and coordinate settings.</description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Algebraic fractions & 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' 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' 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue