Refactor math chapter structure and styling: Update .gitignore to exclude specific CSS directories, enhance CONTRIBUTING.md with new guidelines for subject-specific assets, and streamline chapter content organization. Modify curriculum data to include new chapter dependencies and improve frontend logic for loading shared styles and scripts. Adjust styles for better visual consistency across math chapters.

This commit is contained in:
Hananoshika Yomaru 2026-05-20 18:07:39 -07:00
parent 5175811579
commit a2679aa4de
24 changed files with 2104 additions and 530 deletions

View file

@ -110,10 +110,15 @@ Prerequisites: **only** `graph.edges` (required, global DAG, cross-strand OK). L
Live chapters live under `content/{subject}/{slug}/`: Live chapters live under `content/{subject}/{slug}/`:
1. `data/{subject}-curriculum.json` — catalog metadata (`status`, `curriculums`, `graph.edges`) 1. `data/{subject}-curriculum.json` — catalog metadata (`status`, `curriculums`, `graph.edges`)
2. `_index.md` — front matter only (`template = "chapter.html"`, `[extra] subject`, `chapter_id`, `strand`) 2. **Subject shared** (when a subject has multiple chapters): `content/{subject}/subject.scss` → `/css/subjects/{subject}.css`; `content/{subject}/subject.ts` → `initSubject()` via `frontend/src/generated/subject-modules.ts`. Math HTML conventions: [math-chapter-patterns.md](math-chapter-patterns.md).
3. `core.html` (required), optional `supplement.html`, `assets/`, `widgets/*.tsx`, optional `chapter.scss` 3. `_index.md` — front matter only (`template = "chapter.html"`, `[extra] subject`, `chapter_id`, `strand`)
4. `bun run sync:chapters` merges HTML into `_index.md`, copies assets to `static/chapters/…`, generates `frontend/src/generated/chapter-widgets.ts` 4. `core.html` (required), optional `supplement.html`, `assets/`, `widgets/*.tsx`, optional `chapter.scss` (chapter-only styles — do not duplicate subject.scss)
5. `templates/chapter.html` renders `{{ section.content | safe }}`; widgets use `data-widget` + auto-mount in `main.tsx` 5. `bun run sync:chapters` merges HTML into `_index.md`, copies assets to `static/chapters/…`, compiles subject + chapter CSS, generates `chapter-widgets.ts` and `subject-modules.ts`, writes `data/_generated/subjects-meta.json`
6. `templates/chapter.html` loads subject CSS then chapter CSS; `main.tsx` runs subject init then mounts widgets
**Checkpoints (all subjects):** use `book-question`, `book-question__prompt`, `book-question__solution` (styled in theme `_book.scss`). Never use `book-question__answer`.
**Math checkpoints / dots:** follow [math-chapter-patterns.md](math-chapter-patterns.md); shared dot classes in `content/math/subject.scss`.
Reference: `content/math/quadratic-equations/`. Reference: `content/math/quadratic-equations/`.

View file

@ -0,0 +1,39 @@
# Mathematics chapter patterns
Use these conventions in `core.html` and `supplement.html` so every math chapter looks and behaves the same. Shared styles live in [`content/math/subject.scss`](../../content/math/subject.scss); shared setup in [`content/math/subject.ts`](../../content/math/subject.ts).
## Prose and sections
- Wrapper: `<div class="book-prose">`
- Section titles: `<h2 id="…" class="book-prose__heading">`
- In-chapter links: plain `<a href="/math/…/">` inside `.book-prose` (theme styles them)
## Checkpoints (required shape)
Copy from any live chapter supplement (e.g. `content/math/basic-math-notation/supplement.html`):
```html
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<details class="book-question">
<summary class="book-question__prompt">Your question here.</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> …</p>
</div>
</details>
```
Do **not** use `book-question__answer` — only `book-question__solution` is styled.
## Dot pictures
- Use `counting-dots`, `counting-dots__dot`, `counting-dots--empty`, `counting-dots__none` for amount pictures (see `counting-and-numbers`).
- Use `notation-dots` for smaller chains in notation chapters.
## Widgets
- Mount: `<div class="math-widget-mount" data-widget="widget-name" data-pagefind-ignore></div>`
- Widget file: `widgets/widget-name.tsx` with default export
## Chapter-only CSS
Put styles used in **one** chapter under `chapter.scss`. Move duplicated blocks into `subject.scss` instead.

View file

@ -72,6 +72,19 @@ Mounted from `catalog.html` via `catalog-app.tsx` (in main bundle). Map view laz
**Mermaid theming:** use `theme: "base"` and hex `themeVariables` only; CSS cannot override Mermaid’s inline cluster fills. See [zola-catalog.md](zola-catalog.md). **Mermaid theming:** use `theme: "base"` and hex `themeVariables` only; CSS cannot override Mermaid’s inline cluster fills. See [zola-catalog.md](zola-catalog.md).
## Subject shared assets (chapters)
For subjects with multiple chapters (e.g. mathematics), colocate shared code under `content/{subject}/`:
| File | Build output | Loaded by |
|------|----------------|-----------|
| `subject.scss` | `static/css/subjects/{subject}.css` | `chapter.html` when `subjects-meta.json` has `hasCss` |
| `subject.ts` | chunk via Vite | `main.tsx` → `generated/subject-modules.ts` → `initSubject(article)` |
Run `bun run sync:chapters` to compile CSS and regenerate `subject-modules.ts` + `data/_generated/subjects-meta.json`. Chapter-only styles stay in `content/{subject}/{slug}/chapter.scss`.
Math conventions: `.agents/skills/ofb/math-chapter-patterns.md`.
## KaTeX (math) ## KaTeX (math)
| Use case | API | Where | | Use case | API | Where |

2
.gitignore vendored
View file

@ -28,5 +28,5 @@ themes/openfreebooks/static/
# Chapter sync output (bun run sync:chapters) # Chapter sync output (bun run sync:chapters)
frontend/src/generated/ frontend/src/generated/
data/_generated/ data/_generated/
static/css/chapters/ static/css/
static/chapters/ static/chapters/

View file

