mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
Enhance curriculum catalog functionality: Update CONTRIBUTING.md to reflect changes in curriculum structure, including the introduction of curriculumCoverage for frameworks. Modify frontend components to support a new comparison view for Mathematics, allowing users to compare curricula. Update catalog data to include "Common Core" and improve curriculum filtering logic. Enhance styles for better visual representation of curriculum badges and Mermaid diagrams. Adjust TypeScript types to accommodate new features and ensure backward compatibility with legacy curriculum data.
This commit is contained in:
parent
ede9a7aa63
commit
5175811579
34 changed files with 3702 additions and 176 deletions
|
|
@ -93,7 +93,7 @@ Detailed checklists: [curriculum-data.md](curriculum-data.md). Human-facing guid
|
||||||
| Styles | `themes/openfreebooks/sass/_catalog.scss` |
|
| Styles | `themes/openfreebooks/sass/_catalog.scss` |
|
||||||
|
|
||||||
- Embedded data: `#catalog-data` in `templates/catalog.html` (merged at build time)
|
- Embedded data: `#catalog-data` in `templates/catalog.html` (merged at build time)
|
||||||
- URL: `/catalog/?subject=math` (list), `&view=tree` (map)
|
- URL: `/catalog/?subject=math` (list), `&view=tree` (map), `&view=compare` (math curricula doc — HTML partial `partials/math-curricula-compare.html`)
|
||||||
|
|
||||||
**List view:** full-card link for live chapters; number + title row on wide screens.
|
**List view:** full-card link for live chapters; number + title row on wide screens.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ Site-wide catalog chrome.
|
||||||
{
|
{
|
||||||
"title": "Curriculum Catalog",
|
"title": "Curriculum Catalog",
|
||||||
"subtitle": "Browse chapters by subject and curriculum",
|
"subtitle": "Browse chapters by subject and curriculum",
|
||||||
"curriculums": ["DSE", "IB", "A-Level", "AP", "IGCSE"],
|
"curriculums": ["Foundation", "DSE", "IB", "A-Level", "AP", "IGCSE", "Common Core"],
|
||||||
"subjects": [
|
"subjects": [
|
||||||
{ "id": "math", "name": "Mathematics" }
|
{ "id": "math", "name": "Mathematics" }
|
||||||
]
|
]
|
||||||
|
|
@ -28,7 +28,8 @@ Example: `data/math-curriculum.json`.
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "Mathematics",
|
"title": "Mathematics",
|
||||||
"subtitle": "Short tagline for data (not shown on every page)",
|
"subtitle": "Optional; deprecated — do not use HKDSE-only framing. Catalog chrome uses data/catalog.json.",
|
||||||
|
"intro": "Optional subject-level intro (not shown in catalog UI today)",
|
||||||
"intro": "Optional; was used by old book page",
|
"intro": "Optional; was used by old book page",
|
||||||
"graph": {
|
"graph": {
|
||||||
"edges": [
|
"edges": [
|
||||||
|
|
@ -63,8 +64,9 @@ Example: `data/math-curriculum.json`.
|
||||||
| `title` | yes | Catalog list + map node label |
|
| `title` | yes | Catalog list + map node label |
|
||||||
| `description` | yes | 1–2 sentences; shown in list and map cards (clamped to 3 lines on map) |
|
| `description` | yes | 1–2 sentences; shown in list and map cards (clamped to 3 lines on map) |
|
||||||
| `status` | yes | `live` (link to chapter) or `planned` (no link) |
|
| `status` | yes | `live` (link to chapter) or `planned` (no link) |
|
||||||
| `curriculums` | yes | Non-empty array; values must exist in `catalog.json` `curriculums` |
|
| `curriculumCoverage` | yes (math) | Object mapping framework label → `core` \| `extension` \| `related` (Compare matrix + filters) |
|
||||||
| `tier` | no | `foundation` (default) or `non-foundation` — shows an **Extension** badge on cards |
|
| `curriculums` | legacy | Deprecated array; use `curriculumCoverage` instead |
|
||||||
|
| `tier` | no | `foundation` (default) or `non-foundation` — shows an **Extension** badge on cards (DSE M2-style topics) |
|
||||||
|
|
||||||
### Graph model (map)
|
### Graph model (map)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Static site: **Zola** + custom theme `openfreebooks`. Output goes to `public/` (
|
||||||
| `static/` | Copied to site root (`favicons`, `icons/`, etc.) |
|
| `static/` | Copied to site root (`favicons`, `icons/`, etc.) |
|
||||||
| `themes/openfreebooks/templates/` | Tera HTML, partials, macros |
|
| `themes/openfreebooks/templates/` | Tera HTML, partials, macros |
|
||||||
| `themes/openfreebooks/sass/` | Source styles → compiled `public/main.css` |
|
| `themes/openfreebooks/sass/` | Source styles → compiled `public/main.css` |
|
||||||
| `themes/openfreebooks/static/js/` | Vite output: `bundle.js` (site chrome) + hashed chunks (`catalog-app-*.js`, `catalog-canvas-view-*.js`, …) |
|
| `themes/openfreebooks/static/js/` | Vite output: `bundle.js` (site chrome) + lazy chunks (e.g. `catalog-mermaid-view-*.js`) |
|
||||||
|
|
||||||
See [reference.md](reference.md) for Tera pitfalls and build checks.
|
See [reference.md](reference.md) for Tera pitfalls and build checks.
|
||||||
|
|
||||||
|
|
@ -75,10 +75,12 @@ Client behavior lives in `frontend/` and mounts from `base.html`:
|
||||||
| Entry | Mount | Notes |
|
| Entry | Mount | Notes |
|
||||||
|-------|--------|--------|
|
|-------|--------|--------|
|
||||||
| `js/bundle.js` | `main.tsx` | Header, theme, homepage, chapter widgets |
|
| `js/bundle.js` | `main.tsx` | Header, theme, homepage, chapter widgets |
|
||||||
| `js/catalog-app-*.js` | `#catalog-app` on `/catalog/` | Lazy-loaded with `catalog-canvas-view-*.js` for Map view |
|
| `#catalog-app` on `/catalog/` | `catalog-app.tsx` in bundle | List / Map / Compare; Map lazy-loads `catalog-mermaid-view` |
|
||||||
|
|
||||||
**When changing `frontend/` or adding interactivity**, read [zola-solid.md](zola-solid.md) before editing JS.
|
**When changing `frontend/` or adding interactivity**, read [zola-solid.md](zola-solid.md) before editing JS.
|
||||||
|
|
||||||
|
**When changing the catalog page, map, or compare doc**, read [zola-catalog.md](zola-catalog.md).
|
||||||
|
|
||||||
### Search (Pagefind)
|
### Search (Pagefind)
|
||||||
|
|
||||||
- Runs after `zola build`; indexes **live chapter pages** only (`data-pagefind-body` on chapter template).
|
- Runs after `zola build`; indexes **live chapter pages** only (`data-pagefind-body` on chapter template).
|
||||||
|
|
@ -87,8 +89,10 @@ Client behavior lives in `frontend/` and mounts from `base.html`:
|
||||||
### Catalog page
|
### Catalog page
|
||||||
|
|
||||||
- `content/catalog.md` → `template = "catalog.html"`
|
- `content/catalog.md` → `template = "catalog.html"`
|
||||||
- `templates/catalog.html` — empty `#catalog-app` + `#catalog-data` JSON embed (merged subject curricula at build time)
|
- `templates/catalog.html` — `#catalog-app`, `#catalog-data` JSON, `<template id="catalog-compare-template">` (math compare partial)
|
||||||
- `sass/_catalog.scss` — list + map styles; map card title link underline lives under `.catalog-canvas`
|
- Views: list (default), `?view=tree` (Mermaid DAG map), `?view=compare` (math curricula doc)
|
||||||
|
- Map colors: Mermaid `theme: "base"` + hex `themeVariables` in `frontend/src/lib/catalog-mermaid-theme.ts` — **not** Sass `fill` on cluster rects (inline SVG wins)
|
||||||
|
- Full file map, strand header post-render, and verify steps: [zola-catalog.md](zola-catalog.md)
|
||||||
- Run `bun run build:js` after any `frontend/src/components/catalog-*` or `frontend/src/lib/catalog-*` change
|
- Run `bun run build:js` after any `frontend/src/components/catalog-*` or `frontend/src/lib/catalog-*` change
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,16 @@ If styles “don’t apply”, delete stray `themes/openfreebooks/static/main.cs
|
||||||
- `static/contributors.json` — optional mirror for `/contributors.json` if needed.
|
- `static/contributors.json` — optional mirror for `/contributors.json` if needed.
|
||||||
- Contributors UI: `partials/contributors-block.html`; credits page: `credits.html` + `partials/credits-content.html`.
|
- Contributors UI: `partials/contributors-block.html`; credits page: `credits.html` + `partials/credits-content.html`.
|
||||||
|
|
||||||
|
## Catalog URLs
|
||||||
|
|
||||||
|
| URL | View |
|
||||||
|
|-----|------|
|
||||||
|
| `/catalog/?subject=math` | List |
|
||||||
|
| `/catalog/?subject=math&view=tree` | Mermaid map |
|
||||||
|
| `/catalog/?subject=math&view=compare` | Math curricula compare (HTML partial) |
|
||||||
|
|
||||||
|
Catalog build/data/Mermaid details: [zola-catalog.md](zola-catalog.md).
|
||||||
|
|
||||||
## Social icons
|
## Social icons
|
||||||
|
|
||||||
1. Source SVGs: `static/icons/social/{kind}.svg` (from [Tabler Icons](https://tabler.io/icons) outline set).
|
1. Source SVGs: `static/icons/social/{kind}.svg` (from [Tabler Icons](https://tabler.io/icons) outline set).
|
||||||
|
|
|
||||||
96
.agents/skills/zola/zola-catalog.md
Normal file
96
.agents/skills/zola/zola-catalog.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Catalog page (Zola + Solid + Mermaid)
|
||||||
|
|
||||||
|
Read this when editing `/catalog/`, `templates/catalog.html`, `sass/_catalog.scss`, or `frontend/src/**/catalog-*`.
|
||||||
|
|
||||||
|
## Zola side
|
||||||
|
|
||||||
|
| Piece | Path |
|
||||||
|
|-------|------|
|
||||||
|
| Page | `content/catalog.md` → `template = "catalog.html"` |
|
||||||
|
| Shell | `themes/openfreebooks/templates/catalog.html` |
|
||||||
|
| Compare doc (math only) | `templates/partials/math-curricula-compare.html` — embedded in `<template id="catalog-compare-template">` |
|
||||||
|
| Build-time JSON | `#catalog-data` — merged subject curricula from `data/{subject}-curriculum.json` + `data/catalog.json` |
|
||||||
|
| Styles | `themes/openfreebooks/sass/_catalog.scss` |
|
||||||
|
|
||||||
|
**Tera:** never bind curriculum JSON to a variable named `math` (reserved). Use `math_catalog`, etc.
|
||||||
|
|
||||||
|
## URL and views
|
||||||
|
|
||||||
|
| View | `view` param | UI |
|
||||||
|
|------|----------------|-----|
|
||||||
|
| List (default) | (omit) | Strand sections + `CatalogChapterCard` |
|
||||||
|
| Map | `tree` | Lazy `CatalogMermaidView` — DAG from `graph.edges` |
|
||||||
|
| Compare | `compare` | Math only — `CatalogCompareDoc` clones compare partial HTML |
|
||||||
|
|
||||||
|
Example: `/catalog/?subject=math&view=tree`
|
||||||
|
|
||||||
|
Curriculum filter chips come from `data/catalog.json` → `curriculums[]`. Math chapters use `curriculumCoverage` in `math-curriculum.json` (`core` / `extension` / `related` per framework). Compare matrix uses all roles; list/map badges show framework labels only.
|
||||||
|
|
||||||
|
## Solid / Vite
|
||||||
|
|
||||||
|
| Piece | Path |
|
||||||
|
|-------|------|
|
||||||
|
| App shell | `frontend/src/components/catalog-app.tsx` |
|
||||||
|
| List card | `frontend/src/components/catalog-chapter-card.tsx` |
|
||||||
|
| Compare mount | `frontend/src/components/catalog-compare-doc.tsx` |
|
||||||
|
| Map viewer | `frontend/src/components/catalog-mermaid-view.tsx` (lazy chunk `catalog-mermaid-view-*.js`) |
|
||||||
|
| Mermaid source | `frontend/src/lib/catalog-to-mermaid.ts` |
|
||||||
|
| Theme (hex) | `frontend/src/lib/catalog-mermaid-theme.ts` |
|
||||||
|
| Strand header chrome | `frontend/src/lib/catalog-mermaid-strand-headers.ts` |
|
||||||
|
| Light/dark mode | `frontend/src/lib/catalog-canvas-theme.ts` (`siteCanvasTheme`, `watchSiteTheme`) |
|
||||||
|
| Badge classes | `frontend/src/lib/catalog-badge.ts` |
|
||||||
|
| Types | `data/catalog.types.ts` (`CatalogViewMode`: `"linear" \| "tree" \| "compare"`) |
|
||||||
|
|
||||||
|
After any catalog frontend change: `bun run build:js` then hard-refresh.
|
||||||
|
|
||||||
|
## Mermaid map — theming (important)
|
||||||
|
|
||||||
|
Mermaid writes **inline `fill` on SVG nodes**. CSS `fill` on `.cluster rect` does **not** override themed colors.
|
||||||
|
|
||||||
|
| Rule | Detail |
|
||||||
|
|------|--------|
|
||||||
|
| Custom colors | Only with `theme: "base"` + `themeVariables` (hex). Other themes (`default`, `dark`, …) are not customizable via variables. |
|
||||||
|
| Color format | Hex only (`#121110`). Named colors (`red`) are ignored by the theming engine. |
|
||||||
|
| Site-wide init | `mermaid.initialize({ …mermaidInitializeOptions(mode) })` in `catalog-mermaid-view.tsx` |
|
||||||
|
| Strand subgraph body | `clusterBkg` in `mermaidThemeVariables()` |
|
||||||
|
| Strand title band | `secondaryColor` — also applied to overlay header rect in `enhanceStrandClusterHeaders()` |
|
||||||
|
| Borders / edges | `clusterBorder`, `lineColor`, `nodeBorder`, etc. |
|
||||||
|
|
||||||
|
Do **not** try to drive strand cluster fills from Sass variables on `.cluster rect`; keep SCSS for typography, label chrome, and map chrome (pan/zoom, card links).
|
||||||
|
|
||||||
|
### Strand subgraph titles
|
||||||
|
|
||||||
|
Mermaid puts subgraph titles on the cluster border. Post-render:
|
||||||
|
|
||||||
|
1. `enhanceStrandClusterHeaders(svg, mode)` finds `g.cluster` ids containing `strand_`
|
||||||
|
2. Inserts header `rect` + rule `line` at `STRAND_HEADER_HEIGHT` (44px)
|
||||||
|
3. Repositions `foreignObject` label into the header band
|
||||||
|
4. Sets `rx="0"` on cluster frame (square corners)
|
||||||
|
|
||||||
|
Diagram source uses HTML in subgraph titles, e.g. `<header class='catalog-mermaid-strand__header'>…</header>` — see `catalog-to-mermaid.ts`.
|
||||||
|
|
||||||
|
### Map chapter cards
|
||||||
|
|
||||||
|
List and map share card markup via `CatalogChapterCard`. In the map, only the **title** is a link for live chapters; planned chapters are non-clickable.
|
||||||
|
|
||||||
|
## Sass notes (`_catalog.scss`)
|
||||||
|
|
||||||
|
- Curriculum badge tokens and dark-mode contrast live here.
|
||||||
|
- Map card title underline: under `.catalog-mermaid-view` / `.catalog-canvas` legacy class names if present.
|
||||||
|
- Strand label styling: `.catalog-mermaid-strand__header`, `.catalog-mermaid-strand-label-fo` — not cluster `fill`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test frontend/src/lib/catalog-to-mermaid.test.ts
|
||||||
|
bun test frontend/src/lib/catalog-mermaid-theme.test.ts
|
||||||
|
bun test frontend/src/lib/catalog-mermaid-strand-headers.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build:js && zola serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `/catalog/?subject=math`, toggle List / Map / Compare (math), and switch site light/dark — strand header band should differ from subgraph body in both modes.
|
||||||
|
|
@ -48,7 +48,8 @@ Prefer **bootstrap** components that `querySelector` existing markup over re-ren
|
||||||
|
|
||||||
- `outDir`: `themes/openfreebooks/static`
|
- `outDir`: `themes/openfreebooks/static`
|
||||||
- `emptyOutDir: false` — do not wipe theme static (only add `js/bundle.js`)
|
- `emptyOutDir: false` — do not wipe theme static (only add `js/bundle.js`)
|
||||||
- Single entry: `frontend/src/main.tsx` → `js/bundle.js`
|
- Entry: `frontend/src/main.tsx` → `js/bundle.js`
|
||||||
|
- Lazy chunks: e.g. `import("./catalog-mermaid-view")` → `js/catalog-mermaid-view-[hash].js`
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
|
|
@ -57,6 +58,20 @@ Prefer **bootstrap** components that `querySelector` existing markup over re-ren
|
||||||
3. Hard-refresh browser.
|
3. Hard-refresh browser.
|
||||||
4. For production: `bun run build` then `wrangler deploy`.
|
4. For production: `bun run build` then `wrangler deploy`.
|
||||||
|
|
||||||
|
## Catalog (Solid + Mermaid)
|
||||||
|
|
||||||
|
Mounted from `catalog.html` via `catalog-app.tsx` (in main bundle). Map view lazy-loads `catalog-mermaid-view.tsx`.
|
||||||
|
|
||||||
|
| Concern | Module |
|
||||||
|
|---------|--------|
|
||||||
|
| Diagram source | `lib/catalog-to-mermaid.ts` |
|
||||||
|
| Mermaid init / hex theme | `lib/catalog-mermaid-theme.ts` → `mermaidInitializeOptions(mode)` |
|
||||||
|
| Strand title band overlay | `lib/catalog-mermaid-strand-headers.ts` |
|
||||||
|
| Light/dark for map | `lib/catalog-canvas-theme.ts` |
|
||||||
|
| Compare tab HTML | `catalog-compare-doc.tsx` reads `#catalog-compare-template` |
|
||||||
|
|
||||||
|
**Mermaid theming:** use `theme: "base"` and hex `themeVariables` only; CSS cannot override Mermaid’s inline cluster fills. See [zola-catalog.md](zola-catalog.md).
|
||||||
|
|
||||||
## KaTeX (math)
|
## KaTeX (math)
|
||||||
|
|
||||||
| Use case | API | Where |
|
| Use case | API | Where |
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@ Thank you for helping grow free, open textbooks.
|
||||||
## How the catalog fits together
|
## How the catalog fits together
|
||||||
|
|
||||||
```text
|
```text
|
||||||
data/{subject}-curriculum.json → catalog list + map (status, curriculums, graph.edges)
|
data/{subject}-curriculum.json → catalog list + map (status, curriculumCoverage, graph.edges)
|
||||||
content/{subject}/{slug}/ → everything for the live page (one folder)
|
content/{subject}/{slug}/ → everything for the live page (one folder)
|
||||||
_index.md, core.html, supplement.html, assets/, widgets/, chapter.scss
|
_index.md, core.html, supplement.html, assets/, widgets/, chapter.scss
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Catalog** (`/catalog/`) — list and map (`?view=tree`). Curriculum names (DSE, IB, …) appear only in the catalog — not inside chapter prose ([spec.md](spec.md)).
|
- **Catalog** (`/catalog/`) — list, map (`?view=tree`), and for Mathematics a Compare doc (`?view=compare`, HTML partial `themes/openfreebooks/templates/partials/math-curricula-compare.html`). Curriculum names (DSE, IB, …) appear only in the catalog — not inside chapter prose ([spec.md](spec.md)).
|
||||||
- Every chapter needs a **`description`** in curriculum JSON (map clamps to three lines).
|
- Every chapter needs a **`description`** in curriculum JSON (map clamps to three lines).
|
||||||
- **`bun run sync:chapters`** (also run automatically by `bun run build:js`) merges `core.html` + `supplement.html` into `_index.md` for Zola. Edit the HTML files, not the merged body in `_index.md`.
|
- **`bun run sync:chapters`** (also run automatically by `bun run build:js`) merges `core.html` + `supplement.html` into `_index.md` for Zola. Edit the HTML files, not the merged body in `_index.md`.
|
||||||
|
|
||||||
|
|
@ -61,11 +61,17 @@ content/{subject}/{slug}/ → everything for the live page (one folder)
|
||||||
"title": "Human-readable title",
|
"title": "Human-readable title",
|
||||||
"description": "One or two sentences for the catalog list and map card.",
|
"description": "One or two sentences for the catalog list and map card.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "related"
|
||||||
|
},
|
||||||
"tier": "non-foundation"
|
"tier": "non-foundation"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Each framework maps to `core`, `extension`, or `related` (Compare matrix). Use `extension` for elective / M2-style topics; `related` when the topic supports but is not a named unit.
|
||||||
|
|
||||||
3. Use **kebab-case** slugs. No `content/` folder needed for planned chapters.
|
3. Use **kebab-case** slugs. No `content/` folder needed for planned chapters.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -74,7 +80,7 @@ content/{subject}/{slug}/ → everything for the live page (one folder)
|
||||||
|
|
||||||
### 1. Catalog entry
|
### 1. Catalog entry
|
||||||
|
|
||||||
In `data/{subject}-curriculum.json`, set `"status": "live"` and include `description`, `curriculums`, and optional `tier`.
|
In `data/{subject}-curriculum.json`, set `"status": "live"` and include `description`, `curriculumCoverage`, and optional `tier`.
|
||||||
|
|
||||||
### 2. Chapter folder
|
### 2. Chapter folder
|
||||||
|
|
||||||
|
|
@ -187,7 +193,7 @@ Omit `banner` for subjects that should stay unchanged (e.g. roadmap-only placeho
|
||||||
## Add a curriculum label
|
## Add a curriculum label
|
||||||
|
|
||||||
1. Add the label to `curriculums` in `data/catalog.json`.
|
1. Add the label to `curriculums` in `data/catalog.json`.
|
||||||
2. Tag chapters in their `curriculums` arrays.
|
2. Add roles per chapter in `curriculumCoverage` (and update Compare if needed).
|
||||||
3. Add badge style in `themes/openfreebooks/sass/_catalog.scss` and mapping in `frontend/src/lib/catalog-badge.ts`.
|
3. Add badge style in `themes/openfreebooks/sass/_catalog.scss` and mapping in `frontend/src/lib/catalog-badge.ts`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"title": "Curriculum Catalog",
|
"title": "Curriculum Catalog",
|
||||||
"subtitle": "Browse chapters by subject and curriculum",
|
"subtitle": "Browse chapters by subject and curriculum",
|
||||||
"curriculums": ["Foundation", "DSE", "IB", "A-Level", "AP", "IGCSE"],
|
"curriculums": ["Foundation", "DSE", "IB", "A-Level", "AP", "IGCSE", "Common Core"],
|
||||||
"subjects": [
|
"subjects": [
|
||||||
{ "id": "math", "name": "Mathematics", "banner": "/catalog/banners/math.webp" },
|
{ "id": "math", "name": "Mathematics", "banner": "/catalog/banners/math.webp" },
|
||||||
{ "id": "science", "name": "Science" },
|
{ "id": "science", "name": "Science" },
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
export type CatalogChapterTier = "foundation" | "non-foundation";
|
export type CatalogChapterTier = "foundation" | "non-foundation";
|
||||||
|
|
||||||
|
/** Compare matrix + filter relevance per framework label. */
|
||||||
|
export type CurriculumCoverageRole = "core" | "extension" | "related";
|
||||||
|
|
||||||
|
export type CurriculumCoverage = Partial<Record<string, CurriculumCoverageRole>>;
|
||||||
|
|
||||||
export type CatalogChapter = {
|
export type CatalogChapter = {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
status: "live" | "planned";
|
status: "live" | "planned";
|
||||||
curriculums: string[];
|
/** @deprecated Prefer curriculumCoverage; kept for older JSON during migration. */
|
||||||
|
curriculums?: string[];
|
||||||
|
/** Per-framework role for Compare matrix and catalog filters (keys = framework labels). */
|
||||||
|
curriculumCoverage?: CurriculumCoverage;
|
||||||
/** DSE compulsory tier; omit = foundation. */
|
/** DSE compulsory tier; omit = foundation. */
|
||||||
tier?: CatalogChapterTier;
|
tier?: CatalogChapterTier;
|
||||||
};
|
};
|
||||||
|
|
@ -42,4 +50,4 @@ export type CatalogData = {
|
||||||
subjects: CatalogSubject[];
|
subjects: CatalogSubject[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CatalogViewMode = "linear" | "tree";
|
export type CatalogViewMode = "linear" | "tree" | "compare";
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
"type": "standard",
|
"type": "standard",
|
||||||
"question": "What are curriculum labels on the catalog?",
|
"question": "What are curriculum labels on the catalog?",
|
||||||
"answer": [
|
"answer": [
|
||||||
"Each subject in the catalog has a set of curricula you can filter by — exam boards, school programs, and similar labels — so you can see which chapters fit your syllabus.",
|
"Each subject in the catalog has a set of curricula you can filter by — exam boards, school programs, and similar labels — so you can see which chapters fit your syllabus. For Mathematics, the Compare tab documents how frameworks differ and how chapters map to them.",
|
||||||
"We try to keep chapter structure and prerequisite maps compatible with as many curricula as possible. We do our best, but coverage is not perfect; the catalog will grow and improve as more people contribute.",
|
"We try to keep chapter structure and prerequisite maps compatible with as many curricula as possible. We do our best, but coverage is not perfect; the catalog will grow and improve as more people contribute.",
|
||||||
"Those labels appear only in the catalog (badges and filters), not inside chapter pages. You choose what applies to you on the catalog; inside a chapter, the focus stays on the topic itself."
|
"Those labels appear only in the catalog (badges and filters), not inside chapter pages. You choose what applies to you on the catalog; inside a chapter, the focus stays on the topic itself."
|
||||||
]
|
]
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
"type": "standard",
|
"type": "standard",
|
||||||
"question": "How do I find a chapter?",
|
"question": "How do I find a chapter?",
|
||||||
"answer": [
|
"answer": [
|
||||||
"Open the catalog for a subject — list view by default, or map view to follow prerequisites. Use curriculum filters if you need to narrow the list.",
|
"Open the catalog for a subject — list view by default, map view for prerequisites, or (for Mathematics) Compare for how popular curricula relate to our chapters. Use curriculum filters on list and map if you need to narrow the list.",
|
||||||
"Press ⌘K or Ctrl+K (or open Search in the header) to search live chapter pages. The dedicated search page indexes chapter content, not every site page."
|
"Press ⌘K or Ctrl+K (or open Search in the header) to search live chapter pages. The dedicated search page indexes chapter content, not every site page."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,204 @@
|
||||||
{
|
{
|
||||||
"title": "Mathematics",
|
"title": "Mathematics",
|
||||||
"subtitle": "HKDSE Compulsory Part (S4–S6)",
|
"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. The map shows required prerequisites; the list follows a readable teaching order.",
|
|
||||||
"graph": {
|
"graph": {
|
||||||
"edges": [
|
"edges": [
|
||||||
{ "from": "counting-and-numbers", "to": "basic-math-notation" },
|
{
|
||||||
{ "from": "basic-math-notation", "to": "properties-of-operations" },
|
"from": "counting-and-numbers",
|
||||||
{ "from": "properties-of-operations", "to": "written-calculation" },
|
"to": "basic-math-notation"
|
||||||
{ "from": "written-calculation", "to": "directed-numbers-rates" },
|
},
|
||||||
{ "from": "directed-numbers-rates", "to": "polynomials-expansion-factorisation" },
|
{
|
||||||
{ "from": "polynomials-expansion-factorisation", "to": "algebraic-fractions-formulas" },
|
"from": "basic-math-notation",
|
||||||
{ "from": "polynomials-expansion-factorisation", "to": "linear-equations-two-unknowns" },
|
"to": "properties-of-operations"
|
||||||
{ "from": "linear-equations-two-unknowns", "to": "linear-inequalities-one-unknown" },
|
},
|
||||||
{ "from": "polynomials-expansion-factorisation", "to": "laws-of-indices" },
|
{
|
||||||
{ "from": "laws-of-indices", "to": "quadratic-equations" },
|
"from": "properties-of-operations",
|
||||||
{ "from": "polynomials-expansion-factorisation", "to": "factor-remainder-theorems" },
|
"to": "written-calculation"
|
||||||
{ "from": "quadratic-equations", "to": "factor-remainder-theorems" },
|
},
|
||||||
{ "from": "quadratic-equations", "to": "complex-numbers" },
|
{
|
||||||
{ "from": "quadratic-equations", "to": "functions-graphs" },
|
"from": "written-calculation",
|
||||||
{ "from": "quadratic-equations", "to": "sequences-series" },
|
"to": "integer-foundations"
|
||||||
{ "from": "functions-graphs", "to": "exponential-logarithmic" },
|
},
|
||||||
{ "from": "functions-graphs", "to": "variations" },
|
{
|
||||||
{ "from": "functions-graphs", "to": "sequences-series" },
|
"from": "written-calculation",
|
||||||
{ "from": "functions-graphs", "to": "linear-programming" },
|
"to": "rational-number-operations"
|
||||||
{ "from": "sequences-series", "to": "linear-programming" },
|
},
|
||||||
{ "from": "polynomials-expansion-factorisation", "to": "coordinate-geometry-foundations" },
|
{
|
||||||
{ "from": "quadratic-equations", "to": "coordinate-circles" },
|
"from": "integer-foundations",
|
||||||
{ "from": "functions-graphs", "to": "advanced-trigonometry" },
|
"to": "integer-arithmetic"
|
||||||
{ "from": "coordinate-geometry-foundations", "to": "deductive-geometry" },
|
},
|
||||||
{ "from": "coordinate-geometry-foundations", "to": "coordinate-circles" },
|
{
|
||||||
{ "from": "deductive-geometry", "to": "congruence-similarity" },
|
"from": "rational-number-operations",
|
||||||
{ "from": "congruence-similarity", "to": "pythagoras-theorem" },
|
"to": "percentage-applications"
|
||||||
{ "from": "pythagoras-theorem", "to": "introduction-trigonometry" },
|
},
|
||||||
{ "from": "introduction-trigonometry", "to": "mensuration-3d" },
|
{
|
||||||
{ "from": "introduction-trigonometry", "to": "advanced-trigonometry" },
|
"from": "rational-number-operations",
|
||||||
{ "from": "coordinate-circles", "to": "circle-properties" },
|
"to": "ratios-and-rates-foundation"
|
||||||
{ "from": "deductive-geometry", "to": "circle-properties" },
|
},
|
||||||
{ "from": "coordinate-circles", "to": "loci" },
|
{
|
||||||
{ "from": "circle-properties", "to": "loci" },
|
"from": "percentage-applications",
|
||||||
{ "from": "directed-numbers-rates", "to": "statistical-charts-averages" },
|
"to": "ratios-and-rates-foundation"
|
||||||
{ "from": "statistical-charts-averages", "to": "measures-dispersion" },
|
},
|
||||||
{ "from": "statistical-charts-averages", "to": "basic-probability" },
|
{
|
||||||
{ "from": "basic-probability", "to": "permutations-combinations-probability" },
|
"from": "integer-arithmetic",
|
||||||
{ "from": "measures-dispersion", "to": "normal-distribution-z-scores" },
|
"to": "directed-numbers-rates"
|
||||||
{ "from": "measures-dispersion", "to": "statistical-literacy" },
|
},
|
||||||
{ "from": "basic-probability", "to": "statistical-literacy" },
|
{
|
||||||
{ "from": "permutations-combinations-probability", "to": "statistical-literacy" }
|
"from": "ratios-and-rates-foundation",
|
||||||
|
"to": "directed-numbers-rates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "directed-numbers-rates",
|
||||||
|
"to": "polynomials-expansion-factorisation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "polynomials-expansion-factorisation",
|
||||||
|
"to": "algebraic-fractions-formulas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "polynomials-expansion-factorisation",
|
||||||
|
"to": "linear-equations-two-unknowns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "linear-equations-two-unknowns",
|
||||||
|
"to": "linear-inequalities-one-unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "polynomials-expansion-factorisation",
|
||||||
|
"to": "laws-of-indices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "laws-of-indices",
|
||||||
|
"to": "quadratic-equations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "polynomials-expansion-factorisation",
|
||||||
|
"to": "factor-remainder-theorems"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "quadratic-equations",
|
||||||
|
"to": "factor-remainder-theorems"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "quadratic-equations",
|
||||||
|
"to": "complex-numbers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "quadratic-equations",
|
||||||
|
"to": "functions-graphs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "quadratic-equations",
|
||||||
|
"to": "sequences-series"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "functions-graphs",
|
||||||
|
"to": "exponential-logarithmic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "functions-graphs",
|
||||||
|
"to": "variations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "functions-graphs",
|
||||||
|
"to": "sequences-series"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "functions-graphs",
|
||||||
|
"to": "linear-programming"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "sequences-series",
|
||||||
|
"to": "linear-programming"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "polynomials-expansion-factorisation",
|
||||||
|
"to": "coordinate-geometry-foundations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "quadratic-equations",
|
||||||
|
"to": "coordinate-circles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "functions-graphs",
|
||||||
|
"to": "advanced-trigonometry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "coordinate-geometry-foundations",
|
||||||
|
"to": "deductive-geometry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "coordinate-geometry-foundations",
|
||||||
|
"to": "coordinate-circles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "deductive-geometry",
|
||||||
|
"to": "congruence-similarity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "congruence-similarity",
|
||||||
|
"to": "pythagoras-theorem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "pythagoras-theorem",
|
||||||
|
"to": "introduction-trigonometry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "introduction-trigonometry",
|
||||||
|
"to": "mensuration-3d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "introduction-trigonometry",
|
||||||
|
"to": "advanced-trigonometry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "coordinate-circles",
|
||||||
|
"to": "circle-properties"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "deductive-geometry",
|
||||||
|
"to": "circle-properties"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "coordinate-circles",
|
||||||
|
"to": "loci"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "circle-properties",
|
||||||
|
"to": "loci"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "directed-numbers-rates",
|
||||||
|
"to": "statistical-charts-averages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "statistical-charts-averages",
|
||||||
|
"to": "measures-dispersion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "statistical-charts-averages",
|
||||||
|
"to": "basic-probability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "basic-probability",
|
||||||
|
"to": "permutations-combinations-probability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "measures-dispersion",
|
||||||
|
"to": "normal-distribution-z-scores"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "measures-dispersion",
|
||||||
|
"to": "statistical-literacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "basic-probability",
|
||||||
|
"to": "statistical-literacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "permutations-combinations-probability",
|
||||||
|
"to": "statistical-literacy"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"strands": [
|
"strands": [
|
||||||
|
|
@ -58,130 +211,400 @@
|
||||||
"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 from zero, spot patterns, add, multiply, and divide; then meet place value and big numbers from hundreds to billions.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["Foundation"]
|
"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": "basic-math-notation",
|
"slug": "basic-math-notation",
|
||||||
"title": "Basic math notation",
|
"title": "Basic math notation",
|
||||||
"description": "Learn the names and meanings of plus, minus, times, divide, equals, brackets, and other signs used in number sentences.",
|
"description": "Learn the names and meanings of plus, minus, times, divide, equals, brackets, and other signs used in number sentences.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["Foundation"]
|
"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": "properties-of-operations",
|
"slug": "properties-of-operations",
|
||||||
"title": "Properties of operations",
|
"title": "Properties of operations",
|
||||||
"description": "See why order, grouping, and distribution can change how you write a sum or product without changing the result — with interactive area and dot models.",
|
"description": "See why order, grouping, and distribution can change how you write a sum or product without changing the result — with interactive area and dot models.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["Foundation"]
|
"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": "written-calculation",
|
"slug": "written-calculation",
|
||||||
"title": "Written calculation",
|
"title": "Written calculation",
|
||||||
"description": "Add, subtract, multiply, and divide whole numbers using vertical written form: align columns, regroup, and read the result.",
|
"description": "Add, subtract, multiply, and divide whole numbers using vertical written form: align columns, regroup, and read the result.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["Foundation"]
|
"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": "integer-foundations",
|
||||||
|
"title": "Integer foundations",
|
||||||
|
"description": "Model directed magnitude on number lines; use the zero principle and tiles to add and subtract integers, and see subtraction as adding an opposite.",
|
||||||
|
"status": "planned",
|
||||||
|
"curriculumCoverage": {
|
||||||
|
"Foundation": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"DSE": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "integer-arithmetic",
|
||||||
|
"title": "Integer arithmetic",
|
||||||
|
"description": "Multiply and divide integers from models to symbolic rules; apply order of operations with signed numbers in multi-step expressions.",
|
||||||
|
"status": "planned",
|
||||||
|
"curriculumCoverage": {
|
||||||
|
"Foundation": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"DSE": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "rational-number-operations",
|
||||||
|
"title": "Fractions and decimals",
|
||||||
|
"description": "Convert between fractions, terminating decimals, and repeating decimals; add, subtract, multiply, and divide rational numbers and mixed numbers.",
|
||||||
|
"status": "planned",
|
||||||
|
"curriculumCoverage": {
|
||||||
|
"Foundation": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"DSE": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "percentage-applications",
|
||||||
|
"title": "Percentage applications",
|
||||||
|
"description": "Relate fractions and decimals to base-100 ratios; calculate percent increase and decrease in financial and real-world contexts.",
|
||||||
|
"status": "planned",
|
||||||
|
"curriculumCoverage": {
|
||||||
|
"Foundation": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"DSE": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "ratios-and-rates-foundation",
|
||||||
|
"title": "Ratios & rates foundations",
|
||||||
|
"description": "Formulate part-to-part and part-to-whole ratios, find equivalent ratios, compute unit rates, and graph proportional relationships.",
|
||||||
|
"status": "planned",
|
||||||
|
"curriculumCoverage": {
|
||||||
|
"Foundation": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"DSE": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "directed-numbers-rates",
|
"slug": "directed-numbers-rates",
|
||||||
"title": "Directed numbers, rates & ratios",
|
"title": "Directed numbers, rates & ratios",
|
||||||
"description": "Operate with negative numbers and solve ratio, rate, and proportion problems in context.",
|
"description": "Operate with negative numbers and solve ratio, rate, and proportion problems in context.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Foundation": "related",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "polynomials-expansion-factorisation",
|
"slug": "polynomials-expansion-factorisation",
|
||||||
"title": "Polynomials: expansion & factorisation",
|
"title": "Polynomials: expansion & factorisation",
|
||||||
"description": "Expand products, collect like terms, and factorise expressions using common factors and identities.",
|
"description": "Expand products, collect like terms, and factorise expressions using common factors and identities.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"Foundation": "related",
|
||||||
|
"AP": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "algebraic-fractions-formulas",
|
"slug": "algebraic-fractions-formulas",
|
||||||
"title": "Algebraic fractions & formulas",
|
"title": "Algebraic fractions & formulas",
|
||||||
"description": "Simplify algebraic fractions and change the subject of formulas in applied contexts.",
|
"description": "Simplify algebraic fractions and change the subject of formulas in applied contexts.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "related",
|
||||||
|
"Singapore": "related",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"Foundation": "related",
|
||||||
|
"AP": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "linear-equations-two-unknowns",
|
"slug": "linear-equations-two-unknowns",
|
||||||
"title": "Linear equations in two unknowns",
|
"title": "Linear equations in two unknowns",
|
||||||
"description": "Solve simultaneous linear equations by substitution, elimination, and graphical interpretation; classify unique, parallel, and coincident cases.",
|
"description": "Solve simultaneous linear equations by substitution, elimination, and graphical interpretation; classify unique, parallel, and coincident cases.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"AP": "related",
|
||||||
|
"Foundation": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "linear-inequalities-one-unknown",
|
"slug": "linear-inequalities-one-unknown",
|
||||||
"title": "Linear inequalities in one unknown",
|
"title": "Linear inequalities in one unknown",
|
||||||
"description": "Solve and represent linear inequalities on the number line and in word problems.",
|
"description": "Solve and represent linear inequalities on the number line and in word problems.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"Singapore": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "laws-of-indices",
|
"slug": "laws-of-indices",
|
||||||
"title": "Laws of indices",
|
"title": "Laws of indices",
|
||||||
"description": "Apply index laws to simplify expressions with integer and rational exponents.",
|
"description": "Apply index laws to simplify expressions with integer and rational exponents.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"AP": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"Foundation": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "quadratic-equations",
|
"slug": "quadratic-equations",
|
||||||
"title": "Quadratic equations",
|
"title": "Quadratic equations",
|
||||||
"description": "Solve quadratics by factorising, completing the square, and the formula. Model real problems with parabolas and roots.",
|
"description": "Solve quadratics by factorising, completing the square, and the formula. Model real problems with parabolas and roots.",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"AP": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"Foundation": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "factor-remainder-theorems",
|
"slug": "factor-remainder-theorems",
|
||||||
"title": "Factor & remainder theorems",
|
"title": "Factor & remainder theorems",
|
||||||
"description": "Use the factor and remainder theorems to factorise higher-degree polynomials and solve related equations.",
|
"description": "Use the factor and remainder theorems to factorise higher-degree polynomials and solve related equations.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IB": "extension",
|
||||||
|
"A-Level": "extension",
|
||||||
|
"IGCSE": "related",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"AP": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "complex-numbers",
|
"slug": "complex-numbers",
|
||||||
"title": "Complex numbers",
|
"title": "Complex numbers",
|
||||||
"description": "Represent complex numbers in Cartesian form and perform operations including conjugates and division.",
|
"description": "Represent complex numbers in Cartesian form and perform operations including conjugates and division.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IB": "extension",
|
||||||
|
"A-Level": "extension",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"IGCSE": "related",
|
||||||
|
"AP": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "functions-graphs",
|
"slug": "functions-graphs",
|
||||||
"title": "Functions & graphs",
|
"title": "Functions & graphs",
|
||||||
"description": "Study domains, ranges, transformations, and graphs of common functions including quadratics and rationals.",
|
"description": "Study domains, ranges, transformations, and graphs of common functions including quadratics and rationals.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"AP": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"Foundation": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "exponential-logarithmic",
|
"slug": "exponential-logarithmic",
|
||||||
"title": "Exponential & logarithmic functions",
|
"title": "Exponential & logarithmic functions",
|
||||||
"description": "Explore laws of logarithms, exponential growth and decay, and sketch related graphs.",
|
"description": "Explore laws of logarithms, exponential growth and decay, and sketch related graphs.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"AP": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"Singapore": "related",
|
||||||
|
"Foundation": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "sequences-series",
|
"slug": "sequences-series",
|
||||||
"title": "Sequences & series",
|
"title": "Sequences & series",
|
||||||
"description": "Work with arithmetic and geometric sequences, sigma notation, and finite summation formulas.",
|
"description": "Work with arithmetic and geometric sequences, sigma notation, and finite summation formulas.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "related",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"AP": "related",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"Singapore": "related",
|
||||||
|
"Foundation": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "variations",
|
"slug": "variations",
|
||||||
"title": "Variations",
|
"title": "Variations",
|
||||||
"description": "Model direct, inverse, joint, and partial variation and solve combined variation problems.",
|
"description": "Model direct, inverse, joint, and partial variation and solve combined variation problems.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"IGCSE": "related",
|
||||||
|
"Common Core": "related",
|
||||||
|
"Singapore": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "linear-programming",
|
"slug": "linear-programming",
|
||||||
"title": "Linear programming",
|
"title": "Linear programming",
|
||||||
"description": "Graph linear inequalities, identify feasible regions, and optimise objective functions in two variables.",
|
"description": "Graph linear inequalities, identify feasible regions, and optimise objective functions in two variables.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IB": "extension",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"IGCSE": "related"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -194,73 +617,162 @@
|
||||||
"title": "Coordinate geometry foundations",
|
"title": "Coordinate geometry foundations",
|
||||||
"description": "Plot points, find distances and midpoints, and work with slopes of lines in the Cartesian plane.",
|
"description": "Plot points, find distances and midpoints, and work with slopes of lines in the Cartesian plane.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Singapore": "core",
|
||||||
|
"AP": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "deductive-geometry",
|
"slug": "deductive-geometry",
|
||||||
"title": "Deductive geometry",
|
"title": "Deductive geometry",
|
||||||
"description": "Prove angle and side relationships for intersecting lines, triangles, and polygons.",
|
"description": "Prove angle and side relationships for intersecting lines, triangles, and polygons.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "congruence-similarity",
|
"slug": "congruence-similarity",
|
||||||
"title": "Congruence & similarity",
|
"title": "Congruence & similarity",
|
||||||
"description": "Apply congruence and similarity criteria to justify properties of triangles and scale figures.",
|
"description": "Apply congruence and similarity criteria to justify properties of triangles and scale figures.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "pythagoras-theorem",
|
"slug": "pythagoras-theorem",
|
||||||
"title": "Pythagoras' theorem",
|
"title": "Pythagoras' theorem",
|
||||||
"description": "Use Pythagoras' theorem and its converse in 2D problems and coordinate settings.",
|
"description": "Use Pythagoras' theorem and its converse in 2D problems and coordinate settings.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"AP": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "introduction-trigonometry",
|
"slug": "introduction-trigonometry",
|
||||||
"title": "Introduction to trigonometry",
|
"title": "Introduction to trigonometry",
|
||||||
"description": "Define sine, cosine, and tangent ratios and solve right-angled triangle problems.",
|
"description": "Define sine, cosine, and tangent ratios and solve right-angled triangle problems.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"AP": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "mensuration-3d",
|
"slug": "mensuration-3d",
|
||||||
"title": "Mensuration in 3D",
|
"title": "Mensuration in 3D",
|
||||||
"description": "Find volumes and surface areas of prisms, cylinders, pyramids, cones, and spheres.",
|
"description": "Find volumes and surface areas of prisms, cylinders, pyramids, cones, and spheres.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "coordinate-circles",
|
"slug": "coordinate-circles",
|
||||||
"title": "Coordinate geometry of circles",
|
"title": "Coordinate geometry of circles",
|
||||||
"description": "Find equations of circles, tangents, and intersections using coordinate methods and distance.",
|
"description": "Find equations of circles, tangents, and intersections using coordinate methods and distance.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "related",
|
||||||
|
"IB": "extension",
|
||||||
|
"A-Level": "extension",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"UK GCSE": "related",
|
||||||
|
"AP": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "advanced-trigonometry",
|
"slug": "advanced-trigonometry",
|
||||||
"title": "Advanced trigonometry",
|
"title": "Advanced trigonometry",
|
||||||
"description": "Use sine and cosine rules, radians, and trigonometric graphs in 2D and 3D applications.",
|
"description": "Use sine and cosine rules, radians, and trigonometric graphs in 2D and 3D applications.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"AP": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Gaokao": "core",
|
||||||
|
"Common Core": "related",
|
||||||
|
"Singapore": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "circle-properties",
|
"slug": "circle-properties",
|
||||||
"title": "Properties of circles",
|
"title": "Properties of circles",
|
||||||
"description": "Apply chord, tangent, and angle theorems in circles to prove results and solve geometry problems.",
|
"description": "Apply chord, tangent, and angle theorems in circles to prove results and solve geometry problems.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"Singapore": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "loci",
|
"slug": "loci",
|
||||||
"title": "Loci",
|
"title": "Loci",
|
||||||
"description": "Describe and construct loci in the plane, linking geometric constraints to algebraic equations.",
|
"description": "Describe and construct loci in the plane, linking geometric constraints to algebraic equations.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IGCSE": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"IB": "related",
|
||||||
|
"UK GCSE": "related"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -273,44 +785,98 @@
|
||||||
"title": "Charts & averages",
|
"title": "Charts & averages",
|
||||||
"description": "Organise data with tables and charts and calculate mean, median, and mode.",
|
"description": "Organise data with tables and charts and calculate mean, median, and mode.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"AP": "related",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"A-Level": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "measures-dispersion",
|
"slug": "measures-dispersion",
|
||||||
"title": "Measures of dispersion",
|
"title": "Measures of dispersion",
|
||||||
"description": "Interpret range, interquartile range, variance, and standard deviation for comparing data sets.",
|
"description": "Interpret range, interquartile range, variance, and standard deviation for comparing data sets.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Singapore": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "basic-probability",
|
"slug": "basic-probability",
|
||||||
"title": "Basic probability",
|
"title": "Basic probability",
|
||||||
"description": "Calculate theoretical and experimental probabilities for simple events and complements.",
|
"description": "Calculate theoretical and experimental probabilities for simple events and complements.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IGCSE": "core",
|
||||||
|
"IB": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Singapore": "core",
|
||||||
|
"AP": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Gaokao": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "permutations-combinations-probability",
|
"slug": "permutations-combinations-probability",
|
||||||
"title": "Permutations, combinations & probability",
|
"title": "Permutations, combinations & probability",
|
||||||
"description": "Count outcomes with permutations and combinations, then compute probabilities in finite sample spaces.",
|
"description": "Count outcomes with permutations and combinations, then compute probabilities in finite sample spaces.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IGCSE": "extension",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"AP": "related",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"Common Core": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "normal-distribution-z-scores",
|
"slug": "normal-distribution-z-scores",
|
||||||
"title": "Normal distribution & z-scores",
|
"title": "Normal distribution & z-scores",
|
||||||
"description": "Model continuous data with the normal curve, standardise with z-scores, and read probabilities from tables.",
|
"description": "Model continuous data with the normal curve, standardise with z-scores, and read probabilities from tables.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"],
|
"tier": "non-foundation",
|
||||||
"tier": "non-foundation"
|
"curriculumCoverage": {
|
||||||
|
"DSE": "extension",
|
||||||
|
"IB": "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"AP": "related",
|
||||||
|
"IGCSE": "related",
|
||||||
|
"Gaokao": "related",
|
||||||
|
"Common Core": "related"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "statistical-literacy",
|
"slug": "statistical-literacy",
|
||||||
"title": "Statistical literacy",
|
"title": "Statistical literacy",
|
||||||
"description": "Evaluate claims in media and research: sampling bias, correlation versus causation, and misleading charts.",
|
"description": "Evaluate claims in media and research: sampling bias, correlation versus causation, and misleading charts.",
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"curriculums": ["DSE"]
|
"curriculumCoverage": {
|
||||||
|
"DSE": "core",
|
||||||
|
"IB": "related",
|
||||||
|
"Common Core": "related",
|
||||||
|
"IGCSE": "related",
|
||||||
|
"AP": "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
"Singapore": "related"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
docs/research/copilot.md
Normal file
37
docs/research/copilot.md
Normal file
File diff suppressed because one or more lines are too long
1381
docs/research/gemini.md
Normal file
1381
docs/research/gemini.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,9 @@
|
||||||
import { createMemo, createSignal, For, lazy, Show, Suspense } from "solid-js";
|
import { createMemo, createSignal, For, lazy, Show, Suspense } from "solid-js";
|
||||||
import { subjectToMermaid } from "../lib/catalog-to-mermaid";
|
import { subjectToMermaid } from "../lib/catalog-to-mermaid";
|
||||||
|
import { CatalogCompareDoc } from "./catalog-compare-doc";
|
||||||
import { CatalogChapterCard } from "./catalog-chapter-card";
|
import { CatalogChapterCard } from "./catalog-chapter-card";
|
||||||
import { CatalogFiltersMenu } from "./catalog-filters-menu";
|
import { CatalogFiltersMenu } from "./catalog-filters-menu";
|
||||||
|
import { chapterFilterCurriculums } from "../lib/catalog-coverage";
|
||||||
import type {
|
import type {
|
||||||
CatalogChapter,
|
CatalogChapter,
|
||||||
CatalogData,
|
CatalogData,
|
||||||
|
|
@ -14,10 +16,40 @@ const CatalogMermaidView = lazy(() =>
|
||||||
import("./catalog-mermaid-view").then((m) => ({ default: m.CatalogMermaidView }))
|
import("./catalog-mermaid-view").then((m) => ({ default: m.CatalogMermaidView }))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function normalizeChapter(chapter: CatalogChapter): CatalogChapter {
|
||||||
|
const coverage = chapter.curriculumCoverage;
|
||||||
|
if (coverage && typeof coverage === "object" && !Array.isArray(coverage)) {
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
const legacy = chapter.curriculums;
|
||||||
|
if (!legacy?.length) return { ...chapter, curriculumCoverage: {} };
|
||||||
|
const curriculumCoverage: CatalogChapter["curriculumCoverage"] = {};
|
||||||
|
for (const label of legacy) {
|
||||||
|
curriculumCoverage[label] = "core";
|
||||||
|
}
|
||||||
|
return { ...chapter, curriculumCoverage };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSubject(subject: CatalogSubject): CatalogSubject {
|
||||||
|
return {
|
||||||
|
...subject,
|
||||||
|
strands: (subject.strands ?? []).map((strand) => ({
|
||||||
|
...strand,
|
||||||
|
chapters: (strand.chapters ?? []).map(normalizeChapter),
|
||||||
|
})),
|
||||||
|
graph: subject.graph ?? { edges: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function readCatalogData(): CatalogData | null {
|
function readCatalogData(): CatalogData | null {
|
||||||
const el = document.getElementById("catalog-data");
|
const el = document.getElementById("catalog-data");
|
||||||
if (!el?.textContent) return null;
|
if (!el?.textContent) return null;
|
||||||
return JSON.parse(el.textContent) as CatalogData;
|
const raw = JSON.parse(el.textContent) as CatalogData;
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
curriculums: raw.curriculums ?? [],
|
||||||
|
subjects: (raw.subjects ?? []).map(normalizeSubject),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialSubjectId(subjects: CatalogSubject[]): string {
|
function initialSubjectId(subjects: CatalogSubject[]): string {
|
||||||
|
|
@ -31,17 +63,21 @@ function initialSubjectId(subjects: CatalogSubject[]): string {
|
||||||
|
|
||||||
function initialViewMode(): CatalogViewMode {
|
function initialViewMode(): CatalogViewMode {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
return params.get("view") === "tree" ? "tree" : "linear";
|
const view = params.get("view");
|
||||||
|
if (view === "tree") return "tree";
|
||||||
|
if (view === "compare") return "compare";
|
||||||
|
return "linear";
|
||||||
}
|
}
|
||||||
|
|
||||||
function chapterMatchesFilters(chapter: CatalogChapter, activeFilters: Set<string>): boolean {
|
function chapterMatchesFilters(chapter: CatalogChapter, activeFilters: Set<string>): boolean {
|
||||||
if (activeFilters.size === 0) return true;
|
if (activeFilters.size === 0) return true;
|
||||||
return chapter.curriculums.some((c) => activeFilters.has(c));
|
return chapterFilterCurriculums(chapter).some((c) => activeFilters.has(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
function countVisibleChapters(subject: CatalogSubject, activeFilters: Set<string>): number {
|
function countVisibleChapters(subject: CatalogSubject, activeFilters: Set<string>): number {
|
||||||
return subject.strands.reduce((total, strand) => {
|
return (subject.strands ?? []).reduce((total, strand) => {
|
||||||
return total + strand.chapters.filter((ch) => chapterMatchesFilters(ch, activeFilters)).length;
|
const visible = (strand.chapters ?? []).filter((ch) => chapterMatchesFilters(ch, activeFilters));
|
||||||
|
return total + visible.length;
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +86,8 @@ function syncUrl(subjectId: string, viewMode: CatalogViewMode) {
|
||||||
url.searchParams.set("subject", subjectId);
|
url.searchParams.set("subject", subjectId);
|
||||||
if (viewMode === "tree") {
|
if (viewMode === "tree") {
|
||||||
url.searchParams.set("view", "tree");
|
url.searchParams.set("view", "tree");
|
||||||
|
} else if (viewMode === "compare") {
|
||||||
|
url.searchParams.set("view", "compare");
|
||||||
} else {
|
} else {
|
||||||
url.searchParams.delete("view");
|
url.searchParams.delete("view");
|
||||||
}
|
}
|
||||||
|
|
@ -66,23 +104,36 @@ export function CatalogApp() {
|
||||||
const [activeFilters, setActiveFilters] = createSignal<Set<string>>(new Set());
|
const [activeFilters, setActiveFilters] = createSignal<Set<string>>(new Set());
|
||||||
const [viewMode, setViewMode] = createSignal<CatalogViewMode>(initialViewMode());
|
const [viewMode, setViewMode] = createSignal<CatalogViewMode>(initialViewMode());
|
||||||
|
|
||||||
const currentSubject = createMemo(
|
const currentSubject = createMemo(() => {
|
||||||
() => data.subjects.find((s) => s.id === currentSubjectId()) ?? data.subjects[0]
|
const subjects = data.subjects ?? [];
|
||||||
);
|
return subjects.find((s) => s.id === currentSubjectId()) ?? subjects[0];
|
||||||
|
});
|
||||||
|
|
||||||
const visibleCount = createMemo(() => countVisibleChapters(currentSubject(), activeFilters()));
|
const visibleCount = createMemo(() => {
|
||||||
|
const subject = currentSubject();
|
||||||
|
return subject ? countVisibleChapters(subject, activeFilters()) : 0;
|
||||||
|
});
|
||||||
|
|
||||||
const hasChapterTree = createMemo(() => currentSubject().strands.length > 0);
|
const hasChapterTree = createMemo(() => (currentSubject()?.strands ?? []).length > 0);
|
||||||
|
|
||||||
const chapterFilter = createMemo(
|
const chapterFilter = createMemo(
|
||||||
() => (chapter: CatalogChapter) => chapterMatchesFilters(chapter, activeFilters())
|
() => (chapter: CatalogChapter) => chapterMatchesFilters(chapter, activeFilters())
|
||||||
);
|
);
|
||||||
|
|
||||||
const mermaidData = createMemo(() =>
|
const mermaidData = createMemo(() => {
|
||||||
subjectToMermaid(currentSubject(), chapterFilter(), currentSubjectId())
|
const subject = currentSubject();
|
||||||
|
if (!subject) return null;
|
||||||
|
return subjectToMermaid(subject, chapterFilter(), currentSubjectId());
|
||||||
|
});
|
||||||
|
|
||||||
|
const showCompareDoc = createMemo(
|
||||||
|
() => currentSubjectId() === "math" && viewMode() === "compare"
|
||||||
);
|
);
|
||||||
|
|
||||||
const statsLabel = createMemo(() => {
|
const statsLabel = createMemo(() => {
|
||||||
|
if (showCompareDoc()) {
|
||||||
|
return "Curriculum comparison for Mathematics";
|
||||||
|
}
|
||||||
const count = visibleCount();
|
const count = visibleCount();
|
||||||
const filters = activeFilters();
|
const filters = activeFilters();
|
||||||
const filterText =
|
const filterText =
|
||||||
|
|
@ -91,8 +142,13 @@ export function CatalogApp() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectSubject = (subjectId: string) => {
|
const selectSubject = (subjectId: string) => {
|
||||||
|
if (subjectId !== "math" && viewMode() === "compare") {
|
||||||
|
setViewMode("linear");
|
||||||
|
syncUrl(subjectId, "linear");
|
||||||
|
} else {
|
||||||
|
syncUrl(subjectId, viewMode());
|
||||||
|
}
|
||||||
setCurrentSubjectId(subjectId);
|
setCurrentSubjectId(subjectId);
|
||||||
syncUrl(subjectId, viewMode());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCatalogView = (mode: CatalogViewMode) => {
|
const setCatalogView = (mode: CatalogViewMode) => {
|
||||||
|
|
@ -118,10 +174,13 @@ export function CatalogApp() {
|
||||||
|
|
||||||
const filteredStrands = createMemo(() => {
|
const filteredStrands = createMemo(() => {
|
||||||
const subject = currentSubject();
|
const subject = currentSubject();
|
||||||
return subject.strands
|
if (!subject) return [];
|
||||||
|
return (subject.strands ?? [])
|
||||||
.map((strand) => ({
|
.map((strand) => ({
|
||||||
...strand,
|
...strand,
|
||||||
chapters: strand.chapters.filter((ch) => chapterMatchesFilters(ch, activeFilters())),
|
chapters: (strand.chapters ?? []).filter((ch) =>
|
||||||
|
chapterMatchesFilters(ch, activeFilters())
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
.filter((strand) => strand.chapters.length > 0);
|
.filter((strand) => strand.chapters.length > 0);
|
||||||
});
|
});
|
||||||
|
|
@ -129,7 +188,10 @@ export function CatalogApp() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="catalog-shell"
|
class="catalog-shell"
|
||||||
classList={{ "catalog-shell--tree": viewMode() === "tree" }}
|
classList={{
|
||||||
|
"catalog-shell--tree": viewMode() === "tree",
|
||||||
|
"catalog-shell--compare": showCompareDoc(),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<aside class="catalog-sidebar" aria-label="Subjects">
|
<aside class="catalog-sidebar" aria-label="Subjects">
|
||||||
<div class="catalog-sidebar__header">
|
<div class="catalog-sidebar__header">
|
||||||
|
|
@ -155,12 +217,12 @@ export function CatalogApp() {
|
||||||
<div class="catalog-main">
|
<div class="catalog-main">
|
||||||
<header
|
<header
|
||||||
class="catalog-header"
|
class="catalog-header"
|
||||||
classList={{ "catalog-header--has-banner": Boolean(currentSubject().banner) }}
|
classList={{ "catalog-header--has-banner": Boolean(currentSubject()?.banner) }}
|
||||||
>
|
>
|
||||||
<Show when={currentSubject().banner}>
|
<Show when={currentSubject()?.banner}>
|
||||||
<div class="catalog-header__banner" aria-hidden="true">
|
<div class="catalog-header__banner" aria-hidden="true">
|
||||||
<img
|
<img
|
||||||
src={currentSubject().banner!}
|
src={currentSubject()!.banner!}
|
||||||
alt=""
|
alt=""
|
||||||
width={1400}
|
width={1400}
|
||||||
height={700}
|
height={700}
|
||||||
|
|
@ -170,7 +232,7 @@ export function CatalogApp() {
|
||||||
</Show>
|
</Show>
|
||||||
<div class="catalog-header__head">
|
<div class="catalog-header__head">
|
||||||
<div class="catalog-header__intro">
|
<div class="catalog-header__intro">
|
||||||
<h1 class="catalog-header__title">{currentSubject().name}</h1>
|
<h1 class="catalog-header__title">{currentSubject()?.name ?? "Catalog"}</h1>
|
||||||
<p class="catalog-header__stats">{statsLabel()}</p>
|
<p class="catalog-header__stats">{statsLabel()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="catalog-view-toggle" role="tablist" aria-label="Chapter layout">
|
<div class="catalog-view-toggle" role="tablist" aria-label="Chapter layout">
|
||||||
|
|
@ -192,21 +254,37 @@ export function CatalogApp() {
|
||||||
>
|
>
|
||||||
Map
|
Map
|
||||||
</button>
|
</button>
|
||||||
|
<Show when={currentSubjectId() === "math"}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="catalog-view-toggle__btn"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={viewMode() === "compare" ? "true" : "false"}
|
||||||
|
onClick={() => setCatalogView("compare")}
|
||||||
|
>
|
||||||
|
Compare
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="catalog-header__filters">
|
<Show when={!showCompareDoc()}>
|
||||||
<CatalogFiltersMenu
|
<div class="catalog-header__filters">
|
||||||
curriculums={data.curriculums}
|
<CatalogFiltersMenu
|
||||||
activeFilters={activeFilters}
|
curriculums={data.curriculums ?? []}
|
||||||
onToggle={toggleFilter}
|
activeFilters={activeFilters}
|
||||||
/>
|
onToggle={toggleFilter}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="catalog-content"
|
class="catalog-content"
|
||||||
classList={{ "catalog-content--tree": viewMode() === "tree" }}
|
classList={{ "catalog-content--tree": viewMode() === "tree" }}
|
||||||
>
|
>
|
||||||
|
<Show
|
||||||
|
when={showCompareDoc()}
|
||||||
|
fallback={
|
||||||
<Show
|
<Show
|
||||||
when={hasChapterTree() && visibleCount() > 0}
|
when={hasChapterTree() && visibleCount() > 0}
|
||||||
fallback={
|
fallback={
|
||||||
|
|
@ -262,6 +340,10 @@ export function CatalogApp() {
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CatalogCompareDoc />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { For, Show } from "solid-js";
|
import { For, Show } from "solid-js";
|
||||||
import type { CatalogChapter } from "../../../data/catalog.types";
|
import type { CatalogChapter } from "../../../data/catalog.types";
|
||||||
|
import { chapterFilterCurriculums } from "../lib/catalog-coverage";
|
||||||
import { curriculumBadgeClass, tierBadgeLabel } from "../lib/catalog-badge";
|
import { curriculumBadgeClass, tierBadgeLabel } from "../lib/catalog-badge";
|
||||||
|
|
||||||
export type CatalogChapterCardProps = {
|
export type CatalogChapterCardProps = {
|
||||||
|
|
@ -29,7 +30,7 @@ export function CatalogChapterCard(props: CatalogChapterCardProps) {
|
||||||
<p class="catalog-chapter-card__description">{props.chapter.description}</p>
|
<p class="catalog-chapter-card__description">{props.chapter.description}</p>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="catalog-chapter-card__meta">
|
<div class="catalog-chapter-card__meta">
|
||||||
<For each={props.chapter.curriculums}>
|
<For each={chapterFilterCurriculums(props.chapter)}>
|
||||||
{(curriculum) => (
|
{(curriculum) => (
|
||||||
<span class={`catalog-badge ${curriculumBadgeClass(curriculum)}`}>{curriculum}</span>
|
<span class={`catalog-badge ${curriculumBadgeClass(curriculum)}`}>{curriculum}</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
24
frontend/src/components/catalog-compare-doc.tsx
Normal file
24
frontend/src/components/catalog-compare-doc.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createSignal, onMount, Show } from "solid-js";
|
||||||
|
|
||||||
|
function readCompareTemplateHtml(): string {
|
||||||
|
const el = document.getElementById("catalog-compare-template");
|
||||||
|
return el?.innerHTML?.trim() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CatalogCompareDoc() {
|
||||||
|
const [html, setHtml] = createSignal("");
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setHtml(readCompareTemplateHtml());
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={html()}>
|
||||||
|
<article
|
||||||
|
class="catalog-compare-doc book-prose"
|
||||||
|
aria-label="Mathematics curriculum comparison"
|
||||||
|
innerHTML={html()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -74,7 +74,7 @@ export function CatalogFiltersMenu(props: CatalogFiltersMenuProps) {
|
||||||
/>
|
/>
|
||||||
<span class="catalog-filter-menu__option-label">All</span>
|
<span class="catalog-filter-menu__option-label">All</span>
|
||||||
</label>
|
</label>
|
||||||
<For each={props.curriculums}>
|
<For each={props.curriculums ?? []}>
|
||||||
{(curriculum) => (
|
{(curriculum) => (
|
||||||
<label class="catalog-filter-menu__option">
|
<label class="catalog-filter-menu__option">
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { createEffect, onCleanup, onMount } from "solid-js";
|
import { createEffect, onCleanup, onMount } from "solid-js";
|
||||||
import type { MermaidCatalogDiagram } from "../lib/catalog-to-mermaid";
|
import type { MermaidCatalogDiagram } from "../lib/catalog-to-mermaid";
|
||||||
import { siteCanvasTheme, watchSiteTheme } from "../lib/catalog-canvas-theme";
|
import { siteCanvasTheme, watchSiteTheme } from "../lib/catalog-canvas-theme";
|
||||||
|
import { mermaidInitializeOptions } from "../lib/catalog-mermaid-theme";
|
||||||
|
import { enhanceStrandClusterHeaders } from "../lib/catalog-mermaid-strand-headers";
|
||||||
|
|
||||||
type CatalogMermaidViewProps = {
|
type CatalogMermaidViewProps = {
|
||||||
diagram: MermaidCatalogDiagram;
|
diagram: MermaidCatalogDiagram;
|
||||||
|
|
@ -84,7 +86,7 @@ export function CatalogMermaidView(props: CatalogMermaidViewProps) {
|
||||||
mermaidModule.initialize({
|
mermaidModule.initialize({
|
||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
securityLevel: "loose",
|
securityLevel: "loose",
|
||||||
theme: theme === "dark" ? "dark" : "default",
|
...mermaidInitializeOptions(theme),
|
||||||
flowchart: {
|
flowchart: {
|
||||||
htmlLabels: true,
|
htmlLabels: true,
|
||||||
curve: "monotoneY",
|
curve: "monotoneY",
|
||||||
|
|
@ -109,6 +111,8 @@ export function CatalogMermaidView(props: CatalogMermaidViewProps) {
|
||||||
renderedSvg.style.height = "100%";
|
renderedSvg.style.height = "100%";
|
||||||
renderedSvg.style.width = "100%";
|
renderedSvg.style.width = "100%";
|
||||||
|
|
||||||
|
enhanceStrandClusterHeaders(renderedSvg, theme);
|
||||||
|
|
||||||
panZoom = svgPanZoomModule(renderedSvg, {
|
panZoom = svgPanZoomModule(renderedSvg, {
|
||||||
zoomEnabled: true,
|
zoomEnabled: true,
|
||||||
panEnabled: true,
|
panEnabled: true,
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ function GraphPrereqDemo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CatalogViewsDemo() {
|
function CatalogViewsDemo() {
|
||||||
const [view, setView] = createSignal<"list" | "map">("list");
|
const [view, setView] = createSignal<"list" | "map" | "compare">("list");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="contributing-views-demo" aria-label="Catalog list and map views">
|
<div class="contributing-views-demo" aria-label="Catalog list and map views">
|
||||||
|
|
@ -104,9 +104,22 @@ function CatalogViewsDemo() {
|
||||||
>
|
>
|
||||||
Map
|
Map
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
class="contributing-views-demo__tab"
|
||||||
|
classList={{ "is-active": view() === "compare" }}
|
||||||
|
aria-selected={view() === "compare"}
|
||||||
|
onClick={() => setView("compare")}
|
||||||
|
>
|
||||||
|
Compare
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="contributing-views-demo__panel" role="tabpanel">
|
<div class="contributing-views-demo__panel" role="tabpanel">
|
||||||
<Show
|
<Show
|
||||||
|
when={view() === "compare"}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
when={view() === "list"}
|
when={view() === "list"}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="contributing-views-demo__map">
|
<div class="contributing-views-demo__map">
|
||||||
|
|
@ -148,6 +161,13 @@ function CatalogViewsDemo() {
|
||||||
List order follows the <code>chapters[]</code> array — your table of contents.
|
List order follows the <code>chapters[]</code> array — your table of contents.
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p class="contributing-views-demo__caption">
|
||||||
|
Mathematics only: curricula comparison doc at{" "}
|
||||||
|
<code>?view=compare</code> (HTML partial, not JSON).
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export function curriculumBadgeClass(curriculum: string): string {
|
||||||
"A-Level": "catalog-badge--a-level",
|
"A-Level": "catalog-badge--a-level",
|
||||||
AP: "catalog-badge--ap",
|
AP: "catalog-badge--ap",
|
||||||
IGCSE: "catalog-badge--igcse",
|
IGCSE: "catalog-badge--igcse",
|
||||||
|
"Common Core": "catalog-badge--common-core",
|
||||||
};
|
};
|
||||||
return map[curriculum] ?? "catalog-badge--default";
|
return map[curriculum] ?? "catalog-badge--default";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
frontend/src/lib/catalog-coverage.test.ts
Normal file
23
frontend/src/lib/catalog-coverage.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { chapterFilterCurriculums, coverageRole } from "./catalog-coverage";
|
||||||
|
import type { CatalogChapter } from "../../../data/catalog.types";
|
||||||
|
|
||||||
|
describe("catalog-coverage", () => {
|
||||||
|
test("reads role from curriculumCoverage", () => {
|
||||||
|
const chapter: CatalogChapter = {
|
||||||
|
slug: "quadratic-equations",
|
||||||
|
title: "Quadratic equations",
|
||||||
|
description: "",
|
||||||
|
status: "live",
|
||||||
|
curriculumCoverage: {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "extension",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(coverageRole(chapter, "DSE")).toBe("core");
|
||||||
|
expect(coverageRole(chapter, "IB")).toBe("extension");
|
||||||
|
expect(coverageRole(chapter, "AP")).toBeNull();
|
||||||
|
expect(chapterFilterCurriculums(chapter)).toEqual(["DSE", "IGCSE", "IB"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
53
frontend/src/lib/catalog-coverage.ts
Normal file
53
frontend/src/lib/catalog-coverage.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import type { CatalogChapter, CatalogChapterTier } from "../../../data/catalog.types";
|
||||||
|
|
||||||
|
export type CurriculumCoverageRole = "core" | "extension" | "related";
|
||||||
|
|
||||||
|
export type CurriculumCoverage = Partial<Record<string, CurriculumCoverageRole>>;
|
||||||
|
|
||||||
|
/** Columns on the Compare chapter matrix (includes frameworks beyond filter chips). */
|
||||||
|
export const COMPARE_MATRIX_COLUMNS = [
|
||||||
|
"Foundation",
|
||||||
|
"DSE",
|
||||||
|
"IGCSE",
|
||||||
|
"A-Level",
|
||||||
|
"AP",
|
||||||
|
"IB",
|
||||||
|
"Common Core",
|
||||||
|
"Singapore",
|
||||||
|
"UK GCSE",
|
||||||
|
"Gaokao",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type CompareMatrixColumn = (typeof COMPARE_MATRIX_COLUMNS)[number];
|
||||||
|
|
||||||
|
export function coverageRole(
|
||||||
|
chapter: CatalogChapter,
|
||||||
|
framework: string
|
||||||
|
): CurriculumCoverageRole | null {
|
||||||
|
const coverage = chapter.curriculumCoverage;
|
||||||
|
if (coverage && framework in coverage) {
|
||||||
|
return coverage[framework] ?? null;
|
||||||
|
}
|
||||||
|
if (chapter.curriculums?.includes(framework)) {
|
||||||
|
return "core";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Framework labels used for catalog filter chips (any mapped role counts). */
|
||||||
|
export function chapterFilterCurriculums(chapter: CatalogChapter | undefined): string[] {
|
||||||
|
if (!chapter) return [];
|
||||||
|
const coverage = chapter.curriculumCoverage;
|
||||||
|
if (coverage && typeof coverage === "object" && !Array.isArray(coverage)) {
|
||||||
|
return Object.keys(coverage);
|
||||||
|
}
|
||||||
|
if (Array.isArray(chapter.curriculums)) {
|
||||||
|
return chapter.curriculums;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DSE compulsory vs M2-style extension when tier is set. */
|
||||||
|
export function defaultDseRole(tier?: CatalogChapterTier): CurriculumCoverageRole {
|
||||||
|
return tier === "non-foundation" ? "extension" : "core";
|
||||||
|
}
|
||||||
9
frontend/src/lib/catalog-mermaid-strand-headers.test.ts
Normal file
9
frontend/src/lib/catalog-mermaid-strand-headers.test.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { isStrandClusterId } from "./catalog-mermaid-strand-headers";
|
||||||
|
|
||||||
|
describe("catalog-mermaid-strand-headers", () => {
|
||||||
|
test("isStrandClusterId matches strand subgraph ids", () => {
|
||||||
|
expect(isStrandClusterId("flowchart-strand_number_algebra-1")).toBe(true);
|
||||||
|
expect(isStrandClusterId("flowchart-root-0")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
99
frontend/src/lib/catalog-mermaid-strand-headers.ts
Normal file
99
frontend/src/lib/catalog-mermaid-strand-headers.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {
|
||||||
|
mermaidThemeVariables,
|
||||||
|
type MermaidThemeMode,
|
||||||
|
} from "./catalog-mermaid-theme";
|
||||||
|
|
||||||
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
/** Header band height — must fit title line-height + descenders inside foreignObject */
|
||||||
|
export const STRAND_HEADER_HEIGHT = 44;
|
||||||
|
const STRAND_HEADER_PAD_X = 16;
|
||||||
|
|
||||||
|
function applySvgFill(el: SVGGraphicsElement, fill: string): void {
|
||||||
|
el.setAttribute("fill", fill);
|
||||||
|
el.style.fill = fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStrandClusterId(id: string): boolean {
|
||||||
|
return id.includes("strand_");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStrandCluster(cluster: SVGGElement): boolean {
|
||||||
|
return isStrandClusterId(cluster.id ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mermaid subgraph titles are plain labels on the cluster border. Overlay a header band
|
||||||
|
* and tuck the title into it (Cursor-style subgraph chrome).
|
||||||
|
*/
|
||||||
|
export function enhanceStrandClusterHeaders(
|
||||||
|
svg: SVGSVGElement,
|
||||||
|
mode: MermaidThemeMode = "light"
|
||||||
|
): void {
|
||||||
|
const colors = mermaidThemeVariables(mode);
|
||||||
|
const clusters = svg.querySelectorAll<SVGGElement>("g.cluster");
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
if (!isStrandCluster(cluster)) continue;
|
||||||
|
if (cluster.querySelector(".catalog-mermaid-strand-header")) continue;
|
||||||
|
|
||||||
|
const frameRect = cluster.querySelector<SVGRectElement>(":scope > rect");
|
||||||
|
const labelGroup = cluster.querySelector<SVGGElement>(":scope > g.cluster-label");
|
||||||
|
if (!frameRect || !labelGroup) continue;
|
||||||
|
|
||||||
|
const bbox = cluster.getBBox();
|
||||||
|
if (bbox.width <= 0 || bbox.height <= 0) continue;
|
||||||
|
|
||||||
|
cluster.classList.add("catalog-mermaid-strand-cluster");
|
||||||
|
|
||||||
|
const header = document.createElementNS(SVG_NS, "rect");
|
||||||
|
header.setAttribute("class", "catalog-mermaid-strand-header");
|
||||||
|
header.setAttribute("x", String(bbox.x));
|
||||||
|
header.setAttribute("y", String(bbox.y));
|
||||||
|
header.setAttribute("width", String(bbox.width));
|
||||||
|
header.setAttribute("height", String(STRAND_HEADER_HEIGHT));
|
||||||
|
applySvgFill(header, colors.secondaryColor);
|
||||||
|
|
||||||
|
const rule = document.createElementNS(SVG_NS, "line");
|
||||||
|
rule.setAttribute("class", "catalog-mermaid-strand-header__rule");
|
||||||
|
rule.setAttribute("x1", String(bbox.x));
|
||||||
|
rule.setAttribute("y1", String(bbox.y + STRAND_HEADER_HEIGHT));
|
||||||
|
rule.setAttribute("x2", String(bbox.x + bbox.width));
|
||||||
|
rule.setAttribute("y2", String(bbox.y + STRAND_HEADER_HEIGHT));
|
||||||
|
rule.setAttribute("stroke", colors.clusterBorder);
|
||||||
|
|
||||||
|
frameRect.classList.add("catalog-mermaid-strand-frame");
|
||||||
|
applySvgFill(frameRect, colors.clusterBkg);
|
||||||
|
frameRect.setAttribute("rx", "0");
|
||||||
|
frameRect.setAttribute("ry", "0");
|
||||||
|
frameRect.setAttribute("stroke", colors.clusterBorder);
|
||||||
|
cluster.insertBefore(header, frameRect.nextSibling);
|
||||||
|
cluster.insertBefore(rule, header.nextSibling);
|
||||||
|
|
||||||
|
labelGroup.setAttribute("transform", `translate(${bbox.x}, ${bbox.y})`);
|
||||||
|
|
||||||
|
const foreignObject = labelGroup.querySelector<SVGForeignObjectElement>("foreignObject");
|
||||||
|
if (foreignObject) {
|
||||||
|
foreignObject.setAttribute("class", "catalog-mermaid-strand-label-fo");
|
||||||
|
foreignObject.setAttribute("overflow", "visible");
|
||||||
|
foreignObject.setAttribute("width", String(Math.max(bbox.width, 120)));
|
||||||
|
foreignObject.setAttribute("height", String(STRAND_HEADER_HEIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const el of labelGroup.querySelectorAll<HTMLElement>("div, span, p, header")) {
|
||||||
|
el.style.overflow = "visible";
|
||||||
|
el.style.border = "none";
|
||||||
|
el.style.borderRadius = "0";
|
||||||
|
el.style.background = "transparent";
|
||||||
|
el.style.boxShadow = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelRoot = labelGroup.querySelector<HTMLElement>("div");
|
||||||
|
if (labelRoot) {
|
||||||
|
labelRoot.style.height = `${STRAND_HEADER_HEIGHT}px`;
|
||||||
|
labelRoot.style.width = "100%";
|
||||||
|
labelRoot.style.boxSizing = "border-box";
|
||||||
|
labelRoot.style.padding = `0 ${STRAND_HEADER_PAD_X}px`;
|
||||||
|
labelRoot.style.display = "flex";
|
||||||
|
labelRoot.style.alignItems = "center";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
frontend/src/lib/catalog-mermaid-theme.test.ts
Normal file
16
frontend/src/lib/catalog-mermaid-theme.test.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { mermaidInitializeOptions, mermaidThemeVariables } from "./catalog-mermaid-theme";
|
||||||
|
|
||||||
|
describe("catalog-mermaid-theme", () => {
|
||||||
|
test("uses base theme with distinct cluster and secondary colors", () => {
|
||||||
|
const dark = mermaidThemeVariables("dark");
|
||||||
|
expect(dark.clusterBkg).not.toBe(dark.secondaryColor);
|
||||||
|
expect(dark.clusterBkg).toBe("#202020");
|
||||||
|
expect(dark.secondaryColor).toBe("#3d3a36");
|
||||||
|
|
||||||
|
const init = mermaidInitializeOptions("light");
|
||||||
|
expect(init.theme).toBe("base");
|
||||||
|
expect(init.themeVariables.clusterBkg).toBe("#f0efec");
|
||||||
|
expect(init.themeVariables.secondaryColor).toBe("#e3e1dc");
|
||||||
|
});
|
||||||
|
});
|
||||||
69
frontend/src/lib/catalog-mermaid-theme.ts
Normal file
69
frontend/src/lib/catalog-mermaid-theme.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Mermaid only applies custom cluster/node colors via theme "base" + themeVariables (hex).
|
||||||
|
* CSS cannot override Mermaid's inline SVG fills on cluster rects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type MermaidThemeMode = "light" | "dark";
|
||||||
|
|
||||||
|
export type CatalogMermaidThemeVariables = {
|
||||||
|
darkMode: boolean;
|
||||||
|
background: string;
|
||||||
|
primaryColor: string;
|
||||||
|
primaryTextColor: string;
|
||||||
|
primaryBorderColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
tertiaryColor: string;
|
||||||
|
clusterBkg: string;
|
||||||
|
clusterBorder: string;
|
||||||
|
lineColor: string;
|
||||||
|
titleColor: string;
|
||||||
|
mainBkg: string;
|
||||||
|
nodeBorder: string;
|
||||||
|
edgeLabelBackground: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Strand title band (secondary) vs subgraph body (clusterBkg / tertiary). */
|
||||||
|
export function mermaidThemeVariables(mode: MermaidThemeMode): CatalogMermaidThemeVariables {
|
||||||
|
if (mode === "dark") {
|
||||||
|
return {
|
||||||
|
darkMode: true,
|
||||||
|
background: "#1a1917",
|
||||||
|
primaryColor: "#2a2826",
|
||||||
|
primaryTextColor: "#eceae4",
|
||||||
|
primaryBorderColor: "#4a4844",
|
||||||
|
secondaryColor: "#3d3a36",
|
||||||
|
tertiaryColor: "#202020",
|
||||||
|
clusterBkg: "#202020",
|
||||||
|
clusterBorder: "#4a4844",
|
||||||
|
lineColor: "#c4924a",
|
||||||
|
titleColor: "#eceae4",
|
||||||
|
mainBkg: "#2a2826",
|
||||||
|
nodeBorder: "#4a4844",
|
||||||
|
edgeLabelBackground: "#2a2826",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
darkMode: false,
|
||||||
|
background: "#f5f4f1",
|
||||||
|
primaryColor: "#faf9f7",
|
||||||
|
primaryTextColor: "#1a1a1a",
|
||||||
|
primaryBorderColor: "#e8e8e8",
|
||||||
|
secondaryColor: "#e3e1dc",
|
||||||
|
tertiaryColor: "#f0efec",
|
||||||
|
clusterBkg: "#f0efec",
|
||||||
|
clusterBorder: "#e8e8e8",
|
||||||
|
lineColor: "#9a6b2e",
|
||||||
|
titleColor: "#1a1a1a",
|
||||||
|
mainBkg: "#faf9f7",
|
||||||
|
nodeBorder: "#e8e8e8",
|
||||||
|
edgeLabelBackground: "#f5f4f1",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mermaidInitializeOptions(mode: MermaidThemeMode) {
|
||||||
|
return {
|
||||||
|
theme: "base" as const,
|
||||||
|
themeVariables: mermaidThemeVariables(mode),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ function chapter(
|
||||||
title: slug,
|
title: slug,
|
||||||
description: "desc",
|
description: "desc",
|
||||||
status: "planned",
|
status: "planned",
|
||||||
curriculums: ["DSE"],
|
curriculumCoverage: { DSE: "core" },
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +55,8 @@ describe("subjectToMermaid", () => {
|
||||||
expect(diagram!.rootNodeId).toBe("chapter_a");
|
expect(diagram!.rootNodeId).toBe("chapter_a");
|
||||||
|
|
||||||
expect(source).toContain("flowchart TB");
|
expect(source).toContain("flowchart TB");
|
||||||
|
expect(source).toContain("catalog-mermaid-strand__title");
|
||||||
|
expect(source).toContain("Strand A");
|
||||||
expect(source.match(/chapter_a --> chapter_c/g)?.length).toBe(1);
|
expect(source.match(/chapter_a --> chapter_c/g)?.length).toBe(1);
|
||||||
expect(source).toContain('chapter_b -->|after| chapter_c');
|
expect(source).toContain('chapter_b -->|after| chapter_c');
|
||||||
expect(source).toContain('click chapter_a href "/math/a/"');
|
expect(source).toContain('click chapter_a href "/math/a/"');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { CatalogChapter, CatalogGraphEdge, CatalogSubject } from "../../../data/catalog.types";
|
import type { CatalogChapter, CatalogGraphEdge, CatalogSubject } from "../../../data/catalog.types";
|
||||||
|
import { chapterFilterCurriculums } from "./catalog-coverage";
|
||||||
import { curriculumBadgeClass, tierBadgeLabel } from "./catalog-badge";
|
import { curriculumBadgeClass, tierBadgeLabel } from "./catalog-badge";
|
||||||
|
|
||||||
export type MermaidCatalogDiagram = {
|
export type MermaidCatalogDiagram = {
|
||||||
|
|
@ -11,7 +12,7 @@ function collectVisibleChapters(
|
||||||
matches: (chapter: CatalogChapter) => boolean
|
matches: (chapter: CatalogChapter) => boolean
|
||||||
): Map<string, CatalogChapter> {
|
): Map<string, CatalogChapter> {
|
||||||
const visible = new Map<string, CatalogChapter>();
|
const visible = new Map<string, CatalogChapter>();
|
||||||
for (const strand of subject.strands) {
|
for (const strand of subject.strands ?? []) {
|
||||||
for (const chapter of strand.chapters) {
|
for (const chapter of strand.chapters) {
|
||||||
if (matches(chapter)) {
|
if (matches(chapter)) {
|
||||||
visible.set(chapter.slug, chapter);
|
visible.set(chapter.slug, chapter);
|
||||||
|
|
@ -82,7 +83,7 @@ function selectRootSlug(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const strand of subject.strands) {
|
for (const strand of subject.strands ?? []) {
|
||||||
for (const chapter of strand.chapters) {
|
for (const chapter of strand.chapters) {
|
||||||
if (rootCandidates.has(chapter.slug)) {
|
if (rootCandidates.has(chapter.slug)) {
|
||||||
return chapter.slug;
|
return chapter.slug;
|
||||||
|
|
@ -98,7 +99,7 @@ function chapterNodeLabel(chapter: CatalogChapter): string {
|
||||||
const description = chapter.description
|
const description = chapter.description
|
||||||
? `<p class='catalog-mermaid-node__description'>${escapeLabel(chapter.description)}</p>`
|
? `<p class='catalog-mermaid-node__description'>${escapeLabel(chapter.description)}</p>`
|
||||||
: "";
|
: "";
|
||||||
const curriculumBadges = chapter.curriculums
|
const curriculumBadges = chapterFilterCurriculums(chapter)
|
||||||
.map(
|
.map(
|
||||||
(curriculum) =>
|
(curriculum) =>
|
||||||
`<span class='catalog-badge ${curriculumBadgeClass(curriculum)}'>${escapeLabel(curriculum)}</span>`
|
`<span class='catalog-badge ${curriculumBadgeClass(curriculum)}'>${escapeLabel(curriculum)}</span>`
|
||||||
|
|
@ -128,11 +129,13 @@ export function subjectToMermaid(
|
||||||
const rootSlug = selectRootSlug(subject, visible, edges);
|
const rootSlug = selectRootSlug(subject, visible, edges);
|
||||||
const lines: string[] = ["flowchart TB"];
|
const lines: string[] = ["flowchart TB"];
|
||||||
|
|
||||||
for (const strand of subject.strands) {
|
for (const strand of subject.strands ?? []) {
|
||||||
const strandChapters = strand.chapters.filter((chapter) => visible.has(chapter.slug));
|
const strandChapters = strand.chapters.filter((chapter) => visible.has(chapter.slug));
|
||||||
if (strandChapters.length === 0) continue;
|
if (strandChapters.length === 0) continue;
|
||||||
|
|
||||||
lines.push(` subgraph strand_${strand.id.replaceAll(/[^a-zA-Z0-9_]/g, "_")}["${escapeLabel(strand.title)}"]`);
|
const strandKey = strand.id.replaceAll(/[^a-zA-Z0-9_]/g, "_");
|
||||||
|
const strandHeader = `<header class='catalog-mermaid-strand__header'><span class='catalog-mermaid-strand__title'>${escapeLabel(strand.title)}</span></header>`;
|
||||||
|
lines.push(` subgraph strand_${strandKey}["${strandHeader}"]`);
|
||||||
lines.push(" direction TB");
|
lines.push(" direction TB");
|
||||||
for (const chapter of strandChapters) {
|
for (const chapter of strandChapters) {
|
||||||
const id = nodeId(chapter.slug);
|
const id = nodeId(chapter.slug);
|
||||||
|
|
|
||||||
461
scripts/apply-math-coverage.ts
Normal file
461
scripts/apply-math-coverage.ts
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
/**
|
||||||
|
* One-off helper: merge research-based curriculumCoverage into math-curriculum.json.
|
||||||
|
* Run: bun scripts/apply-math-coverage.ts
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
type Role = "core" | "extension" | "related";
|
||||||
|
type Coverage = Partial<Record<string, Role>>;
|
||||||
|
|
||||||
|
const ROOT = path.join(import.meta.dir, "..");
|
||||||
|
const JSON_PATH = path.join(ROOT, "data/math-curriculum.json");
|
||||||
|
|
||||||
|
/** slug → framework label → role */
|
||||||
|
const COVERAGE_BY_SLUG: Record<string, Coverage> = {
|
||||||
|
"counting-and-numbers": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
DSE: "related",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"basic-math-notation": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
DSE: "related",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"properties-of-operations": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
DSE: "related",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"written-calculation": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
DSE: "related",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"integer-foundations": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
DSE: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"integer-arithmetic": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
DSE: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"rational-number-operations": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
DSE: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"percentage-applications": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
DSE: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"ratios-and-rates-foundation": {
|
||||||
|
Foundation: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
DSE: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"directed-numbers-rates": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Foundation: "related",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"polynomials-expansion-factorisation": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Gaokao: "core",
|
||||||
|
Foundation: "related",
|
||||||
|
AP: "related",
|
||||||
|
},
|
||||||
|
"algebraic-fractions-formulas": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "related",
|
||||||
|
Singapore: "related",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Gaokao: "related",
|
||||||
|
Foundation: "related",
|
||||||
|
AP: "related",
|
||||||
|
},
|
||||||
|
"linear-equations-two-unknowns": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Gaokao: "core",
|
||||||
|
AP: "related",
|
||||||
|
Foundation: "related",
|
||||||
|
},
|
||||||
|
"linear-inequalities-one-unknown": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
Singapore: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"laws-of-indices": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
AP: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
Gaokao: "core",
|
||||||
|
Foundation: "related",
|
||||||
|
},
|
||||||
|
"quadratic-equations": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
AP: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
Gaokao: "core",
|
||||||
|
Foundation: "related",
|
||||||
|
},
|
||||||
|
"factor-remainder-theorems": {
|
||||||
|
DSE: "extension",
|
||||||
|
IB: "extension",
|
||||||
|
"A-Level": "extension",
|
||||||
|
IGCSE: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
AP: "related",
|
||||||
|
},
|
||||||
|
"complex-numbers": {
|
||||||
|
DSE: "extension",
|
||||||
|
IB: "extension",
|
||||||
|
"A-Level": "extension",
|
||||||
|
Gaokao: "related",
|
||||||
|
IGCSE: "related",
|
||||||
|
AP: "related",
|
||||||
|
},
|
||||||
|
"functions-graphs": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
AP: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
Gaokao: "core",
|
||||||
|
Foundation: "related",
|
||||||
|
},
|
||||||
|
"exponential-logarithmic": {
|
||||||
|
DSE: "extension",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
AP: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Gaokao: "core",
|
||||||
|
Singapore: "related",
|
||||||
|
Foundation: "related",
|
||||||
|
},
|
||||||
|
"sequences-series": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
"Common Core": "related",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "core",
|
||||||
|
Singapore: "related",
|
||||||
|
Foundation: "related",
|
||||||
|
},
|
||||||
|
"variations": {
|
||||||
|
DSE: "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
IGCSE: "related",
|
||||||
|
"Common Core": "related",
|
||||||
|
Singapore: "related",
|
||||||
|
},
|
||||||
|
"linear-programming": {
|
||||||
|
DSE: "extension",
|
||||||
|
IB: "extension",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
IGCSE: "related",
|
||||||
|
},
|
||||||
|
"coordinate-geometry-foundations": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
Singapore: "core",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"deductive-geometry": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"congruence-similarity": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"pythagoras-theorem": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
Gaokao: "core",
|
||||||
|
AP: "related",
|
||||||
|
},
|
||||||
|
"introduction-trigonometry": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
Gaokao: "core",
|
||||||
|
AP: "related",
|
||||||
|
},
|
||||||
|
"mensuration-3d": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"coordinate-circles": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "related",
|
||||||
|
IB: "extension",
|
||||||
|
"A-Level": "extension",
|
||||||
|
Gaokao: "related",
|
||||||
|
"UK GCSE": "related",
|
||||||
|
AP: "related",
|
||||||
|
},
|
||||||
|
"advanced-trigonometry": {
|
||||||
|
DSE: "extension",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
AP: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Gaokao: "core",
|
||||||
|
"Common Core": "related",
|
||||||
|
Singapore: "related",
|
||||||
|
},
|
||||||
|
"circle-properties": {
|
||||||
|
DSE: "extension",
|
||||||
|
IGCSE: "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
IB: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
Singapore: "related",
|
||||||
|
},
|
||||||
|
"loci": {
|
||||||
|
DSE: "extension",
|
||||||
|
IGCSE: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
IB: "related",
|
||||||
|
"UK GCSE": "related",
|
||||||
|
},
|
||||||
|
"statistical-charts-averages": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
},
|
||||||
|
"measures-dispersion": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "related",
|
||||||
|
AP: "related",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Singapore: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"basic-probability": {
|
||||||
|
DSE: "core",
|
||||||
|
IGCSE: "core",
|
||||||
|
IB: "core",
|
||||||
|
"Common Core": "core",
|
||||||
|
"UK GCSE": "core",
|
||||||
|
Singapore: "core",
|
||||||
|
AP: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
},
|
||||||
|
"permutations-combinations-probability": {
|
||||||
|
DSE: "extension",
|
||||||
|
IGCSE: "extension",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
AP: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
"Common Core": "related",
|
||||||
|
},
|
||||||
|
"normal-distribution-z-scores": {
|
||||||
|
DSE: "extension",
|
||||||
|
IB: "core",
|
||||||
|
"A-Level": "core",
|
||||||
|
AP: "related",
|
||||||
|
IGCSE: "related",
|
||||||
|
Gaokao: "related",
|
||||||
|
"Common Core": "related",
|
||||||
|
},
|
||||||
|
"statistical-literacy": {
|
||||||
|
DSE: "core",
|
||||||
|
IB: "related",
|
||||||
|
"Common Core": "related",
|
||||||
|
IGCSE: "related",
|
||||||
|
AP: "related",
|
||||||
|
"A-Level": "related",
|
||||||
|
Singapore: "related",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Chapter = {
|
||||||
|
slug: string;
|
||||||
|
curriculums?: string[];
|
||||||
|
curriculumCoverage?: Coverage;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Catalog = {
|
||||||
|
strands: { chapters: Chapter[] }[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const catalog = JSON.parse(readFileSync(JSON_PATH, "utf8")) as Catalog;
|
||||||
|
|
||||||
|
for (const strand of catalog.strands) {
|
||||||
|
for (const chapter of strand.chapters) {
|
||||||
|
const coverage = COVERAGE_BY_SLUG[chapter.slug];
|
||||||
|
if (!coverage) {
|
||||||
|
throw new Error(`Missing coverage map for slug: ${chapter.slug}`);
|
||||||
|
}
|
||||||
|
chapter.curriculumCoverage = coverage;
|
||||||
|
delete chapter.curriculums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(JSON_PATH, `${JSON.stringify(catalog, null, 2)}\n`);
|
||||||
|
console.log(`Updated ${Object.keys(COVERAGE_BY_SLUG).length} chapters in math-curriculum.json`);
|
||||||
|
|
@ -487,7 +487,7 @@
|
||||||
|
|
||||||
:global(.catalog-mermaid-view__svg .node rect),
|
:global(.catalog-mermaid-view__svg .node rect),
|
||||||
:global(.catalog-mermaid-view__svg .node polygon),
|
:global(.catalog-mermaid-view__svg .node polygon),
|
||||||
:global(.catalog-mermaid-view__svg .cluster rect) {
|
:global(.catalog-mermaid-view__svg .cluster > rect:not(.catalog-mermaid-strand-header)) {
|
||||||
fill: color-mix(in srgb, var(--paper) 92%, transparent);
|
fill: color-mix(in srgb, var(--paper) 92%, transparent);
|
||||||
stroke: color-mix(in srgb, var(--card-border) 88%, var(--line));
|
stroke: color-mix(in srgb, var(--card-border) 88%, var(--line));
|
||||||
stroke-width: 1.3px;
|
stroke-width: 1.3px;
|
||||||
|
|
@ -533,7 +533,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.catalog-mermaid-view__svg .plannedNode .catalog-badge:not(.book-chapter-badge--planned)) {
|
:global(.catalog-mermaid-view__svg .plannedNode .catalog-badge:not(.book-chapter-badge--planned)) {
|
||||||
opacity: 0.55;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.catalog-mermaid-view__svg .edge-path path) {
|
:global(.catalog-mermaid-view__svg .edge-path path) {
|
||||||
|
|
@ -541,9 +541,57 @@
|
||||||
stroke-width: 1.8px;
|
stroke-width: 1.8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.catalog-mermaid-view__svg .cluster rect) {
|
/* Strand cluster fills come from Mermaid themeVariables (clusterBkg / secondaryColor), not CSS. */
|
||||||
fill: color-mix(in srgb, var(--paper) 78%, transparent);
|
|
||||||
stroke: color-mix(in srgb, var(--accent) 26%, var(--line));
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster > rect.catalog-mermaid-strand-header) {
|
||||||
|
stroke: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.catalog-mermaid-view__svg .catalog-mermaid-strand-header__rule) {
|
||||||
|
stroke-width: 1px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.catalog-mermaid-view__svg foreignObject.catalog-mermaid-strand-label-fo) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label foreignObject),
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label foreignObject div),
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label foreignObject span),
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label foreignObject header),
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label .nodeLabel),
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label .label) {
|
||||||
|
overflow: visible !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label .catalog-mermaid-strand__header) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.catalog-mermaid-view__svg g.cluster.catalog-mermaid-strand-cluster .cluster-label .catalog-mermaid-strand__title) {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 0 4px;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.35;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: var(--ink);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.catalog-mermaid-node) {
|
:global(.catalog-mermaid-node) {
|
||||||
|
|
@ -589,7 +637,7 @@
|
||||||
[data-theme="dark"] .catalog-mermaid-view {
|
[data-theme="dark"] .catalog-mermaid-view {
|
||||||
:global(.catalog-mermaid-view__svg .node rect),
|
:global(.catalog-mermaid-view__svg .node rect),
|
||||||
:global(.catalog-mermaid-view__svg .node polygon),
|
:global(.catalog-mermaid-view__svg .node polygon),
|
||||||
:global(.catalog-mermaid-view__svg .cluster rect) {
|
:global(.catalog-mermaid-view__svg .cluster > rect:not(.catalog-mermaid-strand-header)) {
|
||||||
stroke: color-mix(in srgb, var(--accent) 78%, #ffffff 22%);
|
stroke: color-mix(in srgb, var(--accent) 78%, #ffffff 22%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -733,7 +781,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-badge:not(.book-chapter-badge--planned) {
|
.catalog-badge:not(.book-chapter-badge--planned) {
|
||||||
opacity: 0.5;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-chapter-badge--planned {
|
.book-chapter-badge--planned {
|
||||||
|
|
@ -819,6 +867,76 @@ a.catalog-chapter-card {
|
||||||
// Badges & empty state
|
// Badges & empty state
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@mixin catalog-badge-tokens-dark {
|
||||||
|
--badge-foundation-fg: #5eead4;
|
||||||
|
--badge-foundation-bg: rgba(45, 212, 191, 0.22);
|
||||||
|
--badge-foundation-border: rgba(94, 234, 212, 0.45);
|
||||||
|
--badge-dse-fg: #93c5fd;
|
||||||
|
--badge-dse-bg: rgba(96, 165, 250, 0.22);
|
||||||
|
--badge-dse-border: rgba(147, 197, 253, 0.45);
|
||||||
|
--badge-ib-fg: #c4b5fd;
|
||||||
|
--badge-ib-bg: rgba(167, 139, 250, 0.22);
|
||||||
|
--badge-ib-border: rgba(196, 181, 253, 0.45);
|
||||||
|
--badge-a-level-fg: #6ee7b7;
|
||||||
|
--badge-a-level-bg: rgba(52, 211, 153, 0.2);
|
||||||
|
--badge-a-level-border: rgba(110, 231, 183, 0.42);
|
||||||
|
--badge-ap-fg: #fcd34d;
|
||||||
|
--badge-ap-bg: rgba(251, 191, 36, 0.2);
|
||||||
|
--badge-ap-border: rgba(252, 211, 77, 0.45);
|
||||||
|
--badge-igcse-fg: #fda4af;
|
||||||
|
--badge-igcse-bg: rgba(251, 113, 133, 0.2);
|
||||||
|
--badge-igcse-border: rgba(253, 164, 175, 0.45);
|
||||||
|
--badge-common-core-fg: #93c5fd;
|
||||||
|
--badge-common-core-bg: rgba(59, 130, 246, 0.22);
|
||||||
|
--badge-common-core-border: rgba(147, 197, 253, 0.45);
|
||||||
|
--badge-extension-fg: #fdba74;
|
||||||
|
--badge-extension-bg: rgba(251, 146, 60, 0.22);
|
||||||
|
--badge-extension-border: rgba(253, 186, 116, 0.45);
|
||||||
|
--badge-default-fg: #e7e5e0;
|
||||||
|
--badge-default-bg: rgba(236, 234, 228, 0.1);
|
||||||
|
--badge-default-border: rgba(236, 234, 228, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--badge-foundation-fg: #0f766e;
|
||||||
|
--badge-foundation-bg: rgba(20, 184, 166, 0.12);
|
||||||
|
--badge-foundation-border: rgba(20, 184, 166, 0.28);
|
||||||
|
--badge-dse-fg: #1d4ed8;
|
||||||
|
--badge-dse-bg: rgba(59, 130, 246, 0.12);
|
||||||
|
--badge-dse-border: rgba(59, 130, 246, 0.25);
|
||||||
|
--badge-ib-fg: #6d28d9;
|
||||||
|
--badge-ib-bg: rgba(139, 92, 246, 0.12);
|
||||||
|
--badge-ib-border: rgba(139, 92, 246, 0.25);
|
||||||
|
--badge-a-level-fg: #047857;
|
||||||
|
--badge-a-level-bg: rgba(16, 185, 129, 0.12);
|
||||||
|
--badge-a-level-border: rgba(16, 185, 129, 0.25);
|
||||||
|
--badge-ap-fg: #b45309;
|
||||||
|
--badge-ap-bg: rgba(245, 158, 11, 0.12);
|
||||||
|
--badge-ap-border: rgba(245, 158, 11, 0.25);
|
||||||
|
--badge-igcse-fg: #be123c;
|
||||||
|
--badge-igcse-bg: rgba(244, 63, 94, 0.12);
|
||||||
|
--badge-igcse-border: rgba(244, 63, 94, 0.25);
|
||||||
|
--badge-common-core-fg: #1e40af;
|
||||||
|
--badge-common-core-bg: rgba(37, 99, 235, 0.1);
|
||||||
|
--badge-common-core-border: rgba(37, 99, 235, 0.25);
|
||||||
|
--badge-extension-fg: #9a3412;
|
||||||
|
--badge-extension-bg: rgba(234, 88, 12, 0.1);
|
||||||
|
--badge-extension-border: rgba(234, 88, 12, 0.28);
|
||||||
|
--badge-default-fg: var(--soft-ink);
|
||||||
|
--badge-default-bg: var(--paper);
|
||||||
|
--badge-default-border: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
@include catalog-badge-tokens-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
@include catalog-badge-tokens-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-badge {
|
.catalog-badge {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
@ -830,51 +948,57 @@ a.catalog-chapter-card {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
&--foundation {
|
&--foundation {
|
||||||
color: #0f766e;
|
color: var(--badge-foundation-fg);
|
||||||
background: rgba(20, 184, 166, 0.12);
|
background: var(--badge-foundation-bg);
|
||||||
border-color: rgba(20, 184, 166, 0.28);
|
border-color: var(--badge-foundation-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--dse {
|
&--dse {
|
||||||
color: #1d4ed8;
|
color: var(--badge-dse-fg);
|
||||||
background: rgba(59, 130, 246, 0.12);
|
background: var(--badge-dse-bg);
|
||||||
border-color: rgba(59, 130, 246, 0.25);
|
border-color: var(--badge-dse-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--ib {
|
&--ib {
|
||||||
color: #6d28d9;
|
color: var(--badge-ib-fg);
|
||||||
background: rgba(139, 92, 246, 0.12);
|
background: var(--badge-ib-bg);
|
||||||
border-color: rgba(139, 92, 246, 0.25);
|
border-color: var(--badge-ib-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--a-level {
|
&--a-level {
|
||||||
color: #047857;
|
color: var(--badge-a-level-fg);
|
||||||
background: rgba(16, 185, 129, 0.12);
|
background: var(--badge-a-level-bg);
|
||||||
border-color: rgba(16, 185, 129, 0.25);
|
border-color: var(--badge-a-level-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--ap {
|
&--ap {
|
||||||
color: #b45309;
|
color: var(--badge-ap-fg);
|
||||||
background: rgba(245, 158, 11, 0.12);
|
background: var(--badge-ap-bg);
|
||||||
border-color: rgba(245, 158, 11, 0.25);
|
border-color: var(--badge-ap-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--igcse {
|
&--igcse {
|
||||||
color: #be123c;
|
color: var(--badge-igcse-fg);
|
||||||
background: rgba(244, 63, 94, 0.12);
|
background: var(--badge-igcse-bg);
|
||||||
border-color: rgba(244, 63, 94, 0.25);
|
border-color: var(--badge-igcse-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--common-core {
|
||||||
|
color: var(--badge-common-core-fg);
|
||||||
|
background: var(--badge-common-core-bg);
|
||||||
|
border-color: var(--badge-common-core-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--extension {
|
&--extension {
|
||||||
color: #9a3412;
|
color: var(--badge-extension-fg);
|
||||||
background: rgba(234, 88, 12, 0.1);
|
background: var(--badge-extension-bg);
|
||||||
border-color: rgba(234, 88, 12, 0.28);
|
border-color: var(--badge-extension-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--default {
|
&--default {
|
||||||
color: var(--soft-ink);
|
color: var(--badge-default-fg);
|
||||||
background: var(--paper);
|
background: var(--badge-default-bg);
|
||||||
border-color: var(--line);
|
border-color: var(--badge-default-border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,6 +1027,171 @@ a.catalog-chapter-card {
|
||||||
color: var(--soft-ink);
|
color: var(--soft-ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Compare doc (mathematics curricula)
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.catalog-shell--compare .catalog-content {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc {
|
||||||
|
max-width: 56rem;
|
||||||
|
padding: 8px 24px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__lead {
|
||||||
|
margin: 0 0 24px;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--soft-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__section {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__heading {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__subheading {
|
||||||
|
margin: 20px 0 8px;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__legend {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px 20px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--soft-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--card-bg);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody th[scope="row"] {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__caption {
|
||||||
|
padding: 12px 14px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
caption-side: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chapter a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-cell {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 2.25rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&--core {
|
||||||
|
color: var(--badge-foundation-fg);
|
||||||
|
background: var(--badge-foundation-bg);
|
||||||
|
border: 1px solid var(--badge-foundation-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--ext {
|
||||||
|
color: var(--badge-dse-fg);
|
||||||
|
background: var(--badge-dse-bg);
|
||||||
|
border: 1px solid var(--badge-dse-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--rel {
|
||||||
|
color: var(--badge-ib-fg);
|
||||||
|
background: var(--badge-ib-bg);
|
||||||
|
border: 1px solid var(--badge-ib-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--none {
|
||||||
|
color: var(--muted);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__refs {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-compare-doc__updated {
|
||||||
|
margin: 20px 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Responsive
|
// Responsive
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
@import "tokens";
|
@import "tokens";
|
||||||
@import "particle-background";
|
@import "particle-background";
|
||||||
@import "grain";
|
// @import "grain";
|
||||||
@import "layout";
|
@import "layout";
|
||||||
@import "components";
|
@import "components";
|
||||||
@import "why-sections";
|
@import "why-sections";
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,9 @@
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
|
<template id="catalog-compare-template" hidden>
|
||||||
|
{% include "partials/math-curricula-compare.html" %}
|
||||||
|
</template>
|
||||||
|
|
||||||
<div id="catalog-app" class="catalog-page"></div>
|
<div id="catalog-app" class="catalog-page"></div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ content/{subject}/{slug}/ → live page (one folder)
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>List</strong> — numbered cards; live chapters link from the whole card.</li>
|
<li><strong>List</strong> — numbered cards; live chapters link from the whole card.</li>
|
||||||
<li><strong>Map</strong> (<code>?view=tree</code>) — Mermaid map with strand columns; live chapters link from the <strong>title</strong> only (<code>→</code> + hover underline).</li>
|
<li><strong>Map</strong> (<code>?view=tree</code>) — Mermaid map with strand columns; live chapters link from the <strong>title</strong> only (<code>→</code> + hover underline).</li>
|
||||||
|
<li><strong>Compare</strong> (<code>?view=compare</code>, Mathematics only) — curricula comparison doc (HTML partial, not JSON).</li>
|
||||||
<li>Curriculum names (DSE, IB, …) appear <strong>only in the catalog</strong>, never inside lesson prose.</li>
|
<li>Curriculum names (DSE, IB, …) appear <strong>only in the catalog</strong>, never inside lesson prose.</li>
|
||||||
<li>Every chapter needs a <code>description</code> (1–2 sentences; map clamps to three lines).</li>
|
<li>Every chapter needs a <code>description</code> (1–2 sentences; map clamps to three lines).</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
{# Requires math_catalog in parent template scope #}
|
||||||
|
<div class="catalog-compare-doc__inner book-prose">
|
||||||
|
<p class="catalog-compare-doc__lead">
|
||||||
|
Open Free Books maps chapters to popular school and exam frameworks worldwide. This page summarises
|
||||||
|
how those curricula differ in philosophy and pacing, and how our catalog chapters align. Curriculum
|
||||||
|
labels appear only here and in catalog filters — not inside lesson pages.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section class="catalog-compare-doc__section" aria-labelledby="compare-legend">
|
||||||
|
<h2 id="compare-legend" class="catalog-compare-doc__heading">Legend</h2>
|
||||||
|
<ul class="catalog-compare-doc__legend">
|
||||||
|
<li><span class="catalog-compare-cell catalog-compare-cell--core">core</span> — explicit unit in that framework at this level</li>
|
||||||
|
<li><span class="catalog-compare-cell catalog-compare-cell--ext">ext</span> — extension, elective, or higher tier only</li>
|
||||||
|
<li><span class="catalog-compare-cell catalog-compare-cell--rel">rel</span> — related supporting topic, not a named syllabus unit</li>
|
||||||
|
<li><span class="catalog-compare-cell catalog-compare-cell--none">—</span> — not mapped for this chapter</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="catalog-compare-doc__section" aria-labelledby="compare-frameworks">
|
||||||
|
<h2 id="compare-frameworks" class="catalog-compare-doc__heading">Framework comparison</h2>
|
||||||
|
<div class="catalog-compare-doc__table-wrap">
|
||||||
|
<table class="catalog-compare-table catalog-compare-table--frameworks">
|
||||||
|
<caption class="catalog-compare-table__caption">
|
||||||
|
Philosophy, pacing, and distinguishing features across frameworks covered in our research.
|
||||||
|
</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Dimension</th>
|
||||||
|
<th scope="col">Foundation (OFB)</th>
|
||||||
|
<th scope="col">DSE / HKDSE</th>
|
||||||
|
<th scope="col">IGCSE</th>
|
||||||
|
<th scope="col">A-Level</th>
|
||||||
|
<th scope="col">AP (US)</th>
|
||||||
|
<th scope="col">IB AA</th>
|
||||||
|
<th scope="col">IB AI</th>
|
||||||
|
<th scope="col">Common Core</th>
|
||||||
|
<th scope="col">Singapore</th>
|
||||||
|
<th scope="col">UK GCSE</th>
|
||||||
|
<th scope="col">Gaokao</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Philosophy</th>
|
||||||
|
<td>Prerequisite-first bridge from arithmetic to secondary topics; CPA-friendly sequencing.</td>
|
||||||
|
<td>Exam-focused compulsory syllabus (S4–S6); applied contexts in Number & Algebra.</td>
|
||||||
|
<td>Cambridge international; broad Number, Algebra, Geometry, Statistics.</td>
|
||||||
|
<td>Two-year depth in Pure, Statistics, Mechanics; proof and abstraction.</td>
|
||||||
|
<td>Elective acceleration; AP Calculus AB/BC in Grade 12 for many students.</td>
|
||||||
|
<td>Pure, theoretical; proofs, calculus, abstract reasoning for STEM.</td>
|
||||||
|
<td>Applied modelling; statistics, technology, real-world interpretation.</td>
|
||||||
|
<td>Conceptual understanding + mathematical practices across K–12.</td>
|
||||||
|
<td>Mastery; “teach less, learn more”; CPA and bar models.</td>
|
||||||
|
<td>Key Stages; mandatory GCSE at 16; tiered papers.</td>
|
||||||
|
<td>National entrance exam; extreme rigour and time pressure.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Pacing</th>
|
||||||
|
<td>Split integers vs rationals/proportion before synthesis chapter.</td>
|
||||||
|
<td>Compulsory part bundles secondary algebra after junior foundation.</td>
|
||||||
|
<td>Linear progression to IGCSE at ~16.</td>
|
||||||
|
<td>Slower two-year HL/AL build than single-year AP calculus.</td>
|
||||||
|
<td>Algebra I → Geometry → Algebra II; calculus optional via AP.</td>
|
||||||
|
<td>SL/HL tracks; AA emphasises theory.</td>
|
||||||
|
<td>SL/HL tracks; AI emphasises data and modelling.</td>
|
||||||
|
<td>More topics per year than Singapore; grade-level benchmarks.</td>
|
||||||
|
<td>Deep focus on fewer topics per level.</td>
|
||||||
|
<td>KS3 then GCSE; optional A-Level.</td>
|
||||||
|
<td>Integrated secondary syllabus toward one high-stakes exam.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Distinctive focus</th>
|
||||||
|
<td>Explicit graph prerequisites; global labels in catalog only.</td>
|
||||||
|
<td>Three strands: Number & Algebra, Measures/Shape/Space, Data Handling.</td>
|
||||||
|
<td>International portability; strong algebra and trig base.</td>
|
||||||
|
<td>Complex numbers, induction, mechanics optional components.</td>
|
||||||
|
<td>Standardised AP exams; college credit pathway.</td>
|
||||||
|
<td>Formal proofs and advanced calculus preparation.</td>
|
||||||
|
<td>Calculator-forward statistics and function modelling.</td>
|
||||||
|
<td>Reasoning, modelling, precision in standards.</td>
|
||||||
|
<td>Number bonds, mental strategies, visualisation.</td>
|
||||||
|
<td>Tiered foundation/higher GCSE content.</td>
|
||||||
|
<td>Multi-step synthesis under exam conditions.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="catalog-compare-doc__section" aria-labelledby="compare-elaboration">
|
||||||
|
<h2 id="compare-elaboration" class="catalog-compare-doc__heading">Framework notes</h2>
|
||||||
|
|
||||||
|
<h3 class="catalog-compare-doc__subheading">Foundation (Open Free Books)</h3>
|
||||||
|
<p>
|
||||||
|
Our Foundation track is not a national syllabus — it is the pre-secondary path in this catalog.
|
||||||
|
Research on middle-years progression (integers, rational numbers, proportion) showed a gap between
|
||||||
|
whole-number written calculation and secondary “directed numbers & rates” work. Five planned
|
||||||
|
chapters now sit in the prerequisite graph before the live synthesis chapter.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="catalog-compare-doc__subheading">Hong Kong DSE (compulsory)</h3>
|
||||||
|
<p>
|
||||||
|
The HKDSE compulsory mathematics curriculum organises secondary topics into Number & Algebra,
|
||||||
|
Measures, Shape and Space, and Data Handling. Our chapter graph and strand layout follow that shape
|
||||||
|
while remaining usable for learners outside Hong Kong.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="catalog-compare-doc__subheading">Singapore Mathematics</h3>
|
||||||
|
<p>
|
||||||
|
Emphasises mastery through the Concrete–Pictorial–Abstract sequence, number bonds, and bar modelling.
|
||||||
|
Fewer topics per year, deeper fluency — a useful contrast to broader spiral systems.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="catalog-compare-doc__subheading">Common Core (United States)</h3>
|
||||||
|
<p>
|
||||||
|
Shifts K–12 toward conceptual understanding and mathematical practices (reasoning, modelling,
|
||||||
|
precision). High school often follows Algebra I, Geometry, Algebra II; calculus is commonly via AP
|
||||||
|
rather than mandated in the core standards.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="catalog-compare-doc__subheading">IGCSE, GCSE, A-Level (UK / Cambridge)</h3>
|
||||||
|
<p>
|
||||||
|
GCSE and IGCSE provide broad 14–16 coverage; A-Level allows two years for pure mathematics,
|
||||||
|
statistics, and mechanics — generally more time for abstraction than a single-year AP calculus course.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="catalog-compare-doc__subheading">International Baccalaureate</h3>
|
||||||
|
<p>
|
||||||
|
Students choose Analysis & Approaches (theory, proofs, calculus) or Applications &
|
||||||
|
Interpretation (statistics, modelling, technology). The catalog uses one <code>IB</code> filter chip;
|
||||||
|
this page separates AA and AI for comparison.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="catalog-compare-doc__subheading">Gaokao (China)</h3>
|
||||||
|
<p>
|
||||||
|
The national college entrance mathematics paper integrates advanced algebra, calculus, probability,
|
||||||
|
and coordinate geometry under high-stakes time limits — typically more synthesis per item than many
|
||||||
|
Western school-leaving exams.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="catalog-compare-doc__section" aria-labelledby="compare-chapters">
|
||||||
|
<h2 id="compare-chapters" class="catalog-compare-doc__heading">Chapter coverage</h2>
|
||||||
|
<p>
|
||||||
|
Each cell comes from <code>curriculumCoverage</code> in
|
||||||
|
<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>
|
||||||
|
<div class="catalog-compare-doc__table-wrap">
|
||||||
|
<table class="catalog-compare-table catalog-compare-table--chapters">
|
||||||
|
<caption class="catalog-compare-table__caption">
|
||||||
|
Open Free Books mathematics chapters vs catalog curriculum labels.
|
||||||
|
</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Chapter</th>
|
||||||
|
<th scope="col">Foundation</th>
|
||||||
|
<th scope="col">DSE</th>
|
||||||
|
<th scope="col">IGCSE</th>
|
||||||
|
<th scope="col">A-Level</th>
|
||||||
|
<th scope="col">AP</th>
|
||||||
|
<th scope="col">IB</th>
|
||||||
|
<th scope="col">Common Core</th>
|
||||||
|
<th scope="col">Singapore</th>
|
||||||
|
<th scope="col">UK GCSE</th>
|
||||||
|
<th scope="col">Gaokao</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for strand in math_catalog.strands %}
|
||||||
|
{% for chapter in strand.chapters %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="catalog-compare-table__chapter">
|
||||||
|
{% if chapter.status == "live" %}
|
||||||
|
<a href="{{ get_url(path='/math/' ~ chapter.slug ~ '/') | safe }}">{{ chapter.title }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ chapter.title }}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
{% for col in ["Foundation", "DSE", "IGCSE", "A-Level", "AP", "IB", "Common Core", "Singapore", "UK GCSE", "Gaokao"] %}
|
||||||
|
<td class="catalog-compare-table__cell">
|
||||||
|
{% set role = chapter.curriculumCoverage[col] | default(value="") %}
|
||||||
|
{% if role == "core" %}
|
||||||
|
<span class="catalog-compare-cell catalog-compare-cell--core" title="Core topic in this framework">core</span>
|
||||||
|
{% elif role == "extension" %}
|
||||||
|
<span class="catalog-compare-cell catalog-compare-cell--ext" title="Extension, elective, or higher tier only">ext</span>
|
||||||
|
{% elif role == "related" %}
|
||||||
|
<span class="catalog-compare-cell catalog-compare-cell--rel" title="Related supporting topic, not a named unit">rel</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="catalog-compare-cell catalog-compare-cell--none" aria-label="Not mapped">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="catalog-compare-doc__section" aria-labelledby="compare-references">
|
||||||
|
<h2 id="compare-references" class="catalog-compare-doc__heading">References</h2>
|
||||||
|
<p>Official and secondary sources used when mapping frameworks (verify links periodically).</p>
|
||||||
|
<ul class="catalog-compare-doc__refs">
|
||||||
|
<li>
|
||||||
|
<a href="https://www.hkeaa.edu.hk/en/hkdse/assessment/subject_information_category.html" rel="noopener noreferrer">HKEAA — HKDSE subject information</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.cambridgeinternational.org/programmes-and-qualifications/cambridge-igcse-mathematics-0580/" rel="noopener noreferrer">Cambridge International — IGCSE Mathematics (0580)</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.cambridgeinternational.org/programmes-and-qualifications/cambridge-international-as-and-a-level-mathematics-9709/" rel="noopener noreferrer">Cambridge International — A Level Mathematics (9709)</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://apstudents.collegeboard.org/courses/ap-calculus-ab" rel="noopener noreferrer">College Board — AP Calculus AB</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.ibo.org/programmes/diploma-programme/curriculum/mathematics/" rel="noopener noreferrer">IBO — Diploma Programme mathematics</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.thecorestandards.org/Math/" rel="noopener noreferrer">Common Core State Standards — Mathematics</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.moe.gov.sg/primary/mathematics" rel="noopener noreferrer">Singapore MOE — Primary mathematics</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.gov.uk/government/collections/national-curriculum" rel="noopener noreferrer">UK Government — National curriculum</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.guruforu.com/blog/math/math-curriculum-global-guide" rel="noopener noreferrer">Global curriculum overview (guruforu.com)</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="catalog-compare-doc__updated">
|
||||||
|
Research notes migrated from project curriculum gap analysis (2025–2026). Graph prerequisites follow
|
||||||
|
integer / rational / proportion scaffolding before the live directed-numbers synthesis chapter.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue