mirror of
https://github.com/tiennm99/mathmax.git
synced 2026-06-17 14:48:38 +00:00
docs: brainstorm survey + lesson 4+5 plan
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "KaTeX integration"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: "2h"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 01: KaTeX integration
|
||||
|
||||
## Overview
|
||||
Add KaTeX to the project, wrap it in a single `<Tex>` Svelte component, and verify rendering in a sandbox route. This is the only place KaTeX is touched directly — every lesson uses `<Tex math="..." />`.
|
||||
|
||||
## Requirements
|
||||
- Functional: render inline + display math from a string prop; SSR-safe (KaTeX renders to static HTML, no client JS needed for display)
|
||||
- Non-functional: no FOUC; CSS loaded once at app level; no per-component CSS imports
|
||||
- A11y: KaTeX outputs MathML alongside HTML — keep it; `aria-label` fallback prop for screen readers when math expression is non-trivial
|
||||
|
||||
## Architecture
|
||||
- KaTeX has two APIs: `katex.render(string, element, options)` (mutates DOM) and `katex.renderToString(string, options)` (returns HTML string).
|
||||
- Use `renderToString` — pure function, SSR-friendly, plugs into `{@html}`. No `$effect` needed.
|
||||
- CSS imported once in `+layout.svelte` so it's in the global bundle.
|
||||
- KaTeX fonts are auto-fetched via CSS `@font-face` from the package's `dist/fonts/` — Vite's static asset handling picks them up.
|
||||
|
||||
## Related Code Files
|
||||
- Create: `src/lib/components/tex.svelte` — `<Tex>` component
|
||||
- Create: `src/routes/__sandbox/katex/+page.svelte` — smoke test route (delete in Phase 05 or leave guarded by `dev` mode)
|
||||
- Modify: `src/routes/+layout.svelte` — add `import 'katex/dist/katex.min.css'`
|
||||
- Modify: `package.json` — add `katex` dep + `@types/katex` is unnecessary (we use JSDoc)
|
||||
|
||||
## Implementation Steps
|
||||
1. `npm install katex@^0.16.x` — pin minor; KaTeX is mature, breaking changes rare.
|
||||
2. Create `src/lib/components/tex.svelte`:
|
||||
```svelte
|
||||
<script>
|
||||
import katex from 'katex';
|
||||
/** @type {{ math: string, display?: boolean, ariaLabel?: string }} */
|
||||
let { math, display = false, ariaLabel } = $props();
|
||||
const html = katex.renderToString(math, {
|
||||
displayMode: display,
|
||||
throwOnError: false,
|
||||
output: 'htmlAndMathml',
|
||||
});
|
||||
</script>
|
||||
{#if ariaLabel}
|
||||
<span aria-label={ariaLabel}>{@html html}</span>
|
||||
{:else}
|
||||
{@html html}
|
||||
{/if}
|
||||
```
|
||||
3. Add `import 'katex/dist/katex.min.css';` at top of `<script>` block in `src/routes/+layout.svelte`.
|
||||
4. Create sandbox route `src/routes/__sandbox/katex/+page.svelte` rendering 4-5 expressions covering: inline (`a^2 - b^2`), display (`\frac{a \cdot b}{\gcd(a,b)}`), Vietnamese mixed text, Greek letters, fractions with sqrt.
|
||||
5. `npm run dev` — visit `/__sandbox/katex/` — verify rendering; check no FOUC; confirm fonts load (Network tab).
|
||||
6. `npm run build` — verify static prerender includes KaTeX HTML inline (view-source on built file).
|
||||
7. `npm run check` — must stay green.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `<Tex math="..." />` renders inline math
|
||||
- [ ] `<Tex math="..." display />` renders display math (centered, larger)
|
||||
- [ ] No console errors / warnings on dev or build
|
||||
- [ ] Built static HTML contains rendered math (not just `<Tex>` placeholder)
|
||||
- [ ] Bundle size delta logged in plan.md "Success criteria" section
|
||||
- [ ] svelte-check clean
|
||||
|
||||
## Risk Assessment
|
||||
- **Risk**: KaTeX bundle size (~300KB gzip with fonts) inflates Lighthouse perf score.
|
||||
**Mitigation**: Acceptable trade for math site; document in `RUNBOOK.md`. If we need to shed weight later, we can lazy-load KaTeX per-route.
|
||||
- **Risk**: Font loading flash.
|
||||
**Mitigation**: KaTeX CSS uses `font-display: swap`; system fallback is acceptable for the ~50ms gap.
|
||||
- **Risk**: SSR mismatch warnings (Svelte hydration).
|
||||
**Mitigation**: `renderToString` is deterministic — no mismatch expected. Verify in dev console.
|
||||
|
||||
## Notes
|
||||
- Sandbox route name uses `__sandbox/` prefix so it's obvious it's not user-facing. Decide in Phase 05 whether to delete or keep behind `if (dev)` guard.
|
||||
- Do NOT add `mhchem` / `copy-tex` extensions — YAGNI.
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "Số học engine + GCD lesson"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: "6h"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 02: Số học engine + GCD lesson (Ước chung lớn nhất)
|
||||
|
||||
## Overview
|
||||
Build a pure `numtheory-engine` module (mirrors `geom-engine` shape) and ship the first số học lesson: a step-by-step Euclidean algorithm visualizer for `gcd(a, b)` with `lcm` derived from the result.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- Two number inputs for `a, b` (1 ≤ a, b ≤ 999)
|
||||
- "Step" button advances Euclidean reduction one row at a time
|
||||
- "Tự động" button runs full reduction
|
||||
- "Khởi động lại" resets to current inputs
|
||||
- Final row shows `gcd(a, b) = N` highlighted
|
||||
- LCM formula + value displayed once gcd is found: `lcm(a,b) = (a·b)/gcd(a,b)`
|
||||
- Non-functional:
|
||||
- Pure `gcdSteps(a, b)` returns the trace (no DOM coupling)
|
||||
- All terminology + example aligned to **Cánh Diều lớp 6 chương "Số tự nhiên"**
|
||||
- Vietnamese-first slugs and copy
|
||||
- A11y:
|
||||
- Number inputs labeled (`<label for=>`)
|
||||
- Step button keyboard-focusable
|
||||
- `aria-live="polite"` on the reduction table
|
||||
- Each new row announced
|
||||
|
||||
## Architecture
|
||||
- Engine is pure JS + JSDoc; sibling shape to `geom-engine`.
|
||||
- `gcdSteps` returns an array of `{ a, b, q, r }` rows so the UI is pure render.
|
||||
- Lesson page owns `$state` for input values + step index; uses `$derived` for current visible rows + lcm value.
|
||||
- Goal-state pattern (documented inline): `goal(state) → { reached: state.stepIdx === steps.length, hint: ... }`. Not extracted to shared module — first instance.
|
||||
|
||||
## Related Code Files
|
||||
- Create: `src/lib/numtheory-engine/gcd.js` — `gcd`, `lcm`, `gcdSteps`
|
||||
- Create: `src/lib/numtheory-engine/gcd.test.js` — Vitest unit tests
|
||||
- Create: `src/lib/numtheory-engine/index.js` — barrel export
|
||||
- Create: `src/lib/lessons/uoc-chung-lon-nhat/copy.vi.js` — VN copy
|
||||
- Create: `src/routes/so-hoc/uoc-chung-lon-nhat/+page.svelte` — lesson page
|
||||
- Modify (Phase 04): `src/lib/lessons/registry.js`, root `+page.svelte`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 2.1 Engine
|
||||
1. `gcd(a, b)`:
|
||||
```js
|
||||
/** @param {number} a @param {number} b @returns {number} */
|
||||
export function gcd(a, b) {
|
||||
a = Math.abs(a); b = Math.abs(b);
|
||||
while (b !== 0) { [a, b] = [b, a % b]; }
|
||||
return a;
|
||||
}
|
||||
```
|
||||
2. `lcm(a, b)`:
|
||||
```js
|
||||
/** Throws if both 0 (lcm undefined). */
|
||||
export function lcm(a, b) {
|
||||
if (a === 0 && b === 0) throw new Error('lcm(0,0) undefined');
|
||||
return Math.abs(a * b) / gcd(a, b);
|
||||
}
|
||||
```
|
||||
3. `gcdSteps(a, b)` returns `Array<{a:number, b:number, q:number, r:number}>`:
|
||||
- Each row: current pair before reduction, quotient `q = floor(a/b)`, remainder `r = a % b`
|
||||
- Terminates when `r === 0`; final row's `b` is the gcd
|
||||
4. Tests: cover `gcd(48, 18) === 6`, `gcd(0, 5) === 5`, `gcd(7, 0) === 7`, `gcd(13, 17) === 1` (coprime), `lcm(4, 6) === 12`, `gcdSteps(48, 18)` produces expected 3-row trace.
|
||||
|
||||
### 2.2 Copy module
|
||||
Create `copy.vi.js`:
|
||||
```js
|
||||
export const vi = {
|
||||
slug: 'uoc-chung-lon-nhat',
|
||||
topic: 'so-hoc',
|
||||
grade: 'lop-6',
|
||||
title: 'Ước chung lớn nhất (Thuật toán Euclid)',
|
||||
gradeLabel: 'Lớp 6',
|
||||
intro: 'ƯCLN của hai số là số lớn nhất chia hết cả hai. Thuật toán Euclid tìm nó bằng cách thay liên tiếp (a, b) bằng (b, a mod b) cho đến khi b = 0.',
|
||||
instruction: 'Nhập hai số rồi nhấn "Bước" để xem từng bước rút gọn',
|
||||
resultBadge: (g) => `ƯCLN = ${g}`,
|
||||
lcmLabel: 'Bội chung nhỏ nhất',
|
||||
theoremTitle: 'Thuật toán Euclid',
|
||||
theoremStatement: 'Với mọi a, b ≥ 0 (không cùng bằng 0): gcd(a, b) = gcd(b, a mod b). Khi b = 0, gcd = a.',
|
||||
exampleTitle: 'Ví dụ',
|
||||
exampleBody: 'Tìm ƯCLN(48, 18). Bước 1: 48 = 2·18 + 12. Bước 2: 18 = 1·12 + 6. Bước 3: 12 = 2·6 + 0. Vậy ƯCLN = 6.',
|
||||
nextTeaser: 'Sắp ra mắt: BCNN bằng phân tích thừa số nguyên tố',
|
||||
};
|
||||
```
|
||||
|
||||
### 2.3 Lesson page
|
||||
- `+page.svelte` owns:
|
||||
- `let a = $state(48); let b = $state(18);`
|
||||
- `let stepIdx = $state(0);`
|
||||
- `let steps = $derived(gcdSteps(a, b));`
|
||||
- `let visibleSteps = $derived(steps.slice(0, stepIdx));`
|
||||
- `let g = $derived(stepIdx === steps.length && steps.length > 0 ? steps[steps.length - 1].b : null);`
|
||||
- `let l = $derived(g !== null && g !== 0 ? lcm(a, b) : null);`
|
||||
- UI sections:
|
||||
1. Title + grade badge
|
||||
2. Intro paragraph
|
||||
3. Two number inputs (a, b) + reset button (resets stepIdx to 0)
|
||||
4. "Bước" button (disabled when stepIdx === steps.length); "Tự động" button (sets stepIdx = steps.length)
|
||||
5. `<table>` rendering `visibleSteps`: columns "a", "b", "q", "r" — header "a = q·b + r"
|
||||
6. When `g !== null`: highlighted result + `<Tex math={\`\\gcd(${a}, ${b}) = ${g}\`} display />`
|
||||
7. When `l !== null`: `<Tex math={\`\\text{lcm}(${a}, ${b}) = \\frac{${a} \\cdot ${b}}{${g}} = ${l}\`} display />`
|
||||
8. Theorem block + example (from copy)
|
||||
9. Next teaser
|
||||
|
||||
### 2.4 SEO meta
|
||||
- `<svelte:head>` with `<title>{copy.title} — MathMax</title>`, `<meta name="description" content={copy.intro}>`, canonical URL via `$page.url`.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `gcd.test.js` covers gcd, lcm, gcdSteps with 6+ assertions; all pass
|
||||
- [ ] Lesson renders at `/so-hoc/uoc-chung-lon-nhat/` in dev + build
|
||||
- [ ] Stepping through 48, 18 produces 3 rows ending in gcd=6
|
||||
- [ ] LCM displays correct value (144 for 48,18) once gcd found
|
||||
- [ ] Reset button restores stepIdx=0 + clears LCM display
|
||||
- [ ] Number inputs validate (1-999); invalid input doesn't crash
|
||||
- [ ] Keyboard nav: Tab through inputs → buttons; Enter triggers Step button
|
||||
- [ ] svelte-check clean; vitest green
|
||||
|
||||
## Risk Assessment
|
||||
- **Risk**: User enters 0 or negative → `gcdSteps` infinite loop or wrong output.
|
||||
**Mitigation**: Input `min=1 max=999`; `gcdSteps` defensive: if `b === 0` initial, return single row immediately.
|
||||
- **Risk**: Cánh Diều lớp 6 may use specific notation (e.g. `ƯCLN(a,b)` vs `gcd(a,b)`).
|
||||
**Mitigation**: Copy uses `ƯCLN` per Vietnamese standard; KaTeX uses `\gcd` LaTeX command (renders as "gcd"). Add a one-line `\text{ƯCLN}` substitution if textbook check shows preference.
|
||||
- **Risk**: Lesson page exceeds 200 LOC (modularization rule).
|
||||
**Mitigation**: If approaching, extract reduction `<table>` into a tiny `gcd-trace-table.svelte` component. Defer until needed.
|
||||
|
||||
## Notes
|
||||
- No drag interaction in this lesson — just inputs + buttons + table. Doesn't touch `draggable.svelte.js`.
|
||||
- Goal-state in this lesson is implicit: "all rows revealed → gcd shown → lcm shown." No formal `goal()` function v1; just `$derived`.
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "Đại số factor a²-b² lesson"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: "5h"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 03: Đại số — Hiệu hai bình phương (a² − b² visualizer)
|
||||
|
||||
## Overview
|
||||
Interactive geometric proof of `a² − b² = (a + b)(a − b)`. Two sliders set `a` and `b`; SVG shows the L-shape (square `a²` minus corner square `b²`) being rearranged into a rectangle of `(a+b) × (a−b)`. KaTeX renders the formula with live numeric values.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- Slider for `a` (range 2-10), slider for `b` (range 1-10, capped at `a`)
|
||||
- SVG shows side-by-side: (1) big square with corner cut, (2) rearranged rectangle
|
||||
- Numeric panel: `a² = N`, `b² = N`, `a² - b² = N`, `(a+b)(a-b) = N`
|
||||
- KaTeX displays: `a^2 - b^2 = (a+b)(a-b)` with current numbers substituted
|
||||
- Edge case `b = a` → both sides = 0; show degenerate state without crashing
|
||||
- Non-functional:
|
||||
- No new engine module (algebra is `a*a - b*b`; YAGNI)
|
||||
- Aligned to Cánh Diều lớp 7 chương "Hằng đẳng thức đáng nhớ"
|
||||
- A11y:
|
||||
- Sliders have `<label>` + `aria-valuetext` (e.g. "a bằng 5")
|
||||
- Numeric panel has `aria-live="polite"`
|
||||
- Reduced motion: no transitions when `prefers-reduced-motion: reduce`
|
||||
|
||||
## Architecture
|
||||
- Pure render lesson — no draggable, no engine module needed.
|
||||
- Math fits inside the lesson page; `a*a - b*b` is too small to extract.
|
||||
- SVG viewBox sized to fit `a + b ≤ 20` worst case; use `viewBox` units = math units * 20px.
|
||||
- Two SVG groups side-by-side, both inside one `<svg>`. CSS Grid for layout.
|
||||
|
||||
## Related Code Files
|
||||
- Create: `src/lib/lessons/hieu-hai-binh-phuong/copy.vi.js`
|
||||
- Create: `src/routes/dai-so/hieu-hai-binh-phuong/+page.svelte`
|
||||
- Read for pattern: `src/routes/hinh-hoc/tam-giac-bang-nhau/+page.svelte` (existing SVG lesson shape)
|
||||
- Modify (Phase 04): `src/lib/lessons/registry.js`, root `+page.svelte`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 3.1 Copy module
|
||||
```js
|
||||
export const vi = {
|
||||
slug: 'hieu-hai-binh-phuong',
|
||||
topic: 'dai-so',
|
||||
grade: 'lop-7',
|
||||
title: 'Hiệu hai bình phương',
|
||||
gradeLabel: 'Lớp 7',
|
||||
intro: 'Hằng đẳng thức a² − b² = (a + b)(a − b). Kéo thanh trượt để thấy vì sao hình vuông cạnh a, cắt đi góc b, có cùng diện tích với hình chữ nhật (a+b)(a−b).',
|
||||
instruction: 'Kéo hai thanh trượt để chọn a và b (với b ≤ a)',
|
||||
identityTitle: 'Hằng đẳng thức',
|
||||
identityStatement: 'Với mọi số thực a, b: a² − b² = (a + b)(a − b).',
|
||||
exampleTitle: 'Ví dụ',
|
||||
exampleBody: 'Tính 7² − 3². Áp dụng hằng đẳng thức: 7² − 3² = (7+3)(7−3) = 10 · 4 = 40. Cũng có thể tính trực tiếp: 49 − 9 = 40.',
|
||||
numericTitle: 'Giá trị',
|
||||
rearrangeNote: 'Phần hình chữ L (diện tích a² − b²) được cắt và ghép lại thành hình chữ nhật cạnh (a + b) × (a − b).',
|
||||
nextTeaser: 'Sắp ra mắt: (a + b)² và (a − b)²',
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 Lesson page state
|
||||
```js
|
||||
let a = $state(5);
|
||||
let b = $state(2);
|
||||
// keep b ≤ a:
|
||||
$effect(() => { if (b > a) b = a; });
|
||||
|
||||
let aSq = $derived(a * a);
|
||||
let bSq = $derived(b * b);
|
||||
let diff = $derived(aSq - bSq);
|
||||
let product = $derived((a + b) * (a - b)); // always === diff; shown for pedagogy
|
||||
```
|
||||
|
||||
### 3.3 SVG layout
|
||||
|
||||
Two diagrams in one `<svg>`:
|
||||
|
||||
**Left diagram — square minus corner**:
|
||||
- Big square: `<rect x=0 y=0 width=a*U height=a*U>` where `U = 20px`
|
||||
- Cut corner: `<rect x=(a-b)*U y=0 width=b*U height=b*U fill="white">`
|
||||
- Labels: "a" along top + left; "b" on the cut edges
|
||||
|
||||
**Right diagram — rearranged rectangle**:
|
||||
- Two pieces side-by-side: top piece `(a-b) × (a+b)` rotated/translated to form `(a+b) × (a-b)`
|
||||
- v1 simplification: just draw the result rectangle `(a+b) × (a-b)` with the two regions colored differently to show the L-shape parts. Don't animate the rearrangement (WAAPI deferred per port spec).
|
||||
- Labels: "a + b" along width; "a − b" along height
|
||||
|
||||
Color tokens (extend `tailwind.config.js` if needed):
|
||||
- L-shape part 1 (top strip): `pair-1`
|
||||
- L-shape part 2 (right strip): `pair-2`
|
||||
- Removed corner: white/transparent
|
||||
|
||||
### 3.4 KaTeX panel
|
||||
```svelte
|
||||
<Tex display math={`${a}^2 - ${b}^2 = (${a}+${b})(${a}-${b}) = ${diff}`} />
|
||||
```
|
||||
Plus a static formula card:
|
||||
```svelte
|
||||
<Tex display math="a^2 - b^2 = (a+b)(a-b)" />
|
||||
```
|
||||
|
||||
### 3.5 SEO meta
|
||||
- Same pattern as Phase 02: `<svelte:head>` with title/desc/canonical.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Lesson renders at `/dai-so/hieu-hai-binh-phuong/`
|
||||
- [ ] Sliders update both diagrams + numeric panel + KaTeX in real time
|
||||
- [ ] `b > a` is auto-corrected via `$effect` (not visible to user as glitch)
|
||||
- [ ] `a = b` degenerate case renders without overlap or NaN
|
||||
- [ ] Reduced-motion respected (no transitions if user prefers it)
|
||||
- [ ] svelte-check clean
|
||||
|
||||
## Risk Assessment
|
||||
- **Risk**: SVG rearrangement might be confusing without animation.
|
||||
**Mitigation**: Static side-by-side with text annotation `rearrangeNote`. Animation in v2.
|
||||
- **Risk**: Sliders on touch devices may be too small.
|
||||
**Mitigation**: Native `<input type="range">` is fine on mobile; add `accent-color` Tailwind class for theming.
|
||||
- **Risk**: Slider $effect to clamp `b ≤ a` may cause feedback loop.
|
||||
**Mitigation**: `$effect` only writes when `b > a` (guard); Svelte 5 batches; tested manually.
|
||||
- **Risk**: Page exceeds 200 LOC.
|
||||
**Mitigation**: Likely tight; if it grows, extract `<square-cut-svg>` and `<rectangle-svg>` as small components. Defer until needed.
|
||||
|
||||
## Notes
|
||||
- Goal-state for this lesson: implicit (sandbox). User experiments freely. No `goal()` function needed.
|
||||
- KaTeX `<Tex>` is the only place we render math text. Don't manually compose math with HTML `<sup>`.
|
||||
- Slider primitive stays raw `<input type="range">` — extracting `<Slider>` is YAGNI at first use.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
phase: 4
|
||||
title: "Registry + landing glue"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "2h"
|
||||
dependencies: [2, 3]
|
||||
---
|
||||
|
||||
# Phase 04: Registry + landing glue
|
||||
|
||||
## Overview
|
||||
Wire the two new lessons into the lesson registry, enable the Số học and Đại số tiles on the root landing, and create topic landing pages at `/so-hoc/` and `/dai-so/`.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- `lessons` registry includes both new lessons (slug, topic, grade, copy)
|
||||
- Root `/` page: Số học + Đại số tiles linked + clickable (no longer "Sắp ra mắt")
|
||||
- `/so-hoc/` lists Ước chung lớn nhất; placeholder text for upcoming lessons
|
||||
- `/dai-so/` lists Hiệu hai bình phương; placeholder text for upcoming lessons
|
||||
- Hình học landing already exists; check it still works
|
||||
- Non-functional:
|
||||
- Topic landing pages reuse the same component structure as `/hinh-hoc/`
|
||||
- SEO meta on every new page
|
||||
|
||||
## Architecture
|
||||
- Registry stays a single `lessons` array with helper `lessonsByTopic`. No restructuring.
|
||||
- Topic landing pages are nearly identical except for the topic filter; copy lives in `src/lib/i18n/site.vi.js` (extend if needed).
|
||||
- Root tile state derived from `lessonsByTopic('so-hoc').length > 0` etc. — auto-enables when first lesson per topic ships.
|
||||
|
||||
## Related Code Files
|
||||
- Modify: `src/lib/lessons/registry.js` — import + push two new copies
|
||||
- Modify: `src/routes/+page.svelte` — root landing tile state
|
||||
- Modify: `src/lib/i18n/site.vi.js` — topic page chrome strings (if not already present)
|
||||
- Create: `src/routes/so-hoc/+page.svelte` — số học topic landing
|
||||
- Create: `src/routes/dai-so/+page.svelte` — đại số topic landing
|
||||
- Read for pattern: `src/routes/hinh-hoc/+page.svelte`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 4.1 Registry update
|
||||
```js
|
||||
// src/lib/lessons/registry.js
|
||||
import { vi as sssCopy } from './tam-giac-bang-nhau/copy.vi.js';
|
||||
import { vi as similarityCopy } from './tam-giac-dong-dang/copy.vi.js';
|
||||
import { vi as inscribedCopy } from './goc-noi-tiep/copy.vi.js';
|
||||
import { vi as gcdCopy } from './uoc-chung-lon-nhat/copy.vi.js';
|
||||
import { vi as diffSquaresCopy } from './hieu-hai-binh-phuong/copy.vi.js';
|
||||
|
||||
export const lessons = [
|
||||
gcdCopy, // số học
|
||||
diffSquaresCopy, // đại số
|
||||
sssCopy, // hình học
|
||||
similarityCopy,
|
||||
inscribedCopy,
|
||||
];
|
||||
```
|
||||
Order: by topic → grade ascending. Nav order will follow the array.
|
||||
|
||||
### 4.2 Root landing
|
||||
- Read current `src/routes/+page.svelte` first (likely uses hard-coded "Sắp ra mắt" labels)
|
||||
- Replace per-topic disabled state with derived check:
|
||||
```js
|
||||
let hasSoHoc = $derived(lessonsByTopic('so-hoc').length > 0);
|
||||
let hasDaiSo = $derived(lessonsByTopic('dai-so').length > 0);
|
||||
let hasHinhHoc = $derived(lessonsByTopic('hinh-hoc').length > 0);
|
||||
```
|
||||
- Tile component:
|
||||
- Enabled → `<a href="/so-hoc/">…</a>` with lesson count badge
|
||||
- Disabled → `<div>` with "Sắp ra mắt" overlay (kept for future-empty topics, none currently)
|
||||
|
||||
### 4.3 Topic landing pages
|
||||
Pattern for both `/so-hoc/+page.svelte` and `/dai-so/+page.svelte`:
|
||||
- Import `lessonsByTopic` from registry
|
||||
- Render a list of lesson cards (title, grade badge, intro snippet, link)
|
||||
- Order: by grade ascending, then by registry position
|
||||
- One placeholder card "Sắp ra mắt" for future lessons (optional; or omit)
|
||||
- `<svelte:head>` with topic-level SEO
|
||||
|
||||
### 4.4 i18n site chrome
|
||||
- Check `src/lib/i18n/site.vi.js` for existing topic strings
|
||||
- Add if missing:
|
||||
```js
|
||||
topics: {
|
||||
'so-hoc': { title: 'Số học', tagline: '...' },
|
||||
'dai-so': { title: 'Đại số', tagline: '...' },
|
||||
'hinh-hoc': { title: 'Hình học', tagline: '...' },
|
||||
}
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Root page tiles for Số học + Đại số are clickable (linked to topic pages)
|
||||
- [ ] `/so-hoc/` shows Ước chung lớn nhất card → links to lesson
|
||||
- [ ] `/dai-so/` shows Hiệu hai bình phương card → links to lesson
|
||||
- [ ] `/hinh-hoc/` still shows all 3 existing lessons (regression check)
|
||||
- [ ] All routes prerender at build (no SSR/CSR mismatch)
|
||||
- [ ] svelte-check clean
|
||||
|
||||
## Risk Assessment
|
||||
- **Risk**: Existing hinh-hoc landing pattern may not directly generalize.
|
||||
**Mitigation**: Read it first; refactor to `lessonsByTopic(topic)` pattern if it isn't already.
|
||||
- **Risk**: Lesson order in registry affects nav consistency.
|
||||
**Mitigation**: Document ordering rule in registry.js comment.
|
||||
|
||||
## Notes
|
||||
- next/prev lesson navigation footer is **deferred** (port spec §13 Q4) — revisit at lesson 6+.
|
||||
- Grade-filter UI on topic pages also deferred (Sub-project §3.4).
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
phase: 5
|
||||
title: "Test + a11y verify"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "3h"
|
||||
dependencies: [4]
|
||||
---
|
||||
|
||||
# Phase 05: Test + a11y verify
|
||||
|
||||
## Overview
|
||||
Run full quality gate before declaring lessons 4 + 5 shippable: Vitest, svelte-check, build, and manual keyboard/screen-reader pass on both new lessons.
|
||||
|
||||
## Requirements
|
||||
- Functional: build passes; all tests green; routes prerender to static HTML
|
||||
- Non-functional: bundle size delta logged; KaTeX impact documented
|
||||
- A11y: keyboard nav verified; ARIA-live announcements work; reduced-motion respected
|
||||
|
||||
## Architecture
|
||||
N/A — verification phase.
|
||||
|
||||
## Related Code Files
|
||||
- Modify (if issues found): any lesson files, draggable action, Tex component
|
||||
- Modify: `README.md` — add new lessons to status section
|
||||
- Possibly modify: `RUNBOOK.md` — note KaTeX bundle impact
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 5.1 Automated checks
|
||||
1. `npm run check` — must be 0 errors, 0 warnings
|
||||
2. `npm run test` — must be all green; verify new `gcd.test.js` runs
|
||||
3. `npm run build` — must succeed; check `build/` for:
|
||||
- `/so-hoc/uoc-chung-lon-nhat/index.html` exists, contains rendered KaTeX HTML
|
||||
- `/dai-so/hieu-hai-binh-phuong/index.html` exists, contains rendered KaTeX HTML
|
||||
- `/so-hoc/index.html`, `/dai-so/index.html`, root `index.html` present
|
||||
4. `npm run preview` — smoke-test all routes manually
|
||||
|
||||
### 5.2 Bundle size delta
|
||||
- Note `build/_app/` total size before this plan vs after
|
||||
- Specifically: KaTeX JS chunk + CSS + fonts
|
||||
- Expected: ~300KB gzipped delta
|
||||
- Document in `plan.md` Success Criteria
|
||||
|
||||
### 5.3 Manual a11y pass — GCD lesson
|
||||
1. Tab through page: number a → number b → reset → bước → tự động
|
||||
2. Enter on bước advances one row; aria-live announces new row
|
||||
3. Screen reader (use VoiceOver or NVDA) announces final gcd correctly
|
||||
4. Increase browser font size 200% — layout doesn't break
|
||||
|
||||
### 5.4 Manual a11y pass — Diff-of-squares lesson
|
||||
1. Tab to slider a → arrow keys change value → aria-valuetext announces "a bằng N"
|
||||
2. Tab to slider b → same
|
||||
3. Numeric panel updates announced (aria-live)
|
||||
4. Enable `prefers-reduced-motion: reduce` (DevTools rendering tab) → no transitions visible
|
||||
5. SVG has `aria-label="Sơ đồ hiệu hai bình phương"` (or similar)
|
||||
|
||||
### 5.5 Mobile / touch check
|
||||
1. Open dev URL on phone or DevTools mobile emulation (iPhone 12)
|
||||
2. GCD lesson: tap number inputs, native keypad shows; buttons big enough to tap
|
||||
3. Factor lesson: drag sliders with finger; numbers update smoothly
|
||||
4. No horizontal scroll on either page
|
||||
|
||||
### 5.6 Sandbox cleanup
|
||||
- Decide: delete `/__sandbox/katex/` route OR guard with `import { dev } from '$app/environment'; if (!dev) error(404)`
|
||||
- Recommend: delete; it served Phase 01 purpose
|
||||
|
||||
### 5.7 README update
|
||||
Update `README.md` Status section:
|
||||
```md
|
||||
## Status
|
||||
|
||||
3 bài hình học + 2 bài đầu tiên cho số học và đại số đã ra mắt.
|
||||
|
||||
- Lớp 6 — Ước chung lớn nhất (Euclid): `/so-hoc/uoc-chung-lon-nhat/`
|
||||
- Lớp 7 — Hiệu hai bình phương: `/dai-so/hieu-hai-binh-phuong/`
|
||||
- Lớp 7 — Tam giác bằng nhau (SSS): `/hinh-hoc/tam-giac-bang-nhau/`
|
||||
- Lớp 8 — Tam giác đồng dạng: `/hinh-hoc/tam-giac-dong-dang/`
|
||||
- Lớp 9 — Góc nội tiếp: `/hinh-hoc/goc-noi-tiep/`
|
||||
```
|
||||
|
||||
### 5.8 RUNBOOK note
|
||||
Add KaTeX bundle impact note to `RUNBOOK.md` under a "Bundle" or "Performance" section (create if absent).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] All automated checks green (check, test, build, preview)
|
||||
- [ ] Bundle delta logged with actual KB number
|
||||
- [ ] Manual a11y pass complete on both new lessons (no findings, or findings filed in next plan)
|
||||
- [ ] Mobile/touch smoke test passes
|
||||
- [ ] Sandbox route removed or dev-guarded
|
||||
- [ ] README + RUNBOOK updated
|
||||
- [ ] No regressions in 3 existing geometry lessons (open each, verify drag still works)
|
||||
|
||||
## Risk Assessment
|
||||
- **Risk**: KaTeX SSR may produce hydration warnings.
|
||||
**Mitigation**: Captured in Phase 01; if it surfaces here, escalate.
|
||||
- **Risk**: Bundle delta exceeds 400KB gzipped (KaTeX is heavier than estimated).
|
||||
**Mitigation**: Acceptable for math site; note in RUNBOOK; consider per-route lazy load in a future plan if Lighthouse perf drops below 90.
|
||||
- **Risk**: Manual a11y findings = blockers.
|
||||
**Mitigation**: If found, fix in this phase; don't punt to next plan.
|
||||
|
||||
## Notes
|
||||
- This phase is pure verify — no new features. Stay disciplined.
|
||||
- Don't add tests "for completeness" beyond gcd-engine. Lesson page integration tests are deferred (port spec §5: `@testing-library/svelte` is dropped).
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "Lesson 4 + 5: GCD Euclidean game + factor a²-b² visualizer"
|
||||
status: in-progress
|
||||
created: 2026-05-03
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# Lesson 4 + 5 — GCD Euclidean + Factor a²-b²
|
||||
|
||||
## Context
|
||||
- Brainstorm survey: `plans/reports/brainstorm-260503-1121-curriculum-logic-survey.md` §3.1
|
||||
- Port spec (conventions): `plans/reports/brainstorm-260430-2207-improved-port-spec.md`
|
||||
- Strategy: C (lesson-driven, infra emerges)
|
||||
- Textbook alignment: **Cánh Diều** (lớp 6 + lớp 7)
|
||||
|
||||
## Lessons
|
||||
1. **Ước chung lớn nhất** (số học, lớp 6) — Euclidean algorithm visualizer at `/so-hoc/uoc-chung-lon-nhat/`
|
||||
2. **Hiệu hai bình phương** (đại số, lớp 7) — `a²-b² = (a+b)(a-b)` interactive at `/dai-so/hieu-hai-binh-phuong/`
|
||||
|
||||
## New infra (only what these lessons force)
|
||||
- **KaTeX** — Tex.svelte component (algebra needs `\frac{a \cdot b}{\gcd(a,b)}` and `a^2-b^2`)
|
||||
- **`numtheory-engine/`** — pure module: `gcd`, `lcm`, `gcdSteps` (mirrors geom-engine shape)
|
||||
- **Goal-state pattern** — per-lesson `goal(state) → { reached, hint? }` (documented, not abstracted)
|
||||
|
||||
## Explicit non-goals (YAGNI gate)
|
||||
- ❌ algebra-engine module (`a*a - b*b` is too small; defer until 2nd algebra lesson)
|
||||
- ❌ shared NumberInput / Slider components (use raw inputs)
|
||||
- ❌ shared GoalState type or quiz primitives (Sub-project §3.2 territory)
|
||||
- ❌ scoring, persistence, hint-cost, undo/redo (Sub-projects §3.2/§3.3)
|
||||
- ❌ EN locale (Sub-project §3.5)
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Status | Owns |
|
||||
|---|-------|--------|------|
|
||||
| 01 | KaTeX integration | completed | `tex.svelte`, deps, smoke test (sandbox removed) |
|
||||
| 02 | Số học engine + GCD lesson | completed | `numtheory-engine/` (12 tests), `/so-hoc/uoc-chung-lon-nhat/` |
|
||||
| 03 | Đại số factor a²-b² lesson | completed | `/dai-so/hieu-hai-binh-phuong/` |
|
||||
| 04 | Registry + landing glue | completed | `registry.js`, `site.vi.js`, topic landings, root tiles |
|
||||
| 05 | Test + a11y verify | partial | check ✓, test 46/46 ✓, build ✓ — **manual a11y/mobile pending user verify** |
|
||||
|
||||
## Outcomes
|
||||
- 5 routes added: `/so-hoc/`, `/so-hoc/uoc-chung-lon-nhat/`, `/dai-so/`, `/dai-so/hieu-hai-binh-phuong/` (and reuses `/hinh-hoc/`)
|
||||
- 12 new unit tests (numtheory-engine), all green; total 46/46 passing
|
||||
- KaTeX integrated SSR-style via `<Tex>` component; ~280KB CSS+fonts bundle delta
|
||||
- All three topic tiles on landing now live
|
||||
- README + RUNBOOK updated
|
||||
|
||||
## Dependencies
|
||||
- Phase 01 blocks Phase 03 (algebra lesson needs `<Tex>`)
|
||||
- Phase 01 blocks Phase 02 (LCM formula uses `<Tex>`)
|
||||
- Phase 04 blocks Phase 05 (need glue done before final verify)
|
||||
- Phases 02 + 03 can run in parallel after 01
|
||||
|
||||
## Success criteria
|
||||
- Both new lessons live at correct URLs, rendered server-side via SvelteKit prerender
|
||||
- KaTeX renders inline + display math with no FOUC
|
||||
- `npm run check` clean (svelte-check + JSDoc strict)
|
||||
- `npm run test` green (existing tests + new numtheory-engine tests)
|
||||
- Keyboard nav + ARIA work for all new interactive elements
|
||||
- Bundle size delta documented (KaTeX is ~300KB gzipped; expected)
|
||||
|
||||
## Open questions (resolve during implementation)
|
||||
- Cánh Diều lớp 6 GCD example numbers — use canonical (48, 18) → 6 default?
|
||||
- Đại số lesson: slider range for `a` and `b` — recommend `1 ≤ b ≤ a ≤ 10` to keep visual clean
|
||||
- KaTeX font subset — load full or VN-friendly subset?
|
||||
@@ -0,0 +1,238 @@
|
||||
# Brainstorm — Curriculum Logic Survey
|
||||
|
||||
- Date: 2026-05-03
|
||||
- Mode: brutal-honesty advisory. No code.
|
||||
- Scope: all logics applicable across MathMax (lớp 6-9, số học + đại số + hình học × 4 dimensions).
|
||||
- Predecessor: `plans/reports/brainstorm-260430-2207-improved-port-spec.md` (port spec, already shipped).
|
||||
- Conventions: tag = `[grade] [now|next|later|YAGNI]`. `now` = something a current lesson needs. `next` = next 1-2 lessons likely need. `later` = real but not soon. `YAGNI` = stop, don't build.
|
||||
|
||||
## TL;DR
|
||||
|
||||
1. Pick **Strategy C** (lesson-driven, infra emerges). Stop catalog-shopping; ship lessons.
|
||||
2. Next concrete sub-project: **one số học lesson + one đại số lesson** (you pick which). Brainstorm each separately.
|
||||
3. Don't build quiz/scoring/persistence/i18n-en until 2+ lessons demand them. Hard rule.
|
||||
4. Three things you almost-certainly need before lesson 4: KaTeX (algebra needs `\frac`/`\sqrt`), an "expression input" interaction primitive, and goal-state detection (sandbox-only stops working when you have homework-shaped lessons).
|
||||
5. Three things to **never** build: spaced-repetition, accounts/auth, custom analytics.
|
||||
|
||||
---
|
||||
|
||||
## 1 · Catalog
|
||||
|
||||
### 1.1 Math content logic (pure modules; sibling of `geom-engine/`)
|
||||
|
||||
**Số học (lớp 6-7)**
|
||||
- `[6][next]` divisibility tests (2/3/5/9/10/11) — pure predicates
|
||||
- `[6][next]` GCD/LCM via Euclidean — pure
|
||||
- `[6][next]` prime check + sieve up to N — pure
|
||||
- `[6][next]` prime factorization — pure
|
||||
- `[6][later]` modular arithmetic (mod, congruence) — pure
|
||||
- `[6][next]` fraction normalize / simplify / common-denominator — pure
|
||||
- `[6][next]` mixed ↔ improper fraction — pure
|
||||
- `[6][later]` decimal ↔ fraction (terminating + repeating) — pure
|
||||
- `[6][later]` percent / ratio / proportion — pure
|
||||
- `[6][next]` integer ops (negatives, abs, sign rules) — pure
|
||||
|
||||
**Đại số (lớp 7-9)**
|
||||
- `[7][next]` polynomial: add/subtract/scale (1 var) — pure
|
||||
- `[7][next]` polynomial multiply (foil + general) — pure
|
||||
- `[8][next]` factoring: common factor, grouping, identities (a²-b², a²±2ab+b², a³±b³) — pure
|
||||
- `[8][later]` factoring: AC method for trinomials — pure
|
||||
- `[7][next]` linear equation 1 var: solve + step trace — pure
|
||||
- `[8][later]` linear system 2 var: substitution + elimination + step trace — pure
|
||||
- `[8][later]` inequality 1 var (with sign-flip on negative mult) — pure
|
||||
- `[9][later]` quadratic: discriminant, roots, Viète — pure
|
||||
- `[9][later]` rational expression: simplify, restrict domain — pure
|
||||
- `[9][later]` square root: simplify √n, conjugate denom — pure
|
||||
- `[7][later]` linear function: y = ax + b, plot, slope/intercept — pure + render
|
||||
- `[9][later]` quadratic function: y = ax² + bx + c, vertex, axis — pure + render
|
||||
|
||||
**Hình học (lớp 6-9, beyond current 3 lessons)**
|
||||
- `[6][next]` polygon area/perimeter (rect, tri, parallelogram, trapezoid) — pure
|
||||
- `[7][later]` Pythagoras + converse — pure (already in vec via `len`/`dist`)
|
||||
- `[7][later]` triangle inequality predicate — pure
|
||||
- `[8][later]` transformations: translate, rotate, reflect, dilate (2D matrix or pointwise) — pure
|
||||
- `[8][later]` similarity: SAS/SSS/AA — pure (mathmax has SSS-position-strict; permutation-invariant variants are real lessons later)
|
||||
- `[9][later]` circle: chord/secant/tangent, arc length, inscribed quadrilateral — pure (extend `circle.js`)
|
||||
- `[9][later]` trig ratios (sin/cos/tan in right triangle) — pure
|
||||
- `[9][YAGNI]` 3D solids (cube/cylinder/cone/sphere V+SA) — pure but render layer is heavy; defer
|
||||
- `[8][later]` coordinate geometry: distance, midpoint, slope between points — pure
|
||||
- `[7][YAGNI]` compass+straightedge construction primitives — interaction layer is its own project; defer
|
||||
|
||||
**Thống kê & Xác suất (lớp 7-9)**
|
||||
- `[7][later]` mean/median/mode/range — pure
|
||||
- `[7][later]` frequency table + bin — pure
|
||||
- `[8][later]` variance / std dev — pure
|
||||
- `[9][later]` basic probability (favorable/total, independent events) — pure
|
||||
- `[9][YAGNI]` combinatorics (nCr, nPr) for advanced lessons — defer
|
||||
|
||||
### 1.2 Lesson interaction logic
|
||||
|
||||
- `[now]` drag w/ pointer + keyboard + ARIA — exists (`use:draggable`)
|
||||
- `[now]` projector (snap to circle, line) — exists (used by inscribed angle)
|
||||
- `[next]` slider (one-knob parameter, e.g. similarity ratio k) — trivial; native `<input type="range">` w/ Tailwind
|
||||
- `[next]` snap-to-grid (integer or fraction lattice) — extend draggable action w/ `snap` opt
|
||||
- `[next]` snap-to-angle (15°/30°/45° increments) — variant of projector
|
||||
- `[next]` toggle/checkbox to show/hide auxiliary lines — Svelte `$state` only
|
||||
- `[next]` multi-choice tap (1-correct, multi-correct) — interaction primitive
|
||||
- `[next]` drag-bin categorize (sort items into buckets) — interaction primitive
|
||||
- `[next]` drag-reorder (sequence proof steps, sort numbers) — interaction primitive
|
||||
- `[next]` number input + validation — bare `<input type="number">` w/ derived feedback
|
||||
- `[later]` expression input (e.g. `2x+3`) + parser — needs grammar; large; only when first algebra lesson lands
|
||||
- `[later]` fraction input (numerator/denominator pair) — small variant of number input
|
||||
- `[later]` step-state machine (assertion → justification → next step) — for proof lessons
|
||||
- `[later]` undo/redo stack — generic over `$state` snapshots
|
||||
- `[later]` restart/reset to initial config — trivial; per-lesson save initial `$state`
|
||||
- `[later]` progressive hint reveal — `$state` index + `<details>` or button-driven
|
||||
- `[later]` animation playback (apply transformation step-by-step) — WAAPI or Svelte transitions
|
||||
- `[later]` plot point on coordinate plane (click to place) — needs canvas grid render
|
||||
- `[later]` draw line/ray (drag two points, render) — extension of draggable
|
||||
- `[YAGNI]` freehand sketch — overkill; pedagogy doesn't need it
|
||||
|
||||
### 1.3 Pedagogy / scoring logic
|
||||
|
||||
- `[now]` sandbox mode — current lessons are pure exploration; no goal
|
||||
- `[next]` goal-state detection (predicate: "is this a right triangle?", "did you simplify fully?") — emerges from lesson; pure
|
||||
- `[next]` correct/wrong feedback (visual + ARIA-live announcement) — primitive
|
||||
- `[next]` partial credit / "almost" feedback — judgment call per lesson
|
||||
- `[later]` exercise variants (seeded random problem generator per lesson) — pure
|
||||
- `[later]` mastery flag (3-correct-in-a-row) — localStorage key per lesson
|
||||
- `[later]` hint reveal w/ optional cost (star deduction) — needs scoring vocabulary
|
||||
- `[later]` streak counter — localStorage per user
|
||||
- `[later]` worked-example fading (fully-shown → partial → solo) — pedagogical pattern; high effort
|
||||
- `[later]` self-check button vs auto-grade — per-lesson UX choice
|
||||
- `[later]` quiz primitive: MC, free-response, drag-match, ordering — composes interaction primitives
|
||||
- `[YAGNI]` spaced repetition (SM-2 etc) — wrong tool for middle-school topic mastery
|
||||
- `[YAGNI]` adaptive difficulty (next-problem easier/harder by perf) — overkill; static pacing is fine
|
||||
- `[YAGNI]` leaderboards / social — out of scope for self-paced learning
|
||||
|
||||
### 1.4 App-level logic
|
||||
|
||||
- `[now]` lesson registry + per-lesson copy module — exists
|
||||
- `[next]` registry: add `prerequisites: []`, `tags: []`, `estimatedMinutes` fields — schema bump
|
||||
- `[next]` next/prev lesson nav (registry-driven, ordered by topic+grade) — only after lesson 4
|
||||
- `[next]` per-lesson `<svelte:head>` SEO (title, desc, canonical, OG) — pulled from copy module
|
||||
- `[next]` topic landing page enhancements (group by grade, show progress) — extends `/hinh-hoc/`
|
||||
- `[later]` grade-filter UI on topic landing — tabs or chips
|
||||
- `[later]` search/filter by tag, slug — client-side over registry (no server)
|
||||
- `[later]` localStorage persistence: completed lessons, last-state per lesson, prefs — pure JS module
|
||||
- `[later]` deep-link state: encode lesson config in URL hash (e.g. `#a=60,80&b=180,80`) — for sharing problems
|
||||
- `[later]` sitemap.xml generated at build — SvelteKit `+server.js` or static
|
||||
- `[later]` robots.txt — trivial static
|
||||
- `[later]` OG images per lesson — needs render pipeline (satori/playwright); defer to lesson 6+
|
||||
- `[later]` i18n: add EN parallel to copy.vi.js — only when EN audience signal exists
|
||||
- `[later]` dark mode — Tailwind `dark:` + prefers-color-scheme; cheap but needs design pass
|
||||
- `[later]` reduced motion — already gated in port spec; honor when animations land
|
||||
- `[later]` print stylesheet (worksheet export) — `@media print` rules
|
||||
- `[later]` PDF export of lesson — needs render pipeline; defer
|
||||
- `[later]` privacy-respecting analytics (Plausible / Umami) — one snippet; cheap; pick later
|
||||
- `[YAGNI]` user accounts / auth — static-site killer; don't add
|
||||
- `[YAGNI]` server-side anything (Postgres, API) — kills GitHub Pages deploy
|
||||
- `[YAGNI]` PWA / service worker / offline — premature optimization
|
||||
- `[YAGNI]` Sentry / error reporting — overkill for static educational site
|
||||
- `[YAGNI]` content CMS (Sanity/Contentful) — copy modules are the CMS
|
||||
|
||||
---
|
||||
|
||||
## 2 · Strategy comparison
|
||||
|
||||
| Strategy | Pros | Cons | Verdict |
|
||||
|---|---|---|---|
|
||||
| **A. Topic-first** (finish hình học → số học → đại số) | Deep math-engine reuse per topic; topic-coherent UX | App-level features lag; users see uneven progress across topics | Reject — uneven curriculum hurts perceived completeness |
|
||||
| **B. Infra-first** (build scoring + persistence + i18n + registry v2 before lessons) | Each new lesson cheap once infra exists | YAGNI violation; infra built without consumers always rots; 3 lessons doesn't justify it | Reject — recipe for over-engineering |
|
||||
| **C. Lesson-driven, infra emerges** *(recommended)* | Pure YAGNI; every line has a consumer; matches how the geom-engine actually evolved | Some rework when 3rd consumer reveals shared shape | **Adopt** — proven by the port itself |
|
||||
|
||||
**Why C wins**: the existing geom-engine (vec / triangle / circle / ticks) emerged exactly this way — it's pure and small because every function has 1+ caller. Doing infra-first now would invert that virtue.
|
||||
|
||||
---
|
||||
|
||||
## 3 · Decomposition into sub-projects
|
||||
|
||||
Each sub-project = its own brainstorm → plan → implement cycle. Order is **suggested**, not enforced; pick what unblocks the most lessons.
|
||||
|
||||
### 3.1 Sub-project: "Lesson 4 + 5" (number theory + algebra debut)
|
||||
- Pick 1 số học lesson (recommend: **GCD/LCM via Euclidean game** — drag two numbers; watch ladder reduce; goal: hit (gcd, 0))
|
||||
- Pick 1 đại số lesson (recommend: **factor a² - b² visualizer** — drag two square sides; show area = (a+b)(a-b))
|
||||
- Triggers: KaTeX (almost certainly), goal-state detection primitive, possibly slider primitive
|
||||
- Don't build: scoring system, persistence, hint UI, undo
|
||||
- Risk: LOW (both lessons are bounded, single-screen, single-goal)
|
||||
|
||||
### 3.2 Sub-project: "Quiz primitives" (only after 2+ lessons need it)
|
||||
- MC component, free-response w/ check, drag-match
|
||||
- Reuses the same `goal-state` predicate shape from §3.1
|
||||
- Schema for quiz items in copy modules (questions, choices, correct, explain)
|
||||
- Triggers: per-lesson "Bài tập" tab vs sandbox-only
|
||||
- Don't build: scoring back-end, attempt history (defer to §3.3)
|
||||
- Risk: LOW-MEDIUM (component design needs 2 real lessons to validate)
|
||||
|
||||
### 3.3 Sub-project: "Local progress" (localStorage)
|
||||
- Schema: `mathmax/v1/progress` → `{ [lessonSlug]: { completed: bool, lastVisited: ISOString, attempts: number } }`
|
||||
- Pure module + Svelte store wrapper
|
||||
- Migration story: version key in storage; if missing, treat as fresh
|
||||
- Triggers: first lesson w/ "completion" state; landing page shows ✓ on completed lessons
|
||||
- Don't build: cross-device sync, accounts, server persistence
|
||||
- Risk: LOW (pure browser API)
|
||||
|
||||
### 3.4 Sub-project: "Registry v2 + nav" (after 6+ lessons)
|
||||
- Add `prerequisites`, `tags`, `estimatedMinutes`, `order` to registry entries
|
||||
- Generate prev/next links at build time
|
||||
- Topic landing page groups lessons by grade with progress badges
|
||||
- Triggers: lesson list becomes too long to scan; users ask "what's next"
|
||||
- Don't build: complex search; tags-cloud UI
|
||||
- Risk: LOW (just data shape; no new behavior)
|
||||
|
||||
### 3.5 Sub-project: "EN locale" (only with audience signal)
|
||||
- Parallel `copy.en.js` per lesson + `site.en.js`
|
||||
- Locale switcher in header (defaults to `vi`; remembers in localStorage)
|
||||
- URL strategy: `?lang=en` query param OR `/en/...` prefix — decide later
|
||||
- Triggers: external request, partner school, analytics showing non-VN traffic
|
||||
- Don't build: third locale, automatic translation, RTL
|
||||
- Risk: LOW once triggered; HIGH effort otherwise (translation cost is the actual blocker)
|
||||
|
||||
### 3.6 Sub-project: "Polish" (after 6+ lessons)
|
||||
- Sitemap.xml + robots.txt
|
||||
- OG images (per-lesson; consider Satori @ build)
|
||||
- Dark mode (design pass)
|
||||
- Print stylesheet for paper worksheets
|
||||
- Analytics (Plausible — privacy-respecting)
|
||||
- Triggers: lesson count past 6; SEO becomes worth caring about
|
||||
- Risk: LOW (mostly mechanical)
|
||||
|
||||
---
|
||||
|
||||
## 4 · Cross-cutting recommendations
|
||||
|
||||
### Math expression rendering (KaTeX trigger)
|
||||
First algebra lesson needs `\frac{a}{b}`, `\sqrt{x}`, `x^2`. Add KaTeX **then**, not before. Pin one place to render: a tiny `<Tex math="..." />` Svelte component wrapping `katex.render`.
|
||||
|
||||
### Goal-state detection pattern
|
||||
Don't build a "scoring engine." Each lesson exports a `goal(state) → { reached: bool, hint?: string }` pure function. Page renders feedback. Three lessons later, harvest the shape into a shared interface. Don't pre-design.
|
||||
|
||||
### Pure-module discipline
|
||||
Every math content logic above goes under `src/lib/<topic>-engine/` (e.g. `numtheory-engine/`, `algebra-engine/`). Same shape as `geom-engine/`: pure JS, JSDoc, Vitest, no DOM. Lessons import. Period.
|
||||
|
||||
### "Sắp ra mắt" guard
|
||||
Don't enable Số học / Đại số landing tiles until **at least one lesson per topic ships**. Empty topics are worse than missing ones.
|
||||
|
||||
### Vietnamese-first slugs
|
||||
Every new lesson: VN slug (`uoc-chung-lon-nhat`, not `gcd-game`). Confirmed in port spec.
|
||||
|
||||
### A11y by default
|
||||
Every new interactive primitive: keyboard equivalent + ARIA from day one. The draggable action set the bar; don't regress.
|
||||
|
||||
---
|
||||
|
||||
## 5 · Open questions
|
||||
|
||||
1. **Which two lessons next?** §3.1 recommends GCD-Euclidean and factor-a²-b² but you may have stronger pedagogy preferences (textbook alignment, school partnerships, etc.).
|
||||
2. **KaTeX bundle size** — KaTeX is ~300KB gzipped w/ fonts. Acceptable for a math site? (Recommend yes; alternative `MathJax` is bigger.)
|
||||
3. **Analytics provider** — Plausible (paid hosted), Umami (self-host), or none? Defer until §3.6.
|
||||
4. **Lesson "completion" definition** — does sandbox-only count as complete on first visit, or do we require a goal? Affects §3.3 schema.
|
||||
5. **EN locale priority** — do you have any concrete audience signal (collaborators, partner schools, GitHub stars from EN-speakers), or is it purely speculative? If speculative, drop to "never" until signal arrives.
|
||||
6. **Worksheet/print export demand** — is this asked-for by teachers, or my speculation? If speculation, drop.
|
||||
7. **Curriculum source-of-truth** — are lessons aligned to a specific Vietnamese textbook (Cánh Diều, Kết nối tri thức, Chân trời sáng tạo)? If yes, that constrains lesson selection and ordering.
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE
|
||||
**Summary:** Survey of all 4-dimension logics (math content × interaction × pedagogy × app), with grade + risk tags. Recommended Strategy C (lesson-driven). Six concrete sub-projects scoped, ordered, and gated on real triggers. Hard YAGNI rules: no SR, no auth, no PWA, no analytics until lesson 6+.
|
||||
**Concerns:** Q7 (textbook alignment) is the biggest unknown — may rewrite §3.1 lesson picks. Q1 needs your call before any next planning step.
|
||||
Reference in New Issue
Block a user