@ -87,14 +87,17 @@ In `data/{subject}-curriculum.json`, set `"status": "live"` and include `descrip
Create `content/{subject}/{slug}/`: Create `content/{subject}/{slug}/`:
```text ```text
content/math/your-chapter-slug/ content/math/ # subject folder (mathematics today)
├── _index.md # page metadata (see below) ├── subject.scss # shared CSS for all math chapters → /css/subjects/math.css
├── core.html # required — main lesson HTML ├── subject.ts # shared JS init for all math chapters
├── supplement.html # optional — history, checkpoints, extra demos └── your-chapter-slug/
├── chapter.scss # optional — chapter-specific styles ├── _index.md # page metadata (see below)
├── assets/ # optional — images (synced to static/chapters/…) ├── core.html # required — main lesson HTML
└── widgets/ # optional — Solid widgets (auto-mounted) ├── supplement.html # optional — history, checkpoints, extra demos
└── my-widget.tsx # default export; use data-widget="my-widget" in HTML ├── chapter.scss # optional — styles for this chapter only
├── assets/ # optional — images (synced to static/chapters/…)
└── widgets/ # optional — Solid widgets (auto-mounted)
└── my-widget.tsx # default export; data-widget="my-widget" in HTML
``` ```
**`_index.md` front matter** (page fields only — catalog fields stay in JSON): **`_index.md` front matter** (page fields only — catalog fields stay in JSON):
@ -115,6 +118,8 @@ strand = "Number & Algebra"
### 3. HTML and widgets ### 3. HTML and widgets
- Use shared classes: `book-prose`, `book-prose__heading`, `book-callout`, etc. Copy from `content/math/quadratic-equations/`. - Use shared classes: `book-prose`, `book-prose__heading`, `book-callout`, etc. Copy from `content/math/quadratic-equations/`.
- **Checkpoints:** `<details class="book-question">` with `book-question__prompt` and `book-question__solution` (not `__answer`). See `.agents/skills/ofb/math-chapter-patterns.md`.
- Put styles reused across several chapters in `content/{subject}/subject.scss`, not in each `chapter.scss`.
- **Do not** mention DSE, IB, or other curriculum names in chapter HTML. - **Do not** mention DSE, IB, or other curriculum names in chapter HTML.
- Mount widgets with `<div class="math-widget-mount" data-widget="widget-name" data-pagefind-ignore></div>` (filename without `.tsx` = `data-widget` value). - Mount widgets with `<div class="math-widget-mount" data-widget="widget-name" data-pagefind-ignore></div>` (filename without `.tsx` = `data-widget` value).
- **Math (KaTeX):** write `\( … \)` and `\[ … \]` in HTML; prose is auto-rendered on chapter load. In widgets use `import { renderLatex } from "@ofb/katex"` — do not add a per-widget `renderLatex` helper. - **Math (KaTeX):** write `\( … \)` and `\[ … \]` in HTML; prose is auto-rendered on chapter load. In widgets use `import { renderLatex } from "@ofb/katex"` — do not add a per-widget `renderLatex` helper.
@ -131,7 +136,7 @@ zola serve
- `/math/your-chapter-slug/` — page renders. - `/math/your-chapter-slug/` — page renders.
- `bun run build` — full production build. - `bun run build` — full production build.
No theme edits, no `main.tsx` edits, no per-chapter template branches. New subjects with shared CSS/JS: add `content/{subject}/subject.scss` and/or `subject.ts`; `sync:chapters` wires them automatically. No per-chapter template branches.
--- ---

View file

@ -37,34 +37,7 @@
color: var(--muted); color: var(--muted);
} }
// Dot pictures for compare chains (same idea as counting chapter) // notation-dots: content/math/subject.scss
.notation-dots {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
vertical-align: middle;
}
.notation-dots__dot {
display: block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #0f766e;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
flex-shrink: 0;
}
.notation-dots--chain {
max-width: 88px;
justify-content: center;
}
.notation-dots--decimal .notation-dots__dot {
width: 14px;
height: 14px;
}
.notation-compare-chain { .notation-compare-chain {
margin: 1.25rem 0; margin: 1.25rem 0;

View file

@ -1,8 +1,8 @@
+++ +++
title = "Counting and numbers" title = "Counting and numbers"
description = "Count from zero, spot patterns, add, multiply, and divide; then meet place value and big numbers from hundreds to billions." description = "Count objects, spot patterns, add, multiply, and divide; then meet place value and big numbers from hundreds to billions."
template = "chapter.html" template = "chapter.html"
weight = 0 weight = 1
[extra] [extra]
subject = "math" subject = "math"
chapter_id = "counting-and-numbers" chapter_id = "counting-and-numbers"
@ -11,10 +11,11 @@ strand = "Number & Algebra"
<div class="book-prose"> <div class="book-prose">
<p> <p>
Look around you. How many birds? How many cups? You can answer by You already met <strong>0</strong> and <strong>1</strong> in
<strong>counting</strong> — pointing at one thing at a time until everything has been pointed at. <a href="/math/zero-and-one/">Zero and one</a>. Now look around: how many birds? how many cups?
This chapter uses <strong>pictures of dots</strong> to stand for real things, like marbles or You answer by <strong>counting</strong> — pointing at one thing at a time until everything has
fingers. been pointed at. This chapter uses <strong>pictures of dots</strong> to stand for real things,
like marbles or fingers.
</p> </p>
<h2 id="what-is-counting" class="book-prose__heading">What is counting?</h2> <h2 id="what-is-counting" class="book-prose__heading">What is counting?</h2>
@ -27,20 +28,13 @@ strand = "Number & Algebra"
the number of fingers matches the number of objects. the number of fingers matches the number of objects.
</p> </p>
<h2 id="zero" class="book-prose__heading">Zero</h2> <h2 id="zero" class="book-prose__heading">Zero and one (recap)</h2>
<p> <p>
When there is <strong>nothing</strong> to point at, we write <strong>0</strong> and say <strong>0</strong> means nothing to count; <strong>1</strong> means exactly one. When you count
<strong>zero</strong>. An empty plate, no toys in the box — zero dots: aloud you still say zero before one, then two, and so on. If you want the full picture of
nothing versus one — and how that pairs with false and true — revisit
<a href="/math/zero-and-one/#zero">Zero and one</a>.
</p> </p>
<p class="counting-dots counting-dots--empty" aria-label="zero dots">
<span class="counting-dots__none">(none)</span>
</p>
<p>Zero comes before one when we count: first nothing, then one, then two, and so on.</p>
<div class="book-callout" role="note">
<p class="book-callout__label">Remember</p>
<p>Zero means “none here,” not “forgot to count.”</p>
</div>
<h2 id="numbers-one-to-ten" class="book-prose__heading">Numbers 1 to 10</h2> <h2 id="numbers-one-to-ten" class="book-prose__heading">Numbers 1 to 10</h2>
<p> <p>

View file

@ -1,77 +1,4 @@
// Figurative dots and patterns in chapter prose // counting-dots and shared dot rules: content/math/subject.scss
.counting-dots {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
vertical-align: middle;
}
.counting-dots__dot {
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
background: #0f766e;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
flex-shrink: 0;
}
.counting-dots--empty .counting-dots__none {
font-size: 0.85rem;
font-style: italic;
color: var(--muted);
}
.counting-dots--compare {
max-width: 100px;
}
.counting-dots--pair {
display: inline-flex;
flex-wrap: wrap;
gap: 8px;
max-width: 200px;
}
.counting-dots__pair {
display: inline-flex;
gap: 4px;
padding: 4px 6px;
border-radius: 6px;
background: rgba(20, 184, 166, 0.12);
border: 1px dashed rgba(15, 118, 110, 0.35);
}
.counting-dots--odd {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
max-width: 220px;
}
.counting-dots__singleton {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
border-radius: 6px;
background: rgba(234, 88, 12, 0.14);
border: 2px solid rgba(180, 83, 9, 0.45);
}
.counting-dots__singleton .counting-dots__dot {
background: #c2410c;
}
.counting-pattern-ladder--odd .counting-pattern-step {
background: rgba(234, 88, 12, 0.06);
}
.counting-number-table td:last-child {
min-width: 110px;
}
.counting-compare { .counting-compare {
display: flex; display: flex;
@ -473,235 +400,314 @@
gap: 6px; gap: 6px;
} }
// Big numbers scale widget // Big numbers scale widget (canvas + audio)
.big-numbers-scale__picker { .book-prose .math-widget-mount:has(.big-numbers-scale) {
display: flex; max-width: none;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
} }
.big-numbers-scale__btn { .big-numbers-scale {
padding: 6px 12px; --big-scale-canvas-bg: var(--paper);
--big-scale-grid: color-mix(in srgb, var(--line) 70%, transparent);
--big-scale-star: color-mix(in srgb, var(--soft-ink) 48%, transparent);
width: min(1320px, 96vw);
max-width: none;
}
[data-theme="dark"] .big-numbers-scale,
:root:not([data-theme="light"]) .big-numbers-scale {
--big-scale-canvas-bg: color-mix(in srgb, var(--card-bg) 90%, black);
--big-scale-grid: color-mix(in srgb, var(--line) 78%, rgba(148, 163, 184, 0.2));
--big-scale-star: color-mix(in srgb, var(--soft-ink) 62%, rgba(255, 255, 255, 0.18));
}
.big-numbers-scale__layout {
display: grid;
gap: 14px;
}
.big-numbers-scale__canvas-panel,
.big-numbers-scale__controls,
.big-numbers-scale__section {
min-width: 0;
}
.big-numbers-scale__canvas-panel {
border: 1px solid var(--line);
border-radius: 12px;
background: var(--paper);
overflow: hidden;
}
.big-numbers-scale__toolbar,
.big-numbers-scale__slider-wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
}
.big-numbers-scale__slider-wrap {
border-top: 1px solid var(--line);
border-bottom: 0;
flex-direction: column;
align-items: stretch;
}
.big-numbers-scale__slider-meta {
display: flex;
justify-content: space-between;
font-family: var(--mono);
font-size: 0.75rem;
color: var(--soft-ink);
}
.big-numbers-scale__slider {
width: 100%;
}
.big-numbers-scale__tool-btn,
.big-numbers-scale__autoplay-btn,
.speed-btn,
.preset-step-btn {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
font-family: var(--mono);
font-size: 0.8rem;
background: var(--card-bg); background: var(--card-bg);
color: var(--ink); color: var(--ink);
font-family: var(--mono);
cursor: pointer; cursor: pointer;
} }
.big-numbers-scale__btn--active { .big-numbers-scale__tool-btn,
border-color: #0f766e; .big-numbers-scale__autoplay-btn {
padding: 7px 10px;
font-size: 0.76rem;
}
.big-numbers-scale__autoplay-btn {
border-color: rgba(20, 184, 166, 0.45);
background: rgba(20, 184, 166, 0.15); background: rgba(20, 184, 166, 0.15);
font-weight: 700; font-weight: 700;
} }
.big-numbers-scale__display { .big-numbers-scale__autoplay-btn--pause {
margin: 16px 0 8px; border-color: rgba(234, 88, 12, 0.45);
text-align: center; background: rgba(234, 88, 12, 0.16);
} }
.big-numbers-scale__number { .big-numbers-scale__badge {
padding: 3px 7px;
border-radius: 999px;
border: 1px solid var(--line);
font-family: var(--mono); font-family: var(--mono);
font-size: 2rem; font-size: 0.72rem;
font-weight: 700;
}
.big-numbers-scale__compare {
margin: 0 0 12px;
font-size: 0.9rem;
text-align: center;
color: var(--soft-ink); color: var(--soft-ink);
} }
.big-numbers-scale__visual { .big-numbers-scale__hud {
display: flex; display: grid;
justify-content: center; grid-template-columns: 1fr;
min-height: 120px; gap: 8px;
margin: 12px 0; padding: 10px 12px;
padding: 16px; }
border: 1px dashed var(--line);
.big-numbers-scale__hud-card {
padding: 8px;
border: 1px solid var(--line);
border-radius: 10px;
background: color-mix(in srgb, var(--card-bg) 84%, #0b1220);
}
.big-numbers-scale__hud-card--right {
text-align: right;
}
.big-numbers-scale__hud-label {
display: block;
font-size: 0.7rem;
color: var(--soft-ink);
}
.big-numbers-scale__hud-value {
display: block;
margin-top: 2px;
font-family: var(--mono);
font-size: 1.35rem;
font-weight: 700;
}
.big-numbers-scale__hud-value--small {
font-size: 1.05rem;
}
.big-numbers-scale__hud-sub {
display: block;
margin-top: 2px;
font-size: 0.72rem;
color: var(--soft-ink);
}
.big-numbers-scale__tier-badge {
display: inline-block;
margin-top: 4px;
padding: 2px 6px;
border-radius: 6px;
font-size: 0.66rem;
font-family: var(--mono);
font-weight: 700;
color: #08111f;
background: #14b8a6;
}
.big-numbers-scale__tier-badge--green {
background: #14b8a6;
}
.big-numbers-scale__tier-badge--purple {
background: #8b5cf6;
}
.big-numbers-scale__tier-badge--orange {
background: #f97316;
}
.big-numbers-scale__tier-badge--yellow {
background: #f59e0b;
}
.big-numbers-scale__tier-badge--cyan {
background: #06b6d4;
}
.big-numbers-scale__canvas-wrap {
position: relative;
height: 400px;
background: var(--big-scale-canvas-bg);
}
.big-numbers-scale__canvas {
width: 100%;
height: 100%;
display: block;
cursor: grab;
}
.big-numbers-scale__canvas:active {
cursor: grabbing;
}
.big-numbers-scale__status {
position: absolute;
left: 12px;
bottom: 12px;
padding: 6px 10px;
border-radius: 8px;
background: rgba(2, 6, 23, 0.85);
border: 1px solid var(--line);
color: var(--soft-ink);
font-size: 0.74rem;
opacity: 0;
transform: translateY(5px);
transition: opacity 160ms ease, transform 160ms ease;
}
.big-numbers-scale__status--visible {
opacity: 1;
transform: translateY(0);
}
.big-numbers-scale__controls {
display: grid;
gap: 10px;
}
.big-numbers-scale__section {
border: 1px solid var(--line);
border-radius: 10px; border-radius: 10px;
background: var(--card-bg); background: var(--card-bg);
padding: 10px;
} }
.big-numbers-scale__grid { .big-numbers-scale__section-title {
display: grid; margin: 0 0 8px;
gap: 4px;
}
.big-numbers-scale__grid--small .big-numbers-scale__cell {
width: 6px;
height: 6px;
}
.big-numbers-scale__cell {
width: 10px;
height: 10px;
border-radius: 50%;
background: #0f766e;
}
.big-numbers-scale__blocks {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
max-width: 280px;
}
.big-numbers-scale__block {
padding: 6px 8px;
border-radius: 4px;
font-family: var(--mono);
font-size: 0.65rem;
font-weight: 700;
background: rgba(20, 184, 166, 0.2);
border: 1px solid rgba(15, 118, 110, 0.35);
}
.big-numbers-scale__block--hundred {
min-width: 36px;
text-align: center;
}
.big-numbers-scale__visual-caption {
flex: 1 1 100%;
margin: 10px 0 0;
font-size: 0.82rem; font-size: 0.82rem;
line-height: 1.45;
text-align: center;
color: var(--soft-ink); color: var(--soft-ink);
} }
.big-numbers-scale__visual-caption--inline { .big-numbers-scale__preset-grid {
margin: 8px 0 0;
}
.big-numbers-scale__math-hint {
font-family: var(--mono);
font-size: 0.85em;
}
// 1 million: 100 × 10,000
.big-numbers-scale__million {
width: 100%;
}
.big-numbers-scale__mega-grid {
display: grid; display: grid;
grid-template-columns: repeat(10, 1fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 3px; gap: 6px;
max-width: 280px;
margin: 0 auto;
} }
.big-numbers-scale__mega-cell { .preset-step-btn {
display: flex;
align-items: center;
justify-content: center;
min-height: 22px;
border-radius: 3px;
font-family: var(--mono);
font-size: 0.5rem;
font-weight: 700;
color: #0f766e;
background: rgba(20, 184, 166, 0.22);
border: 1px solid rgba(15, 118, 110, 0.35);
}
// 1 billion: mini million × 1000 → grid of 1M
.big-numbers-scale__billion {
width: 100%;
}
.big-numbers-scale__billion-compare {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px 14px;
}
.big-numbers-scale__billion-unit {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: flex-start;
gap: 6px; gap: 2px;
padding: 8px; padding: 7px;
border-radius: 8px; min-height: 58px;
background: rgba(20, 184, 166, 0.06);
border: 1px solid var(--line);
} }
.big-numbers-scale__billion-unit--wide { .preset-step-btn__power {
flex: 1 1 200px; font-size: 0.65rem;
max-width: 320px; color: var(--soft-ink);
background: rgba(234, 88, 12, 0.06); font-family: var(--mono);
border-color: rgba(180, 83, 9, 0.35);
} }
.big-numbers-scale__billion-unit-label { .preset-step-btn__label {
font-size: 0.72rem; font-size: 0.78rem;
font-weight: 700; font-weight: 700;
text-transform: uppercase; }
letter-spacing: 0.05em;
.preset-step-btn--active {
border-color: rgba(20, 184, 166, 0.55);
background: rgba(20, 184, 166, 0.15);
}
.big-numbers-scale__speed-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.speed-btn {
padding: 6px 8px;
font-size: 0.75rem;
}
.speed-btn--active {
border-color: rgba(20, 184, 166, 0.55);
background: rgba(20, 184, 166, 0.18);
font-weight: 700;
}
.big-numbers-scale__nav-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
margin-top: 8px;
}
.big-numbers-scale__stat-text,
.big-numbers-scale__analogy {
margin: 0;
font-size: 0.78rem;
color: var(--soft-ink); color: var(--soft-ink);
} }
.big-numbers-scale__mini-grid { .big-numbers-scale__stat-value {
display: grid; margin: 8px 0;
grid-template-columns: repeat(4, 1fr); padding: 8px;
gap: 2px; border-radius: 8px;
} border: 1px solid var(--line);
.big-numbers-scale__mini-cell {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 18px;
border-radius: 2px;
font-family: var(--mono); font-family: var(--mono);
font-size: 0.45rem;
font-weight: 700; font-weight: 700;
color: #0f766e;
background: rgba(20, 184, 166, 0.25);
} }
.big-numbers-scale__billion-times { @media (min-width: 960px) {
font-family: var(--mono); .big-numbers-scale__layout {
font-size: 1rem; grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
font-weight: 700; }
color: var(--accent); .big-numbers-scale__hud {
} grid-template-columns: repeat(2, minmax(0, 1fr));
}
.big-numbers-scale__billion-equals {
font-family: var(--mono);
font-size: 1.25rem;
font-weight: 700;
color: var(--muted);
}
.big-numbers-scale__giga-grid {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 2px;
width: 100%;
}
.big-numbers-scale__giga-cell {
display: flex;
align-items: center;
justify-content: center;
min-height: 18px;
border-radius: 2px;
font-family: var(--mono);
font-size: 0.48rem;
font-weight: 700;
color: #9a3412;
background: rgba(234, 88, 12, 0.28);
border: 1px solid rgba(180, 83, 9, 0.4);
} }

View file

@ -1,9 +1,10 @@
<div class="book-prose"> <div class="book-prose">
<p> <p>
Look around you. How many birds? How many cups? You can answer by You already met <strong>0</strong> and <strong>1</strong> in
<strong>counting</strong> — pointing at one thing at a time until everything has been pointed at. <a href="/math/zero-and-one/">Zero and one</a>. Now look around: how many birds? how many cups?
This chapter uses <strong>pictures of dots</strong> to stand for real things, like marbles or You answer by <strong>counting</strong> — pointing at one thing at a time until everything has
fingers. been pointed at. This chapter uses <strong>pictures of dots</strong> to stand for real things,
like marbles or fingers.
</p> </p>
<h2 id="what-is-counting" class="book-prose__heading">What is counting?</h2> <h2 id="what-is-counting" class="book-prose__heading">What is counting?</h2>
@ -16,20 +17,13 @@
the number of fingers matches the number of objects. the number of fingers matches the number of objects.
</p> </p>
<h2 id="zero" class="book-prose__heading">Zero</h2> <h2 id="zero" class="book-prose__heading">Zero and one (recap)</h2>
<p> <p>
When there is <strong>nothing</strong> to point at, we write <strong>0</strong> and say <strong>0</strong> means nothing to count; <strong>1</strong> means exactly one. When you count
<strong>zero</strong>. An empty plate, no toys in the box — zero dots: aloud you still say zero before one, then two, and so on. If you want the full picture of
nothing versus one — and how that pairs with false and true — revisit
<a href="/math/zero-and-one/#zero">Zero and one</a>.
</p> </p>
<p class="counting-dots counting-dots--empty" aria-label="zero dots">
<span class="counting-dots__none">(none)</span>
</p>
<p>Zero comes before one when we count: first nothing, then one, then two, and so on.</p>
<div class="book-callout" role="note">
<p class="book-callout__label">Remember</p>
<p>Zero means “none here,” not “forgot to count.”</p>
</div>
<h2 id="numbers-one-to-ten" class="book-prose__heading">Numbers 1 to 10</h2> <h2 id="numbers-one-to-ten" class="book-prose__heading">Numbers 1 to 10</h2>
<p> <p>

File diff suppressed because it is too large Load diff

106
content/math/subject.scss Normal file
View file

@ -0,0 +1,106 @@
// Shared styles for all mathematics chapters (compiled to /css/subjects/math.css).
// Chapter-specific rules stay in each chapter's chapter.scss.
// --- Figurative dot pictures (counting, zero/one, tables) ---
.counting-dots {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
vertical-align: middle;
}
.counting-dots__dot {
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
background: #0f766e;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
flex-shrink: 0;
}
.counting-dots--empty .counting-dots__none {
font-size: 0.85rem;
font-style: italic;
color: var(--muted);
}
.counting-dots--compare {
max-width: 100px;
}
.counting-dots--pair {
display: inline-flex;
flex-wrap: wrap;
gap: 8px;
max-width: 200px;
}
.counting-dots__pair {
display: inline-flex;
gap: 4px;
padding: 4px 6px;
border-radius: 6px;
background: rgba(20, 184, 166, 0.12);
border: 1px dashed rgba(15, 118, 110, 0.35);
}
.counting-dots--odd {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
max-width: 220px;
}
.counting-dots__singleton {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
border-radius: 6px;
background: rgba(234, 88, 12, 0.14);
border: 2px solid rgba(180, 83, 9, 0.45);
}
.counting-dots__singleton .counting-dots__dot {
background: #c2410c;
}
.counting-pattern-ladder--odd .counting-pattern-step {
background: rgba(234, 88, 12, 0.06);
}
.counting-number-table td:last-child {
min-width: 110px;
}
// --- Notation chapter dot chains ---
.notation-dots {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
vertical-align: middle;
}
.notation-dots__dot {
display: block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #0f766e;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
flex-shrink: 0;
}
.notation-dots--chain {
max-width: 88px;
justify-content: center;
}
.notation-dots--decimal .notation-dots__dot {
width: 14px;
height: 14px;
}

12
content/math/subject.ts Normal file
View file

@ -0,0 +1,12 @@
/**
* Shared JavaScript for all mathematics chapters.
* Loaded on every /math/{slug}/ page via main.tsx + generated subject-modules.ts.
*/
/** Normalize checkpoint markup and run any math-only setup on the chapter article. */
export function initMathSubject(root: HTMLElement): void {
for (const panel of root.querySelectorAll<HTMLElement>(".book-question__answer")) {
panel.classList.remove("book-question__answer");
panel.classList.add("book-question__solution");
}
}

View file

@ -0,0 +1,137 @@
+++
title = "Zero and one"
description = "0 is nothing; 1 is one. Learn to tell them apart — the same skill as telling false from true — before counting larger amounts."
template = "chapter.html"
weight = 0
[extra]
subject = "math"
chapter_id = "zero-and-one"
strand = "Number & Algebra"
+++
<div class="book-prose">
<p>
Mathematics begins with a simple question: <strong>how many?</strong> The smallest answers are
<strong>0</strong> and <strong>1</strong>. Before you count birds, cups, or billions, you need to
be sure you can tell <strong>nothing</strong> from <strong>one</strong>.
</p>
<h2 id="zero" class="book-prose__heading">Zero — nothing</h2>
<p>
When there is <strong>nothing</strong> to point at, we write <strong>0</strong> and say
<strong>zero</strong>. An empty tray, a switched-off light with no glow, silence — no dot to
count:
</p>
<p class="counting-dots counting-dots--empty" aria-label="zero dots">
<span class="counting-dots__none">(none)</span>
</p>
<p>
Zero is not “forgot to look.” It means <strong>none here</strong> — the answer when the amount
is nothing.
</p>
<h2 id="one" class="book-prose__heading">One — exactly one</h2>
<p>
When there is a single thing to point at once, we write <strong>1</strong> and say
<strong>one</strong>. One apple on the plate, one tap, one dot:
</p>
<p class="counting-dots" role="img" aria-label="one dot">
<span class="counting-dots__dot"></span>
</p>
<p>One is not “a few” and not “many.” It is <strong>exactly one</strong>.</p>
<h2 id="compare" class="book-prose__heading">Tell them apart</h2>
<p>0 and 1 sit side by side. Mixing them up changes the whole answer.</p>
<div class="zero-one-compare" role="group" aria-label="Zero compared with one">
<div class="zero-one-compare__cell zero-one-compare__cell--zero">
<p class="zero-one-compare__digit">0</p>
<p class="zero-one-compare__word">zero</p>
<p class="counting-dots counting-dots--empty" aria-hidden="true">
<span class="counting-dots__none">none</span>
</p>
<p class="zero-one-compare__hint">nothing to count</p>
</div>
<div class="zero-one-compare__cell zero-one-compare__cell--one">
<p class="zero-one-compare__digit">1</p>
<p class="zero-one-compare__word">one</p>
<p class="counting-dots" aria-hidden="true">
<span class="counting-dots__dot"></span>
</p>
<p class="zero-one-compare__hint">one thing to count</p>
</div>
</div>
<h2 id="true-and-false" class="book-prose__heading">True and false</h2>
<p>
You can also ask whether a <strong>statement</strong> matches the world. “The cup is on the
table.” — if it is there, the statement is <strong>true</strong>. If the cup is not there, the
statement is <strong>false</strong>.
</p>
<p>
For a single yes-or-no question about <strong>one</strong> thing, the same care applies:
</p>
<ul>
<li><strong>False</strong> fits when the answer is <strong>no</strong> — none of what we named, like <strong>0</strong>.</li>
<li><strong>True</strong> fits when the answer is <strong>yes, exactly once</strong> — like <strong>1</strong> when we mean one clear match.</li>
</ul>
<p>
Later you will meet longer counts and trickier logic. The habit starts here: look carefully, then
decide — is it <strong>0 or 1</strong>? <strong>false or true</strong>?
</p>
<div class="book-callout" role="note">
<p class="book-callout__label">The essence</p>
<p>
Telling <strong>0</strong> from <strong>1</strong> is the same kind of attention as telling
<strong>false</strong> from <strong>true</strong>. That distinction is the foundation for every
number and for understanding claims about the world.
</p>
</div>
<div class="math-widget-mount" data-widget="zero-one-board" data-pagefind-ignore></div>
<p>
When you are steady with 0 and 1, continue to
<a href="/math/counting-and-numbers/">Counting and numbers</a> to count past one and meet larger
amounts.
</p>
</div>
<div class="book-prose">
<h2 id="history" class="book-prose__heading">History</h2>
<p>
People counted with fingers and marks long before anyone wrote <strong>0</strong> as a number.
In ancient India and among the Maya, thinkers found a symbol for <strong>nothing</strong> — not
just “empty space on the page,” but a number you could calculate with.
</p>
<p>
That step mattered: without zero, place value and modern arithmetic are much harder. The idea
“nothing is something we can name” is as deep as the idea “one is one.”
</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<details class="book-question">
<summary class="book-question__prompt">You look in a box and see no dots. How many?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(0\) (zero) — nothing to count.</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">You see exactly one dot. How many?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(1\) (one).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Is the sentence “2 is less than 1” true or false?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> False — two is not less than one.</p>
</div>
</details>
<h2 id="references" class="book-prose__heading">References</h2>
<ul>
<li>Ifrah, Georges — <em>The Universal History of Numbers</em> (origins of zero and counting).</li>
<li>Crossley &amp; Henry — early Indian arithmetic including zero.</li>
</ul>
</div>

View file

@ -0,0 +1,123 @@
// Dot pictures: counting-dots in subject.scss
.zero-one-compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 20px 0;
padding: 16px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--card-bg);
}
.zero-one-compare__cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 14px 10px;
border-radius: 10px;
text-align: center;
}
.zero-one-compare__cell--zero {
background: rgba(100, 116, 139, 0.08);
border: 1px dashed var(--line);
}
.zero-one-compare__cell--one {
background: rgba(20, 184, 166, 0.1);
border: 1px solid rgba(15, 118, 110, 0.35);
}
.zero-one-compare__digit {
margin: 0;
font-family: var(--mono);
font-size: 2rem;
font-weight: 700;
color: var(--ink);
}
.zero-one-compare__word {
margin: 0;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--soft-ink);
}
.zero-one-compare__hint {
margin: 0;
font-size: 0.85rem;
color: var(--soft-ink);
}
.zero-one-board__scenario {
margin: 0 0 12px;
font-size: 1.05rem;
line-height: 1.45;
}
.zero-one-board__visual {
display: flex;
align-items: center;
justify-content: center;
min-height: 72px;
margin: 12px 0;
padding: 16px;
border: 1px dashed var(--line);
border-radius: 10px;
background: var(--card-bg);
transition: border-color 0.15s ease, background 0.15s ease;
}
.zero-one-board__visual--correct {
border-color: rgba(15, 118, 110, 0.55);
background: rgba(20, 184, 166, 0.08);
}
.zero-one-board__visual--incorrect {
border-color: rgba(180, 83, 9, 0.45);
background: rgba(234, 88, 12, 0.06);
}
.zero-one-board__count {
margin: 0;
font-family: var(--mono);
font-size: 2rem;
font-weight: 700;
color: var(--ink);
}
.zero-one-board__choices {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.zero-one-board__choice--active {
border-color: #0f766e;
background: rgba(20, 184, 166, 0.15);
font-weight: 700;
}
.zero-one-board__feedback {
margin: 12px 0 0;
min-height: 1.4em;
text-align: center;
font-size: 0.95rem;
color: var(--soft-ink);
}
.zero-one-board__feedback--ok {
color: #0f766e;
font-weight: 600;
}
.zero-one-board__feedback--nope {
color: #9a3412;
font-weight: 600;
}

View file

@ -0,0 +1,87 @@
<div class="book-prose">
<p>
Mathematics begins with a simple question: <strong>how many?</strong> The smallest answers are
<strong>0</strong> and <strong>1</strong>. Before you count birds, cups, or billions, you need to
be sure you can tell <strong>nothing</strong> from <strong>one</strong>.
</p>
<h2 id="zero" class="book-prose__heading">Zero — nothing</h2>
<p>
When there is <strong>nothing</strong> to point at, we write <strong>0</strong> and say
<strong>zero</strong>. An empty tray, a switched-off light with no glow, silence — no dot to
count:
</p>
<p class="counting-dots counting-dots--empty" aria-label="zero dots">
<span class="counting-dots__none">(none)</span>
</p>
<p>
Zero is not “forgot to look.” It means <strong>none here</strong> — the answer when the amount
is nothing.
</p>
<h2 id="one" class="book-prose__heading">One — exactly one</h2>
<p>
When there is a single thing to point at once, we write <strong>1</strong> and say
<strong>one</strong>. One apple on the plate, one tap, one dot:
</p>
<p class="counting-dots" role="img" aria-label="one dot">
<span class="counting-dots__dot"></span>
</p>
<p>One is not “a few” and not “many.” It is <strong>exactly one</strong>.</p>
<h2 id="compare" class="book-prose__heading">Tell them apart</h2>
<p>0 and 1 sit side by side. Mixing them up changes the whole answer.</p>
<div class="zero-one-compare" role="group" aria-label="Zero compared with one">
<div class="zero-one-compare__cell zero-one-compare__cell--zero">
<p class="zero-one-compare__digit">0</p>
<p class="zero-one-compare__word">zero</p>
<p class="counting-dots counting-dots--empty" aria-hidden="true">
<span class="counting-dots__none">none</span>
</p>
<p class="zero-one-compare__hint">nothing to count</p>
</div>
<div class="zero-one-compare__cell zero-one-compare__cell--one">
<p class="zero-one-compare__digit">1</p>
<p class="zero-one-compare__word">one</p>
<p class="counting-dots" aria-hidden="true">
<span class="counting-dots__dot"></span>
</p>
<p class="zero-one-compare__hint">one thing to count</p>
</div>
</div>
<h2 id="true-and-false" class="book-prose__heading">True and false</h2>
<p>
You can also ask whether a <strong>statement</strong> matches the world. “The cup is on the
table.” — if it is there, the statement is <strong>true</strong>. If the cup is not there, the
statement is <strong>false</strong>.
</p>
<p>
For a single yes-or-no question about <strong>one</strong> thing, the same care applies:
</p>
<ul>
<li><strong>False</strong> fits when the answer is <strong>no</strong> — none of what we named, like <strong>0</strong>.</li>
<li><strong>True</strong> fits when the answer is <strong>yes, exactly once</strong> — like <strong>1</strong> when we mean one clear match.</li>
</ul>
<p>
Later you will meet longer counts and trickier logic. The habit starts here: look carefully, then
decide — is it <strong>0 or 1</strong>? <strong>false or true</strong>?
</p>
<div class="book-callout" role="note">
<p class="book-callout__label">The essence</p>
<p>
Telling <strong>0</strong> from <strong>1</strong> is the same kind of attention as telling
<strong>false</strong> from <strong>true</strong>. That distinction is the foundation for every
number and for understanding claims about the world.
</p>
</div>
<div class="math-widget-mount" data-widget="zero-one-board" data-pagefind-ignore></div>
<p>
When you are steady with 0 and 1, continue to
<a href="/math/counting-and-numbers/">Counting and numbers</a> to count past one and meet larger
amounts.
</p>
</div>

View file

@ -0,0 +1,38 @@
<div class="book-prose">
<h2 id="history" class="book-prose__heading">History</h2>
<p>
People counted with fingers and marks long before anyone wrote <strong>0</strong> as a number.
In ancient India and among the Maya, thinkers found a symbol for <strong>nothing</strong> — not
just “empty space on the page,” but a number you could calculate with.
</p>
<p>
That step mattered: without zero, place value and modern arithmetic are much harder. The idea
“nothing is something we can name” is as deep as the idea “one is one.”
</p>
<h2 id="checkpoints" class="book-prose__heading">Checkpoints</h2>
<details class="book-question">
<summary class="book-question__prompt">You look in a box and see no dots. How many?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(0\) (zero) — nothing to count.</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">You see exactly one dot. How many?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> \(1\) (one).</p>
</div>
</details>
<details class="book-question">
<summary class="book-question__prompt">Is the sentence “2 is less than 1” true or false?</summary>
<div class="book-question__solution">
<p><strong>Answer:</strong> False — two is not less than one.</p>
</div>
</details>
<h2 id="references" class="book-prose__heading">References</h2>
<ul>
<li>Ifrah, Georges — <em>The Universal History of Numbers</em> (origins of zero and counting).</li>
<li>Crossley &amp; Henry — early Indian arithmetic including zero.</li>
</ul>
</div>

View file

@ -0,0 +1,218 @@
import { createMemo, createSignal, For } from "solid-js";
type Scenario = {
id: string;
prompt: string;
correctAmount: 0 | 1;
showTruth?: boolean;
correctTruth?: boolean;
};
const SCENARIOS: Scenario[] = [
{
id: "empty-box",
prompt: "The box has no marbles inside.",
correctAmount: 0,
},
{
id: "one-apple",
prompt: "There is exactly one apple on the plate.",
correctAmount: 1,
},
{
id: "light-off",
prompt: "The lamp is off — no light showing.",
correctAmount: 0,
},
{
id: "light-on",
prompt: "The lamp is on — one steady light.",
correctAmount: 1,
},
{
id: "statement-false",
prompt: "Statement: “The door is open.” (The door is shut.)",
correctAmount: 0,
showTruth: true,
correctTruth: false,
},
{
id: "statement-true",
prompt: "Statement: “There is one bell ringing.” (You hear one bell.)",
correctAmount: 1,
showTruth: true,
correctTruth: true,
},
];
function ZeroOneBoard() {
const [index, setIndex] = createSignal(0);
const [picked, setPicked] = createSignal<0 | 1 | null>(null);
const [truthPicked, setTruthPicked] = createSignal<boolean | null>(null);
const scenario = createMemo(() => SCENARIOS[index() % SCENARIOS.length]);
const atEnd = createMemo(() => index() >= SCENARIOS.length - 1);
const amountCorrect = createMemo(() => {
const p = picked();
return p !== null && p === scenario().correctAmount;
});
const truthCorrect = createMemo(() => {
const s = scenario();
if (!s.showTruth) return true;
const t = truthPicked();
return t !== null && t === s.correctTruth;
});
const answered = createMemo(() => {
const s = scenario();
if (picked() === null) return false;
if (s.showTruth && truthPicked() === null) return false;
return true;
});
const allCorrect = createMemo(() => answered() && amountCorrect() && truthCorrect());
const feedback = createMemo(() => {
if (!answered()) return "Choose how many, then Next when you are ready.";
if (allCorrect()) {
return atEnd()
? "Well done — you can tell 0 from 1. Continue to Counting and numbers."
: "Correct. Try the next situation.";
}
if (!amountCorrect()) {
return picked() === 0
? "Not quite — this situation is one, not zero."
: "Not quite — this situation is zero, not one.";
}
return truthPicked() === true
? "Check the statement again — it does not match the world (false)."
: "Check the statement again — it matches the world (true).";
});
const pickAmount = (n: 0 | 1) => {
setPicked(n);
};
const pickTruth = (value: boolean) => {
setTruthPicked(value);
};
const goNext = () => {
if (!answered() || !allCorrect()) return;
if (atEnd()) return;
setIndex((i) => i + 1);
setPicked(null);
setTruthPicked(null);
};
const resetAll = () => {
setIndex(0);
setPicked(null);
setTruthPicked(null);
};
return (
<section class="math-widget zero-one-board" aria-labelledby="zero-one-board-title">
<h2 id="zero-one-board-title" class="math-widget__title">
Practice: zero or one
</h2>
<div class="math-widget__body">
<p class="math-widget__caption">
Situation {index() + 1} of {SCENARIOS.length}. Pick the amount that fits. When a statement
is shown, also pick true or false.
</p>
<p class="zero-one-board__scenario">{scenario().prompt}</p>
<div
class="zero-one-board__visual"
classList={{
"zero-one-board__visual--correct": answered() && allCorrect(),
"zero-one-board__visual--incorrect": answered() && !allCorrect(),
}}
aria-live="polite"
>
<p class="zero-one-board__count">
{picked() === null ? "?" : picked()}
</p>
<span class="counting-dots" role="img" aria-hidden="true">
<For each={picked() === 1 ? [0] : []}>
{() => <span class="counting-dots__dot" />}
</For>
{picked() === 0 ? (
<span class="counting-dots__none">none</span>
) : null}
</span>
</div>
<div class="zero-one-board__choices" role="group" aria-label="How many?">
<button
type="button"
class="scenario-demo__btn zero-one-board__choice"
classList={{ "zero-one-board__choice--active": picked() === 0 }}
onClick={() => pickAmount(0)}
>
Nothing (0)
</button>
<button
type="button"
class="scenario-demo__btn scenario-demo__btn--primary zero-one-board__choice"
classList={{ "zero-one-board__choice--active": picked() === 1 }}
onClick={() => pickAmount(1)}
>
One (1)
</button>
</div>
{scenario().showTruth ? (
<div class="zero-one-board__choices" role="group" aria-label="True or false?">
<button
type="button"
class="scenario-demo__btn zero-one-board__choice"
classList={{ "zero-one-board__choice--active": truthPicked() === false }}
onClick={() => pickTruth(false)}
>
False
</button>
<button
type="button"
class="scenario-demo__btn scenario-demo__btn--primary zero-one-board__choice"
classList={{ "zero-one-board__choice--active": truthPicked() === true }}
onClick={() => pickTruth(true)}
>
True
</button>
</div>
) : null}
<p
class="zero-one-board__feedback"
classList={{
"zero-one-board__feedback--ok": answered() && allCorrect(),
"zero-one-board__feedback--nope": answered() && !allCorrect(),
}}
aria-live="polite"
>
{feedback()}
</p>
<div class="scenario-demo__actions">
<button type="button" class="scenario-demo__btn" onClick={resetAll}>
Start over
</button>
<button
type="button"
class="scenario-demo__btn scenario-demo__btn--primary"
onClick={goNext}
disabled={!answered() || !allCorrect() || atEnd()}
>
Next situation
</button>
</div>
</div>
</section>
);
}
export default ZeroOneBoard;

View file

@ -3,6 +3,10 @@
"intro": "Topic units across three strands, shaped for learners worldwide. The map shows required prerequisites; the list follows a readable teaching order; Compare explains how chapters relate to popular curricula.", "intro": "Topic units across three strands, shaped for learners worldwide. The map shows required prerequisites; the list follows a readable teaching order; Compare explains how chapters relate to popular curricula.",
"graph": { "graph": {
"edges": [ "edges": [
{
"from": "zero-and-one",
"to": "counting-and-numbers"
},
{ {
"from": "counting-and-numbers", "from": "counting-and-numbers",
"to": "basic-math-notation" "to": "basic-math-notation"
@ -206,10 +210,28 @@
"id": "number-algebra", "id": "number-algebra",
"title": "Number & Algebra", "title": "Number & Algebra",
"chapters": [ "chapters": [
{
"slug": "zero-and-one",
"title": "Zero and one",
"description": "0 is nothing; 1 is one. Learn to tell them apart — the same skill as telling false from true — before counting larger amounts.",
"status": "live",
"curriculumCoverage": {
"Foundation": "core",
"IGCSE": "core",
"Common Core": "core",
"Singapore": "core",
"UK GCSE": "core",
"DSE": "related",
"IB": "related",
"A-Level": "related",
"AP": "related",
"Gaokao": "related"
}
},
{ {
"slug": "counting-and-numbers", "slug": "counting-and-numbers",
"title": "Counting and numbers", "title": "Counting and numbers",
"description": "Count from zero, spot patterns, add, multiply, and divide; then meet place value and big numbers from hundreds to billions.", "description": "Count objects, spot patterns, add, multiply, and divide; then meet place value and big numbers from hundreds to billions.",
"status": "live", "status": "live",
"curriculumCoverage": { "curriculumCoverage": {
"Foundation": "core", "Foundation": "core",

View file

@ -134,7 +134,21 @@ if (chapterNavRoot) {
}); });
} }
function mountSubjectInit() {
const article = document.querySelector<HTMLElement>("[data-chapter]");
const chapterKey = article?.dataset.chapter;
if (!article || !chapterKey) return;
const subject = chapterKey.split("/")[0];
void import("./generated/subject-modules").then(({ subjectInitLoaders }) => {
const loader = subjectInitLoaders[subject];
if (!loader) return;
void loader().then((mod) => mod.initSubject(article));
});
}
initBookMath(); initBookMath();
mountSubjectInit();
mountChapterWidgets(); mountChapterWidgets();
const catalogRoot = document.getElementById("catalog-app"); const catalogRoot = document.getElementById("catalog-app");

View file

@ -13,6 +13,18 @@ const JSON_PATH = path.join(ROOT, "data/math-curriculum.json");
/** slug → framework label → role */ /** slug → framework label → role */
const COVERAGE_BY_SLUG: Record<string, Coverage> = { const COVERAGE_BY_SLUG: Record<string, Coverage> = {
"zero-and-one": {
Foundation: "core",
IGCSE: "core",
"Common Core": "core",
Singapore: "core",
"UK GCSE": "core",
DSE: "related",
IB: "related",
"A-Level": "related",
AP: "related",
Gaokao: "related",
},
"counting-and-numbers": { "counting-and-numbers": {
Foundation: "core", Foundation: "core",
IGCSE: "core", IGCSE: "core",

View file

@ -8,8 +8,11 @@ const CONTENT = join(ROOT, "content");
const GENERATED_META = join(ROOT, "data/_generated/chapters-meta.json"); const GENERATED_META = join(ROOT, "data/_generated/chapters-meta.json");
const GENERATED_NAV = join(ROOT, "data/_generated/chapter-nav.json"); const GENERATED_NAV = join(ROOT, "data/_generated/chapter-nav.json");
const GENERATED_WIDGETS = join(ROOT, "frontend/src/generated/chapter-widgets.ts"); const GENERATED_WIDGETS = join(ROOT, "frontend/src/generated/chapter-widgets.ts");
const GENERATED_SUBJECT_MODULES = join(ROOT, "frontend/src/generated/subject-modules.ts");
const GENERATED_SUBJECTS_META = join(ROOT, "data/_generated/subjects-meta.json");
const STATIC_ROOT = join(ROOT, "static"); const STATIC_ROOT = join(ROOT, "static");
const CHAPTER_CSS_DIR = join(STATIC_ROOT, "css/chapters"); const CHAPTER_CSS_DIR = join(STATIC_ROOT, "css/chapters");
const SUBJECT_CSS_DIR = join(STATIC_ROOT, "css/subjects");
type ChapterMeta = { type ChapterMeta = {
hasSupplement: boolean; hasSupplement: boolean;
@ -44,6 +47,15 @@ type ChapterNavFile = {
byKey: Record<string, ChapterNavEntry>; byKey: Record<string, ChapterNavEntry>;
}; };
type SubjectMeta = {
hasCss: boolean;
hasJs: boolean;
};
type SubjectsMetaFile = {
subjects: Record<string, SubjectMeta>;
};
function parseChapterTemplate(markdown: string): boolean { function parseChapterTemplate(markdown: string): boolean {
return /template\s*=\s*["']chapter\.html["']/.test(markdown); return /template\s*=\s*["']chapter\.html["']/.test(markdown);
} }
@ -107,6 +119,65 @@ async function compileChapterScss(
return true; return true;
} }
async function compileSubjectScss(subject: string, scssPath: string): Promise<boolean> {
const outPath = join(SUBJECT_CSS_DIR, `${subject}.css`);
await mkdir(SUBJECT_CSS_DIR, { recursive: true });
const result = await $`sass ${scssPath} ${outPath} --style=compressed --no-source-map`.quiet();
if (result.exitCode !== 0) {
console.error(`Failed to compile ${scssPath}`);
process.exit(1);
}
return true;
}
async function discoverSubjects(): Promise<Array<{ id: string; dir: string }>> {
const subjects: Array<{ id: string; dir: string }> = [];
let entries;
try {
entries = await readdir(CONTENT, { withFileTypes: true });
} catch {
return subjects;
}
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
subjects.push({ id: entry.name, dir: join(CONTENT, entry.name) });
}
return subjects;
}
async function syncSubject(subject: { id: string; dir: string }): Promise<SubjectMeta> {
const scssPath = join(subject.dir, "subject.scss");
const tsPath = join(subject.dir, "subject.ts");
let hasCss = false;
let hasJs = false;
if (existsSync(scssPath)) {
hasCss = await compileSubjectScss(subject.id, scssPath);
}
if (existsSync(tsPath)) {
hasJs = true;
}
return { hasCss, hasJs };
}
function buildSubjectModuleRegistry(
subjects: Array<{ id: string; dir: string; hasJs: boolean }>
): string {
const withJs = subjects.filter((s) => s.hasJs);
const lines: string[] = [
"// Auto-generated by scripts/sync-chapters.ts — do not edit.",
"",
"export type SubjectModule = { initSubject: (root: HTMLElement) => void };",
"",
"export const subjectInitLoaders: Record<string, () => Promise<SubjectModule>> = {",
];
for (const { id, dir } of withJs) {
const relFromGenerated = relative(dirname(GENERATED_SUBJECT_MODULES), dir).replace(/\\/g, "/");
lines.push(` "${id}": () => import("${relFromGenerated}/subject.ts"),`);
}
lines.push("};", "");
return lines.join("\n");
}
async function discoverChapters(): Promise< async function discoverChapters(): Promise<
Array<{ subject: string; slug: string; dir: string }> Array<{ subject: string; slug: string; dir: string }>
> { > {
@ -273,6 +344,22 @@ async function main() {
const chapterNav = await buildChapterNav(chapters); const chapterNav = await buildChapterNav(chapters);
await writeFile(GENERATED_NAV, `${JSON.stringify(chapterNav, null, 2)}\n`); await writeFile(GENERATED_NAV, `${JSON.stringify(chapterNav, null, 2)}\n`);
const subjectEntries: Array<{ id: string; dir: string; hasJs: boolean }> = [];
const subjectsMeta: SubjectsMetaFile = { subjects: {} };
for (const subject of await discoverSubjects()) {
const meta = await syncSubject(subject);
subjectsMeta.subjects[subject.id] = meta;
subjectEntries.push({ id: subject.id, dir: subject.dir, hasJs: meta.hasJs });
if (meta.hasCss || meta.hasJs) {
console.log(
`Synced subject ${subject.id} (css: ${meta.hasCss ? "yes" : "no"}, js: ${meta.hasJs ? "yes" : "no"})`
);
}
}
await writeFile(GENERATED_SUBJECTS_META, `${JSON.stringify(subjectsMeta, null, 2)}\n`);
await mkdir(dirname(GENERATED_SUBJECT_MODULES), { recursive: true });
await writeFile(GENERATED_SUBJECT_MODULES, buildSubjectModuleRegistry(subjectEntries));
console.log(`Synced ${chapters.length} chapter(s).`); console.log(`Synced ${chapters.length} chapter(s).`);
} }

View file

@ -240,6 +240,18 @@ a.book-chapter-link:focus-visible {
margin: 0 0 1.1em; margin: 0 0 1.1em;
} }
.book-prose a {
color: var(--accent);
font-weight: 600;
text-decoration: none;
text-underline-offset: 3px;
}
.book-prose a:hover,
.book-prose a:focus-visible {
text-decoration: underline;
}
.book-prose__heading { .book-prose__heading {
margin: 2em 0 0.6em; margin: 2em 0 0.6em;
} }
@ -433,6 +445,18 @@ a.book-chapter-link:focus-visible {
margin: 0.75rem 0 0; margin: 0.75rem 0 0;
} }
// Legacy/alternate markup — prefer book-question__solution in chapter HTML
.book-question__answer {
padding: 0 18px 16px;
border-top: 1px solid var(--card-border);
font-size: 0.95rem;
color: var(--soft-ink);
}
.book-question__answer p {
margin: 0.75rem 0 0;
}
.ratio-demo__bars { .ratio-demo__bars {
display: flex; display: flex;
width: 100%; width: 100%;

View file

@ -14,7 +14,12 @@
{% set slug = section.extra.chapter_id | default(value=section.slug) %} {% set slug = section.extra.chapter_id | default(value=section.slug) %}
{% set chapter_key = subject ~ "/" ~ slug %} {% set chapter_key = subject ~ "/" ~ slug %}
{% set chapters_meta = load_data(path="data/_generated/chapters-meta.json", format="json") %} {% set chapters_meta = load_data(path="data/_generated/chapters-meta.json", format="json") %}
{% set subjects_meta = load_data(path="data/_generated/subjects-meta.json", format="json") %}
{% set chapter_meta = chapters_meta.chapters[chapter_key] %} {% set chapter_meta = chapters_meta.chapters[chapter_key] %}
{% set subject_meta = subjects_meta.subjects[subject] | default(value=false) %}
{% if subject_meta and subject_meta.hasCss %}
<link rel="stylesheet" href="{{ get_url(path='css/subjects/' ~ subject ~ '.css', trailing_slash=false) | safe }}" />
{% endif %}
{% if chapter_meta.hasCss %} {% if chapter_meta.hasCss %}
<link rel="stylesheet" href="{{ get_url(path='css/chapters/' ~ subject ~ '-' ~ slug ~ '.css', trailing_slash=false) | safe }}" /> <link rel="stylesheet" href="{{ get_url(path='css/chapters/' ~ subject ~ '-' ~ slug ~ '.css', trailing_slash=false) | safe }}" />
{% endif %} {% endif %}

View file

@ -142,10 +142,7 @@
<section class="catalog-compare-doc__section" aria-labelledby="compare-chapters"> <section class="catalog-compare-doc__section" aria-labelledby="compare-chapters">
<h2 id="compare-chapters" class="catalog-compare-doc__heading">Chapter coverage</h2> <h2 id="compare-chapters" class="catalog-compare-doc__heading">Chapter coverage</h2>
<p> <p>
Each cell comes from <code>curriculumCoverage</code> in Catalog filter chips still match any framework with a non-empty mapping. List and map badges show framework labels only.
<code>math-curriculum.json</code> (per-framework <code>core</code>,
<code>extension</code>, or <code>related</code>). Catalog filter chips still match any
framework with a non-empty mapping. List and map badges show framework labels only.
</p> </p>
<div class="catalog-compare-doc__table-wrap"> <div class="catalog-compare-doc__table-wrap">
<table class="catalog-compare-table catalog-compare-table--chapters"> <table class="catalog-compare-table catalog-compare-table--chapters">