learningfoundry 0.40.0__tar.gz → 0.46.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/CHANGELOG.md +71 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/PKG-INFO +38 -2
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/README.md +37 -1
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/pyproject.toml +1 -1
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/__init__.py +1 -1
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/config.py +15 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/resolver.py +13 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/schema_v1.py +11 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/e2e/navigation.spec.ts +30 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/e2e/progress.spec.ts +24 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/e2e/video.spec.ts +27 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/package.json +2 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/playwright.config.ts +27 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/components/ContentBlock.svelte +12 -3
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +66 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +82 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.test.ts +117 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +103 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/components/Navigation.svelte +14 -11
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.svelte +47 -22
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.test.ts +118 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0/src/learningfoundry}/sveltekit_template/src/lib/components/QuizBlock.svelte +8 -1
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +51 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.test.ts +78 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +136 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.test.ts +96 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/lesson-view.helpers.ts +63 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/module-list.helpers.ts +105 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/module-list.test.ts +65 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/navigation.helpers.ts +33 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/navigation.test.ts +43 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/progress-dashboard.helpers.ts +39 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.ts +11 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/stores/progress.test.ts +98 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/stores/progress.ts +28 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/types/index.ts +12 -1
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/utils/locking.test.ts +238 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/utils/locking.ts +147 -0
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/utils/viewport-completion.ts +64 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.svelte +12 -18
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/routes/+page.svelte +2 -22
- {learningfoundry-0.40.0 → learningfoundry-0.46.0/src/learningfoundry}/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +6 -4
- learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/routes/layout.test.ts +93 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/ContentBlock.svelte +12 -3
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/LessonList.svelte +66 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/LessonView.svelte +82 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/ModuleList.svelte +103 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/Navigation.svelte +14 -11
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/ProgressDashboard.svelte +47 -22
- {learningfoundry-0.40.0/src/learningfoundry → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/QuizBlock.svelte +8 -1
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/TextBlock.svelte +51 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/VideoBlock.svelte +136 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/lesson-view.helpers.ts +63 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/module-list.helpers.ts +105 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/navigation.helpers.ts +33 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/components/progress-dashboard.helpers.ts +39 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/stores/curriculum.ts +10 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/stores/progress.ts +28 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/types/index.ts +12 -1
- learningfoundry-0.46.0/sveltekit_template/src/lib/utils/locking.ts +147 -0
- learningfoundry-0.46.0/sveltekit_template/src/lib/utils/viewport-completion.ts +64 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/routes/+layout.svelte +12 -19
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/routes/+page.svelte +2 -22
- {learningfoundry-0.40.0/src/learningfoundry → learningfoundry-0.46.0}/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +6 -18
- learningfoundry-0.40.0/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +0 -45
- learningfoundry-0.40.0/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +0 -44
- learningfoundry-0.40.0/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +0 -67
- learningfoundry-0.40.0/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +0 -16
- learningfoundry-0.40.0/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -45
- learningfoundry-0.40.0/sveltekit_template/src/lib/components/LessonList.svelte +0 -45
- learningfoundry-0.40.0/sveltekit_template/src/lib/components/LessonView.svelte +0 -44
- learningfoundry-0.40.0/sveltekit_template/src/lib/components/ModuleList.svelte +0 -67
- learningfoundry-0.40.0/sveltekit_template/src/lib/components/TextBlock.svelte +0 -16
- learningfoundry-0.40.0/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -45
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/.gitignore +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/LICENSE +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/docs/project-guide/README.md +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/__main__.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/asset_resolver.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/cli.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/exceptions.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/generator.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/integrations/__init__.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/integrations/d3foundry_stub.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/integrations/nbfoundry_stub.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/integrations/protocols.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/integrations/quizazz.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/logging_config.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/parser.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/pipeline.py +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/py.typed +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/pnpm-lock.yaml +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/app.css +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/app.html +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/db/database.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/db/index.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.test.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.test.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.test.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/static/.gitkeep +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/svelte.config.js +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/tsconfig.json +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/src/learningfoundry/sveltekit_template/vite.config.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/app.css +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/app.html +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/db/database.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/db/index.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/db/progress.ts +0 -0
- {learningfoundry-0.40.0 → learningfoundry-0.46.0}/sveltekit_template/src/lib/utils/markdown.ts +0 -0
|
@@ -7,6 +7,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.46.0] - 2026-05-01
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Lesson navigation routing.** Sidebar lesson clicks, the Next/Finish button, and dashboard "Start module / Continue" buttons now call `goto()` from `$app/navigation` directly instead of going through the curriculum store helper. The previous flow updated `currentPosition` (and therefore the sidebar highlight) but left the URL untouched, so the lesson route was never re-mounted: `markLessonInProgress` ran only on the first lesson reached by direct URL, the sidebar checkmarks never updated, and the curriculum/module progress bars stayed at zero across the session. Components now route via `Navigation.svelte`, `LessonList.svelte`, `ProgressDashboard.svelte` → `goto('/${moduleId}/${lessonId}')`.
|
|
15
|
+
- **Sticky LessonView state across navigations.** The dynamic lesson route now wraps `<LessonView>` in `{#key \`${moduleId}/${lessonId}\`}` so the subtree tears down and re-mounts whenever either route param changes — guaranteeing fresh `allBlocksComplete` / `completedBlocks` state and a re-run of the on-mount progress check (so revisiting a previously-completed lesson activates Next/Finish immediately).
|
|
16
|
+
- **Stale video iframe across consecutive video lessons.** `LessonView`'s `{#each lesson.content_blocks}` now uses a stable identity key derived from `block.ref` or `block.content.url` (falling back to `${type}-${index}`); previously, two consecutive lessons each with a `video` block reused the same `<VideoBlock>` instance and its iframe player, leaving the previous lesson's video on screen. `VideoBlock.svelte` additionally tracks `content.url` via `$effect` and tears down / recreates its YouTube player whenever the URL changes, as belt-and-suspenders coverage.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Playwright e2e harness.** New `e2e/` directory with three regression specs (`navigation.spec.ts`, `progress.spec.ts`, `video.spec.ts`) covering the FR-P9/FR-P10 lifecycle invariants that vitest cannot exercise (because vitest mocks `$app/navigation`). New `pnpm e2e` script and `playwright.config.ts` driving `pnpm preview` against the built static site. The smoke test runs `pnpm e2e` after `pnpm build` and skips gracefully if Playwright browsers aren't installed locally; CI installs them via `pnpm exec playwright install chromium`.
|
|
21
|
+
- **Vitest navigation regression coverage.** New `navigation.helpers.ts` (`resolveGoNext`, `resolveGoPrev`, `lessonHref`) and `navigation.test.ts` lock down the routing decisions made by Next/Finish, Previous, and lesson rows. New `contentBlockKey` helper (and tests) verifies the FR-P10 stable-identity convention used by `{#each}`.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `navigateTo` in `$lib/stores/curriculum.ts` is now documented as **internal route-sync only — UI code must use `goto` directly**. The function continues to set `currentPosition` for use by the dynamic lesson route's URL→store `$effect`; UI code (sidebar, dashboard, Next/Finish) routes via `goto()` so SvelteKit's full navigation lifecycle (page params, scroll restoration, `{#key}` re-mount) fires predictably.
|
|
26
|
+
|
|
27
|
+
## [0.45.0] - 2026-04-30
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Locking and unlocking UI.** Frontend implementation of the locking model introduced in v0.44.0:
|
|
32
|
+
- New `$lib/utils/locking.ts` with pure functions `isModuleLocked`, `isLessonLocked`, `getOptionalLessons`, `isModuleComplete`, plus convenience set helpers `lockedModuleIds` / `lockedLessonIds`.
|
|
33
|
+
- `ModuleList.svelte` renders a Lucide `Lock` icon, suppresses expansion, and skips the active-module highlight for locked modules.
|
|
34
|
+
- `LessonList.svelte` shows a `◇` indicator for optional lessons (sibling lessons within a module whose `unlock_module_on_complete` lesson has been completed) and renders locked lessons as muted, non-clickable rows.
|
|
35
|
+
- `ProgressDashboard.svelte` uses `isModuleComplete` for per-module status (treats optional lessons as not blocking module completion) while still counting all completed lessons toward the curriculum-level progress bar.
|
|
36
|
+
- Type extensions: `LessonStatus` gains `'optional'`; `Lesson.unlock_module_on_complete?`, `Module.locked?`, `Curriculum.locking?`, and a new `LockingConfig` interface.
|
|
37
|
+
- The unlock cascade is fully reactive: completing an `unlock_module_on_complete` lesson refreshes `progressStore` once via `invalidateProgress`, and all locked/optional state re-derives automatically — no extra DB writes or store updates required.
|
|
38
|
+
|
|
39
|
+
## [0.44.0] - 2026-04-30
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Locking configuration schema.** Python-side schema, resolver, and config support for sequential content access control:
|
|
44
|
+
- `LockingConfig` Pydantic model (`sequential`, `lesson_sequential`) on `CurriculumDef`.
|
|
45
|
+
- `Module.locked: bool | None` per-module override.
|
|
46
|
+
- `Lesson.unlock_module_on_complete: bool` gateway-lesson flag.
|
|
47
|
+
- `QuizBlock.pass_threshold: float` (0.0–1.0) for quiz completion scoring.
|
|
48
|
+
- Global config (`~/.config/learningfoundry/config.yml`) gains `locking` block with the same fields.
|
|
49
|
+
- Config hierarchy: global defaults → curriculum YAML `locking` → per-module `locked` override.
|
|
50
|
+
- All fields propagate through the resolver into `curriculum.json` for frontend consumption (Story I.j).
|
|
51
|
+
|
|
52
|
+
## [0.43.0] - 2026-04-30
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **Curriculum-level progress bar on the dashboard.** `ProgressDashboard.svelte` now renders a summary bar above the module cards showing `"{totalComplete} of {totalLessons} lessons completed"`. Computed reactively from `progressStore` — updates live when lessons complete during the session. Hidden when the curriculum has zero lessons. The dashboard `+page.svelte` no longer does its own one-shot progress fetch; it reads from the shared `progressStore` populated by the layout.
|
|
57
|
+
|
|
58
|
+
## [0.42.0] - 2026-04-30
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **Block completion events drive lesson auto-complete.** Each content block fires an independent completion event when sufficiently engaged with: `TextBlock` after 1 s in the viewport (IntersectionObserver + debounce timer), `VideoBlock` on YouTube IFrame Player API `ENDED` state (falls back to 3 s viewport if API fails to load within 5 s), `QuizBlock` when score ≥ `passThreshold` (default 0.0). `LessonView` tracks which blocks have completed; when all have fired, the lesson is marked complete in SQLite and the sidebar updates immediately.
|
|
63
|
+
- **Reactive progress store** (`$lib/stores/progress.ts`). `progressStore` is a writable Svelte store holding `Record<string, ModuleProgress>`. `invalidateProgress(curriculum)` re-fetches all module progress from SQLite and writes to the store. `+layout.svelte` subscribes to this store instead of a one-shot `$effect` fetch; lesson completions call `invalidateProgress` so the sidebar reflects changes without a page reload.
|
|
64
|
+
- **Revisit behaviour.** On mount, `LessonView` reads the lesson's current DB status; if already `complete`, all blocks are pre-filled as done so the Next/Finish button is immediately active.
|
|
65
|
+
- **Zero-block edge case.** A lesson with no content blocks is treated as immediately complete on mount.
|
|
66
|
+
- **`QuizManifest.passThreshold`** added to TypeScript types for future quiz scoring threshold support.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **Next/Finish no longer trigger completion marking.** Navigation is decoupled from completion: `Navigation.svelte` handles its own routing (Next → `navigateTo`, Finish → `goto('/')`) and accepts a `disabled` prop. The `onComplete` callback prop has been removed. `LessonView` no longer has `handleNavComplete` or `oncomplete`.
|
|
71
|
+
- **`+page.svelte`** migrated from deprecated `$app/stores` to `$app/state` for route params.
|
|
72
|
+
|
|
73
|
+
## [0.41.0] - 2026-04-30
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **Finish button on last lesson now navigates to the dashboard.** `+page.svelte` was not passing an `oncomplete` handler to `<LessonView>`; clicking Finish was a no-op. It now calls `goto('/')` to redirect to the progress dashboard.
|
|
78
|
+
- **Sidebar module expand/contract no longer reverts immediately.** The `$effect` in `ModuleList.svelte` read `expandedModuleId` (the value it writes), creating a self-dependency that overwrote manual toggles on every re-run. A separate `lastAutoExpandedModuleId` state variable breaks the cycle — the effect only fires when `currentPosition.moduleId` changes to a genuinely new value.
|
|
79
|
+
- **Active module in sidebar is now visually highlighted.** The module card containing the current lesson receives a left-border accent (`border-l-2 border-l-blue-500`) and a light background tint (`bg-blue-50`).
|
|
80
|
+
|
|
10
81
|
## [0.40.0] - 2026-04-29
|
|
11
82
|
|
|
12
83
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learningfoundry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.46.0
|
|
4
4
|
Summary: A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pointmatic/learningfoundry
|
|
6
6
|
Project-URL: Repository, https://github.com/pointmatic/learningfoundry
|
|
@@ -48,6 +48,7 @@ A curriculum engine that turns a YAML curriculum definition into a deployable Sv
|
|
|
48
48
|
- [Video blocks](#video-blocks)
|
|
49
49
|
- [Lesson titles and markdown headings](#lesson-titles-and-markdown-headings)
|
|
50
50
|
- [Images and assets](#images-and-assets)
|
|
51
|
+
- [Content locking](#content-locking)
|
|
51
52
|
- [Configuration File](#configuration-file)
|
|
52
53
|
- [Development Setup](#development-setup)
|
|
53
54
|
|
|
@@ -389,9 +390,40 @@ For production deployment to a CDN, just run `cd dist && pnpm build` — the `st
|
|
|
389
390
|
|
|
390
391
|
---
|
|
391
392
|
|
|
393
|
+
## Content locking
|
|
394
|
+
|
|
395
|
+
Control access to modules and lessons with a three-level configuration hierarchy (most local wins):
|
|
396
|
+
|
|
397
|
+
1. **Per-module `locked`** — explicit `true`/`false` override; trumps everything.
|
|
398
|
+
2. **Curriculum `locking.sequential`** — when true, module N+1 requires module N complete.
|
|
399
|
+
3. **Global config `locking.sequential`** — project-wide default (see Configuration File below).
|
|
400
|
+
|
|
401
|
+
```yaml
|
|
402
|
+
curriculum:
|
|
403
|
+
locking:
|
|
404
|
+
sequential: true # modules must be completed in order
|
|
405
|
+
lesson_sequential: false # lessons within a module are free-order
|
|
406
|
+
|
|
407
|
+
modules:
|
|
408
|
+
- id: mod-01
|
|
409
|
+
locked: false # always accessible regardless of sequential
|
|
410
|
+
lessons:
|
|
411
|
+
- id: lesson-01
|
|
412
|
+
unlock_module_on_complete: true # completing this unlocks siblings + next module
|
|
413
|
+
content_blocks:
|
|
414
|
+
- type: quiz
|
|
415
|
+
source: quizazz
|
|
416
|
+
ref: assessments/quiz.yml
|
|
417
|
+
pass_threshold: 0.7 # 70% required to count as passed
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`unlock_module_on_complete` is useful for "gateway" lessons — a single assessment that, once passed, opens the rest of the module and the next one.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
392
424
|
## Configuration File
|
|
393
425
|
|
|
394
|
-
An optional config file can set defaults for logging. The CLI always takes precedence.
|
|
426
|
+
An optional config file can set defaults for logging and locking. The CLI always takes precedence.
|
|
395
427
|
|
|
396
428
|
**Default location:** `~/.config/learningfoundry/config.yml`
|
|
397
429
|
|
|
@@ -399,6 +431,10 @@ An optional config file can set defaults for logging. The CLI always takes prece
|
|
|
399
431
|
logging:
|
|
400
432
|
level: INFO # DEBUG | INFO | WARNING | ERROR
|
|
401
433
|
output: stdout # stdout | stderr
|
|
434
|
+
|
|
435
|
+
locking:
|
|
436
|
+
sequential: false # default for all curricula on this machine
|
|
437
|
+
lesson_sequential: false
|
|
402
438
|
```
|
|
403
439
|
|
|
404
440
|
Pass a custom config location with `-c / --config`.
|
|
@@ -19,6 +19,7 @@ A curriculum engine that turns a YAML curriculum definition into a deployable Sv
|
|
|
19
19
|
- [Video blocks](#video-blocks)
|
|
20
20
|
- [Lesson titles and markdown headings](#lesson-titles-and-markdown-headings)
|
|
21
21
|
- [Images and assets](#images-and-assets)
|
|
22
|
+
- [Content locking](#content-locking)
|
|
22
23
|
- [Configuration File](#configuration-file)
|
|
23
24
|
- [Development Setup](#development-setup)
|
|
24
25
|
|
|
@@ -360,9 +361,40 @@ For production deployment to a CDN, just run `cd dist && pnpm build` — the `st
|
|
|
360
361
|
|
|
361
362
|
---
|
|
362
363
|
|
|
364
|
+
## Content locking
|
|
365
|
+
|
|
366
|
+
Control access to modules and lessons with a three-level configuration hierarchy (most local wins):
|
|
367
|
+
|
|
368
|
+
1. **Per-module `locked`** — explicit `true`/`false` override; trumps everything.
|
|
369
|
+
2. **Curriculum `locking.sequential`** — when true, module N+1 requires module N complete.
|
|
370
|
+
3. **Global config `locking.sequential`** — project-wide default (see Configuration File below).
|
|
371
|
+
|
|
372
|
+
```yaml
|
|
373
|
+
curriculum:
|
|
374
|
+
locking:
|
|
375
|
+
sequential: true # modules must be completed in order
|
|
376
|
+
lesson_sequential: false # lessons within a module are free-order
|
|
377
|
+
|
|
378
|
+
modules:
|
|
379
|
+
- id: mod-01
|
|
380
|
+
locked: false # always accessible regardless of sequential
|
|
381
|
+
lessons:
|
|
382
|
+
- id: lesson-01
|
|
383
|
+
unlock_module_on_complete: true # completing this unlocks siblings + next module
|
|
384
|
+
content_blocks:
|
|
385
|
+
- type: quiz
|
|
386
|
+
source: quizazz
|
|
387
|
+
ref: assessments/quiz.yml
|
|
388
|
+
pass_threshold: 0.7 # 70% required to count as passed
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`unlock_module_on_complete` is useful for "gateway" lessons — a single assessment that, once passed, opens the rest of the module and the next one.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
363
395
|
## Configuration File
|
|
364
396
|
|
|
365
|
-
An optional config file can set defaults for logging. The CLI always takes precedence.
|
|
397
|
+
An optional config file can set defaults for logging and locking. The CLI always takes precedence.
|
|
366
398
|
|
|
367
399
|
**Default location:** `~/.config/learningfoundry/config.yml`
|
|
368
400
|
|
|
@@ -370,6 +402,10 @@ An optional config file can set defaults for logging. The CLI always takes prece
|
|
|
370
402
|
logging:
|
|
371
403
|
level: INFO # DEBUG | INFO | WARNING | ERROR
|
|
372
404
|
output: stdout # stdout | stderr
|
|
405
|
+
|
|
406
|
+
locking:
|
|
407
|
+
sequential: false # default for all curricula on this machine
|
|
408
|
+
lesson_sequential: false
|
|
373
409
|
```
|
|
374
410
|
|
|
375
411
|
Pass a custom config location with `-c / --config`.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "learningfoundry"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.46.0"
|
|
8
8
|
description = "A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -16,6 +16,7 @@ DEFAULT_CONFIG_PATH = Path.home() / ".config" / "learningfoundry" / "config.yml"
|
|
|
16
16
|
|
|
17
17
|
_KNOWN_KEYS: dict[str, set[str]] = {
|
|
18
18
|
"logging": {"level", "output"},
|
|
19
|
+
"locking": {"sequential", "lesson_sequential"},
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
|
|
@@ -25,9 +26,16 @@ class LoggingConfig:
|
|
|
25
26
|
output: str = "stdout"
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
@dataclass
|
|
30
|
+
class LockingConfig:
|
|
31
|
+
sequential: bool = False
|
|
32
|
+
lesson_sequential: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
28
35
|
@dataclass
|
|
29
36
|
class AppConfig:
|
|
30
37
|
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
38
|
+
locking: LockingConfig = field(default_factory=LockingConfig)
|
|
31
39
|
|
|
32
40
|
|
|
33
41
|
def load_config(
|
|
@@ -66,6 +74,13 @@ def load_config(
|
|
|
66
74
|
config.logging.level = logging_raw["level"]
|
|
67
75
|
if "output" in logging_raw:
|
|
68
76
|
config.logging.output = logging_raw["output"]
|
|
77
|
+
locking_raw = raw.get("locking", {})
|
|
78
|
+
if "sequential" in locking_raw:
|
|
79
|
+
config.locking.sequential = bool(locking_raw["sequential"])
|
|
80
|
+
if "lesson_sequential" in locking_raw:
|
|
81
|
+
config.locking.lesson_sequential = bool(
|
|
82
|
+
locking_raw["lesson_sequential"]
|
|
83
|
+
)
|
|
69
84
|
|
|
70
85
|
if cli_overrides:
|
|
71
86
|
if "log_level" in cli_overrides and cli_overrides["log_level"] is not None:
|
|
@@ -40,6 +40,7 @@ class ResolvedContentBlock:
|
|
|
40
40
|
class ResolvedLesson:
|
|
41
41
|
id: str
|
|
42
42
|
title: str
|
|
43
|
+
unlock_module_on_complete: bool = False
|
|
43
44
|
content_blocks: list[ResolvedContentBlock] = field(default_factory=list)
|
|
44
45
|
|
|
45
46
|
|
|
@@ -48,6 +49,7 @@ class ResolvedModule:
|
|
|
48
49
|
id: str
|
|
49
50
|
title: str
|
|
50
51
|
description: str
|
|
52
|
+
locked: bool | None
|
|
51
53
|
pre_assessment: dict[str, Any] | None
|
|
52
54
|
post_assessment: dict[str, Any] | None
|
|
53
55
|
lessons: list[ResolvedLesson] = field(default_factory=list)
|
|
@@ -58,6 +60,7 @@ class ResolvedCurriculum:
|
|
|
58
60
|
version: str
|
|
59
61
|
title: str
|
|
60
62
|
description: str
|
|
63
|
+
locking: dict[str, Any] = field(default_factory=dict)
|
|
61
64
|
modules: list[ResolvedModule] = field(default_factory=list)
|
|
62
65
|
# Image assets referenced from any text block's markdown, deduped by
|
|
63
66
|
# content hash. Carried out-of-band — the generator copies these into
|
|
@@ -122,10 +125,17 @@ def resolve_curriculum(
|
|
|
122
125
|
)
|
|
123
126
|
)
|
|
124
127
|
|
|
128
|
+
locking = curriculum.curriculum.locking
|
|
129
|
+
locking_dict: dict[str, Any] = {
|
|
130
|
+
"sequential": locking.sequential,
|
|
131
|
+
"lesson_sequential": locking.lesson_sequential,
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
return ResolvedCurriculum(
|
|
126
135
|
version=curriculum.version,
|
|
127
136
|
title=curriculum.curriculum.title,
|
|
128
137
|
description=curriculum.curriculum.description,
|
|
138
|
+
locking=locking_dict,
|
|
129
139
|
modules=resolved_modules,
|
|
130
140
|
assets=list(assets_by_dest.values()),
|
|
131
141
|
)
|
|
@@ -174,6 +184,7 @@ def _resolve_module(
|
|
|
174
184
|
id=module.id,
|
|
175
185
|
title=module.title,
|
|
176
186
|
description=module.description,
|
|
187
|
+
locked=module.locked,
|
|
177
188
|
pre_assessment=pre,
|
|
178
189
|
post_assessment=post,
|
|
179
190
|
lessons=resolved_lessons,
|
|
@@ -206,6 +217,7 @@ def _resolve_lesson(
|
|
|
206
217
|
return ResolvedLesson(
|
|
207
218
|
id=lesson.id,
|
|
208
219
|
title=lesson.title,
|
|
220
|
+
unlock_module_on_complete=lesson.unlock_module_on_complete,
|
|
209
221
|
content_blocks=resolved_blocks,
|
|
210
222
|
)
|
|
211
223
|
|
|
@@ -228,6 +240,7 @@ def _resolve_block(
|
|
|
228
240
|
manifest = quiz_provider.compile_assessment(
|
|
229
241
|
Path(block.ref), base_dir
|
|
230
242
|
)
|
|
243
|
+
manifest["pass_threshold"] = block.pass_threshold
|
|
231
244
|
return ResolvedContentBlock(
|
|
232
245
|
type="quiz", source=block.source, ref=block.ref, content=manifest
|
|
233
246
|
)
|
|
@@ -63,6 +63,7 @@ class QuizBlock(BaseModel):
|
|
|
63
63
|
type: Literal["quiz"]
|
|
64
64
|
source: str
|
|
65
65
|
ref: str
|
|
66
|
+
pass_threshold: float = Field(0.0, ge=0.0, le=1.0)
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
class ExerciseBlock(BaseModel):
|
|
@@ -86,6 +87,7 @@ ContentBlock = Annotated[
|
|
|
86
87
|
class Lesson(BaseModel):
|
|
87
88
|
id: str
|
|
88
89
|
title: str
|
|
90
|
+
unlock_module_on_complete: bool = False
|
|
89
91
|
content_blocks: list[ContentBlock]
|
|
90
92
|
|
|
91
93
|
@field_validator("id")
|
|
@@ -98,6 +100,7 @@ class Module(BaseModel):
|
|
|
98
100
|
id: str
|
|
99
101
|
title: str
|
|
100
102
|
description: str = ""
|
|
103
|
+
locked: bool | None = None
|
|
101
104
|
pre_assessment: AssessmentRef | None = None
|
|
102
105
|
post_assessment: AssessmentRef | None = None
|
|
103
106
|
lessons: list[Lesson]
|
|
@@ -114,9 +117,17 @@ class Module(BaseModel):
|
|
|
114
117
|
return self
|
|
115
118
|
|
|
116
119
|
|
|
120
|
+
class LockingConfig(BaseModel):
|
|
121
|
+
"""Curriculum-level content locking configuration."""
|
|
122
|
+
|
|
123
|
+
sequential: bool = False
|
|
124
|
+
lesson_sequential: bool = False
|
|
125
|
+
|
|
126
|
+
|
|
117
127
|
class CurriculumDef(BaseModel):
|
|
118
128
|
title: str
|
|
119
129
|
description: str = ""
|
|
130
|
+
locking: LockingConfig = Field(default_factory=LockingConfig)
|
|
120
131
|
modules: list[Module]
|
|
121
132
|
|
|
122
133
|
@model_validator(mode="after")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright 2026 Pointmatic
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// FR-P9 regression coverage: in-app navigation must update the URL.
|
|
5
|
+
// Before v0.46.0 the sidebar / Next button updated `currentPosition` but
|
|
6
|
+
// not the route, so `LessonView` was never re-mounted on lesson change.
|
|
7
|
+
import { expect, test } from '@playwright/test';
|
|
8
|
+
|
|
9
|
+
test.describe('lesson navigation routing', () => {
|
|
10
|
+
test('sidebar lesson click updates URL', async ({ page }) => {
|
|
11
|
+
await page.goto('/');
|
|
12
|
+
// Open the first module by clicking its header in the sidebar.
|
|
13
|
+
const firstModuleHeader = page.locator('aside nav button').first();
|
|
14
|
+
await firstModuleHeader.click();
|
|
15
|
+
|
|
16
|
+
// Click the first lesson row in the now-expanded module.
|
|
17
|
+
const firstLessonRow = page.locator('aside nav ul ul button').first();
|
|
18
|
+
await firstLessonRow.click();
|
|
19
|
+
|
|
20
|
+
// URL must reflect the click; before v0.46.0 it stayed at "/".
|
|
21
|
+
await expect(page).toHaveURL(/\/[^/]+\/[^/]+$/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('dashboard "Start module" deep-links into a lesson', async ({ page }) => {
|
|
25
|
+
await page.goto('/');
|
|
26
|
+
const startBtn = page.getByRole('button', { name: /start module|continue/i }).first();
|
|
27
|
+
await startBtn.click();
|
|
28
|
+
await expect(page).toHaveURL(/\/[^/]+\/[^/]+$/);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright 2026 Pointmatic
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// FR-P9 regression: progress reactivity. Reaching a lesson page must mark
|
|
5
|
+
// it `in_progress` and the sidebar status icon must reflect that on the
|
|
6
|
+
// next sidebar render — without a page reload.
|
|
7
|
+
import { expect, test } from '@playwright/test';
|
|
8
|
+
|
|
9
|
+
test.describe('progress reactivity', () => {
|
|
10
|
+
test('navigating into a lesson marks it in_progress in the sidebar', async ({ page }) => {
|
|
11
|
+
await page.goto('/');
|
|
12
|
+
|
|
13
|
+
// Expand the first module and click into its first lesson.
|
|
14
|
+
await page.locator('aside nav button').first().click();
|
|
15
|
+
await page.locator('aside nav ul ul button').first().click();
|
|
16
|
+
await expect(page).toHaveURL(/\/[^/]+\/[^/]+$/);
|
|
17
|
+
|
|
18
|
+
// Sidebar status icons: ○ = not_started, … = in_progress, ✓ = complete.
|
|
19
|
+
// After landing on the lesson page, the active row should not still
|
|
20
|
+
// show ○ (regression check; the previous bug left status forever ○).
|
|
21
|
+
const activeIcon = page.locator('aside nav ul ul button.bg-blue-100 span').first();
|
|
22
|
+
await expect(activeIcon).not.toHaveText('○');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright 2026 Pointmatic
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// FR-P10 regression coverage: when navigating between two lessons that
|
|
5
|
+
// each contain a video block, only the new lesson's iframe should be
|
|
6
|
+
// present — the previous player must be destroyed. The {#key} wrapper
|
|
7
|
+
// in `[module]/[lesson]/+page.svelte` plus the stable block key in
|
|
8
|
+
// `LessonView` enforce this.
|
|
9
|
+
import { expect, test } from '@playwright/test';
|
|
10
|
+
|
|
11
|
+
test.describe('video block lifecycle', () => {
|
|
12
|
+
test('lesson page renders at most one YouTube iframe per video block', async ({ page }) => {
|
|
13
|
+
await page.goto('/');
|
|
14
|
+
await page.locator('aside nav button').first().click();
|
|
15
|
+
await page.locator('aside nav ul ul button').first().click();
|
|
16
|
+
await expect(page).toHaveURL(/\/[^/]+\/[^/]+$/);
|
|
17
|
+
|
|
18
|
+
// Wait briefly for the YouTube IFrame API to upgrade the placeholder
|
|
19
|
+
// `<div id="yt-player-…">` into an `<iframe>`. If the API is blocked
|
|
20
|
+
// (no network in CI), there will be zero iframes — that's still a
|
|
21
|
+
// pass for "no leaked iframes from a prior lesson", which is what
|
|
22
|
+
// the regression cares about.
|
|
23
|
+
await page.waitForTimeout(2000);
|
|
24
|
+
const iframes = await page.locator('iframe[src*="youtube"]').count();
|
|
25
|
+
expect(iframes).toBeLessThanOrEqual(1);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
10
10
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
11
11
|
"test": "vitest run",
|
|
12
|
+
"e2e": "playwright test",
|
|
12
13
|
"postinstall": "node -e \"const fs=require('fs'); fs.mkdirSync('static',{recursive:true}); fs.copyFileSync(require.resolve('sql.js/dist/sql-wasm.wasm'),'static/sql-wasm.wasm')\""
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"svelte": "^5.0.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
25
|
+
"@playwright/test": "^1.48.0",
|
|
24
26
|
"@sveltejs/adapter-static": "^3.0.0",
|
|
25
27
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
26
28
|
"@tailwindcss/typography": "^0.5.16",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright 2026 Pointmatic
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
4
|
+
|
|
5
|
+
const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 4173);
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testDir: './e2e',
|
|
9
|
+
fullyParallel: false,
|
|
10
|
+
reporter: [['list']],
|
|
11
|
+
use: {
|
|
12
|
+
baseURL: `http://localhost:${PORT}`,
|
|
13
|
+
trace: 'retain-on-failure'
|
|
14
|
+
},
|
|
15
|
+
webServer: {
|
|
16
|
+
command: `pnpm preview --port ${PORT} --strictPort`,
|
|
17
|
+
port: PORT,
|
|
18
|
+
reuseExistingServer: !process.env.CI,
|
|
19
|
+
timeout: 60_000
|
|
20
|
+
},
|
|
21
|
+
projects: [
|
|
22
|
+
{
|
|
23
|
+
name: 'chromium',
|
|
24
|
+
use: { ...devices['Desktop Chrome'] }
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<!-- Copyright 2026 Pointmatic — SPDX-License-Identifier: Apache-2.0 -->
|
|
2
2
|
<!--
|
|
3
3
|
Dispatcher component — renders the correct block component based on `block.type`.
|
|
4
|
+
Forwards child completion events upward as `onblockcomplete(blockIndex)`.
|
|
4
5
|
-->
|
|
5
6
|
<script lang="ts">
|
|
6
7
|
import type { ContentBlock, QuizScore } from '$lib/types/index.js';
|
|
@@ -20,20 +21,28 @@
|
|
|
20
21
|
|
|
21
22
|
interface Props {
|
|
22
23
|
block: ContentBlock;
|
|
24
|
+
blockIndex: number;
|
|
25
|
+
onblockcomplete?: (blockIndex: number) => void;
|
|
23
26
|
onquizcomplete?: (score: QuizScore) => void;
|
|
24
27
|
}
|
|
25
|
-
let { block, onquizcomplete }: Props = $props();
|
|
28
|
+
let { block, blockIndex, onblockcomplete, onquizcomplete }: Props = $props();
|
|
29
|
+
|
|
30
|
+
function handleBlockComplete() {
|
|
31
|
+
onblockcomplete?.(blockIndex);
|
|
32
|
+
}
|
|
26
33
|
</script>
|
|
27
34
|
|
|
28
35
|
{#if block.type === 'text'}
|
|
29
|
-
<TextBlock content={block.content as TextContent} />
|
|
36
|
+
<TextBlock content={block.content as TextContent} ontextcomplete={handleBlockComplete} />
|
|
30
37
|
{:else if block.type === 'video'}
|
|
31
|
-
<VideoBlock content={block.content as VideoContent} />
|
|
38
|
+
<VideoBlock content={block.content as VideoContent} onvideocomplete={handleBlockComplete} />
|
|
32
39
|
{:else if block.type === 'quiz'}
|
|
33
40
|
<QuizBlock
|
|
34
41
|
manifest={block.content as QuizManifest}
|
|
35
42
|
quizRef={block.ref ?? ''}
|
|
43
|
+
passThreshold={(block.content as QuizManifest).passThreshold ?? 0.0}
|
|
36
44
|
oncomplete={onquizcomplete}
|
|
45
|
+
onquizcomplete={handleBlockComplete}
|
|
37
46
|
/>
|
|
38
47
|
{:else if block.type === 'exercise'}
|
|
39
48
|
<ExerciseBlock content={block.content as ExerciseContent} />
|
learningfoundry-0.46.0/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!-- Copyright 2026 Pointmatic — SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { goto } from '$app/navigation';
|
|
4
|
+
import { currentPosition } from '$lib/stores/curriculum.js';
|
|
5
|
+
import type { Lesson, LessonProgress } from '$lib/types/index.js';
|
|
6
|
+
import { lessonStatusIcon, resolveLessonClick } from './module-list.helpers.js';
|
|
7
|
+
import { lessonHref } from './navigation.helpers.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
moduleId: string;
|
|
11
|
+
lessons: Lesson[];
|
|
12
|
+
progress?: Record<string, LessonProgress>;
|
|
13
|
+
optionalLessons?: Set<string>;
|
|
14
|
+
lockedLessons?: Set<string>;
|
|
15
|
+
}
|
|
16
|
+
let {
|
|
17
|
+
moduleId,
|
|
18
|
+
lessons,
|
|
19
|
+
progress = {},
|
|
20
|
+
optionalLessons = new Set(),
|
|
21
|
+
lockedLessons = new Set()
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
function statusIcon(lessonId: string): string {
|
|
25
|
+
const s = progress[lessonId]?.status;
|
|
26
|
+
const concrete = s === 'optional' ? undefined : s;
|
|
27
|
+
return lessonStatusIcon(lessonId, concrete, optionalLessons);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function statusClass(lessonId: string): string {
|
|
31
|
+
const s = progress[lessonId]?.status;
|
|
32
|
+
if (s === 'complete') return 'text-green-600';
|
|
33
|
+
if (s === 'in_progress') return 'text-blue-500';
|
|
34
|
+
return 'text-gray-400';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handleClick(lessonId: string) {
|
|
38
|
+
if (resolveLessonClick(lessonId, lockedLessons) === 'noop') return;
|
|
39
|
+
void goto(lessonHref(moduleId, lessonId));
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<ul class="space-y-1">
|
|
44
|
+
{#each lessons as lesson (lesson.id)}
|
|
45
|
+
{@const isActive =
|
|
46
|
+
$currentPosition?.moduleId === moduleId && $currentPosition?.lessonId === lesson.id}
|
|
47
|
+
{@const locked = lockedLessons.has(lesson.id)}
|
|
48
|
+
<li>
|
|
49
|
+
<button
|
|
50
|
+
onclick={() => handleClick(lesson.id)}
|
|
51
|
+
class="flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-sm transition-colors
|
|
52
|
+
{locked
|
|
53
|
+
? 'cursor-not-allowed text-gray-300'
|
|
54
|
+
: isActive
|
|
55
|
+
? 'bg-blue-100 font-medium text-blue-700'
|
|
56
|
+
: 'text-gray-700 hover:bg-gray-100'}"
|
|
57
|
+
aria-disabled={locked}
|
|
58
|
+
>
|
|
59
|
+
<span class="shrink-0 text-xs {locked ? 'text-gray-300' : statusClass(lesson.id)}"
|
|
60
|
+
>{statusIcon(lesson.id)}</span
|
|
61
|
+
>
|
|
62
|
+
<span class="truncate">{lesson.title}</span>
|
|
63
|
+
</button>
|
|
64
|
+
</li>
|
|
65
|
+
{/each}
|
|
66
|
+
</ul>
|