# 11 — The Learning Section (`/learn/`)

An interactive deep-dive into Vega-Lite, served at `/learn/`: a marketing surface separate
from the app, where each lesson walks a spec from an 80%-naive version to a polished one to
teach the grammar's dormant power and funnel readers into the app. It is pitched past the
basics — fundamentals are left to the official Vega-Lite docs (linked from the index), so
the section leads with advanced cases (interaction, composition, transforms) rather than
fundamentals.

## A marketing surface, like the landing

`/learn/` is a third Vite entry (`learn/index.html` → `src/learn/`) alongside the landing
(`/`) and the app (`/app/`), under the same marketing-surface rules:

- Reuses **`src/core` and the landing's `LandingChart`** only — never stores, modals,
  orchestration, or app components. Vega is lazy-loaded through `LandingChart`, so the entry
  stays light.
- **Out of PWA scope.** The service worker is scoped to `/app/`, so the learning pages stay
  uncontrolled, always-fresh, and indexable — the point for organic-reach content.
- **An index plus a page per lesson.** `/learn/` is the index (a card per lesson); each
  lesson is its own URL `/learn/<slug>/` — a separate indexable document with its
  frontmatter-derived `<title>`/`<meta>`. The per-lesson HTML shells are generated from the
  lesson frontmatter by `scripts/learn-pages.ts` (run from `vite.config` on every dev/build,
  so "drop a file" still holds; the shells are git-ignored). The single `src/learn` entry
  renders the index or one lesson from `location.pathname`.

## Lessons are markdown; the renderer is general

A lesson is a `.md` file in `src/learn/lessons/`, discovered with `import.meta.glob` — so
**adding a lesson is dropping a file**, with no registry to edit. A lesson is a _free-form
document of ordered blocks_, not a fixed template:

| Authored as                                          | Block         | Rendered by                   |
| ---------------------------------------------------- | ------------- | ----------------------------- |
| plain markdown                                       | `prose`       | `Markdown`                    |
| `:::progression` wrapping `##` stages + fenced specs | `progression` | `SpecProgression`             |
| a bare fenced `vega-lite` block                      | `chart`       | `LandingChart`                |
| `:::name … :::`                                      | `callout`     | `Markdown` (styled by name)   |
| `:::data` wrapping a `{ name: rows }` JSON object    | — metadata    | injected into specs at render |

Inside a `:::progression`, each `##` heading is a stage: heading → tab label, prose → note,
the following fenced `vega-lite` block → spec. A `:::data` block names datasets once for the
whole lesson; specs reference them with `{ "data": { "name": … } }` and `injectDatasets`
merges the rows in at render time — so a shared dataset isn't repeated per stage, and the
source pane keeps a stage's grammar legible instead of burying it under data.

## The pipeline

`lessons/*.md` → `import.meta.glob` + `parseLesson` (`src/learn/lessons.ts`, using
`core/lesson-parse`) → `LESSONS`. The `src/learn` entry reads the path: `/learn/` →
`LearnIndex`, `/learn/<slug>/` → `LessonView`, both inside `LearnLayout` (shared
header/footer/theme). `LessonView` dispatches each block → `Markdown` | `SpecProgression` |
`LandingChart`. `SpecProgression` renders each stage's spec with `formatSpec`
(`core/json-format`) and highlights what the stage changed with `changedLines`
(`core/spec-diff`, an LCS line-diff). `parseLesson` also returns `datasets` (the `:::data`
blocks); `LessonView`/`SpecProgression` call `injectDatasets` so only the _rendered_ spec
carries the rows — the displayed-and-diffed spec keeps its by-name reference.

## Rules

- **The engine consumes plain data, so the authoring surface is swappable.**
  `SpecProgression`, `spec-diff`, and `formatSpec` take parsed blocks and specs and know
  nothing about markdown — the parser is the only thing coupled to the `.md` format. The
  authoring format can change without touching the widget.
- **Parsing, diff, and formatting are pure and live in `core`** (tested hardest); rendering
  and the markdown library live in `src/learn`. **Core stays dependency-free** — `marked` is
  imported only by `src/learn/Markdown`.
- **`dangerouslySetInnerHTML` renders first-party lesson files** (repo content authored by
  us), never user input — not an XSS surface.
- Lesson specs are fenced JSON parsed with `JSON.parse`; the source pane re-formats them with
  `formatSpec` so the shown JSON matches the editor's house style.
- **Lesson charts render through `LandingChart` with `fitMode: 'width'`** — which sets
  `width: "container"` and drops fixed heights on every view, and container width only works
  for a single or layered view, not side-by-side. A multi-view lesson is therefore a
  `vconcat` (a stacked column), not an `hconcat` dashboard, which would fight the sizing.
