learningfoundry 0.28.0__tar.gz → 0.30.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.
Files changed (80) hide show
  1. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/CHANGELOG.md +27 -0
  2. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/PKG-INFO +1 -1
  3. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/pyproject.toml +1 -1
  4. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/__init__.py +1 -1
  5. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/package.json +1 -0
  6. learningfoundry-0.30.0/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.test.ts +164 -0
  7. {learningfoundry-0.28.0 → learningfoundry-0.30.0/src/learningfoundry}/sveltekit_template/src/lib/stores/curriculum.ts +22 -20
  8. learningfoundry-0.30.0/src/learningfoundry/sveltekit_template/src/routes/+layout.ts +10 -0
  9. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/vite.config.ts +7 -1
  10. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/.gitignore +0 -0
  11. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/LICENSE +0 -0
  12. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/README.md +0 -0
  13. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/docs/project-guide/README.md +0 -0
  14. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/__main__.py +0 -0
  15. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/cli.py +0 -0
  16. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/config.py +0 -0
  17. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/exceptions.py +0 -0
  18. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/generator.py +0 -0
  19. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/integrations/__init__.py +0 -0
  20. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/integrations/d3foundry_stub.py +0 -0
  21. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/integrations/nbfoundry_stub.py +0 -0
  22. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/integrations/protocols.py +0 -0
  23. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/integrations/quizazz.py +0 -0
  24. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/logging_config.py +0 -0
  25. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/parser.py +0 -0
  26. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/pipeline.py +0 -0
  27. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/py.typed +0 -0
  28. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/resolver.py +0 -0
  29. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/schema_v1.py +0 -0
  30. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/pnpm-lock.yaml +0 -0
  31. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/app.css +0 -0
  32. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/app.html +0 -0
  33. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/ContentBlock.svelte +0 -0
  34. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
  35. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +0 -0
  36. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +0 -0
  37. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +0 -0
  38. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
  39. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
  40. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
  41. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -0
  42. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/QuizBlock.svelte +0 -0
  43. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
  44. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
  45. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
  46. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/db/database.ts +0 -0
  47. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/db/index.ts +0 -0
  48. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +0 -0
  49. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/types/index.ts +0 -0
  50. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.ts +0 -0
  51. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.svelte +0 -0
  52. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/routes/+page.svelte +0 -0
  53. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
  54. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/static/.gitkeep +0 -0
  55. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/svelte.config.js +0 -0
  56. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/src/learningfoundry/sveltekit_template/tsconfig.json +0 -0
  57. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/app.css +0 -0
  58. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/app.html +0 -0
  59. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/ContentBlock.svelte +0 -0
  60. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
  61. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/LessonList.svelte +0 -0
  62. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/LessonView.svelte +0 -0
  63. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/ModuleList.svelte +0 -0
  64. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
  65. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
  66. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
  67. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -0
  68. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/QuizBlock.svelte +0 -0
  69. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
  70. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
  71. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
  72. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/db/database.ts +0 -0
  73. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/db/index.ts +0 -0
  74. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/db/progress.ts +0 -0
  75. {learningfoundry-0.28.0/src/learningfoundry → learningfoundry-0.30.0}/sveltekit_template/src/lib/stores/curriculum.ts +0 -0
  76. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/types/index.ts +0 -0
  77. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/lib/utils/markdown.ts +0 -0
  78. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/routes/+layout.svelte +0 -0
  79. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/routes/+page.svelte +0 -0
  80. {learningfoundry-0.28.0 → learningfoundry-0.30.0}/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.30.0] - 2026-04-27
11
+
12
+ ### Fixed
13
+
14
+ - **`Cannot call fetch eagerly during server-side rendering with relative URL (/curriculum.json)`** — SvelteKit's prerender pass was subscribing to the `curriculum` readable store during SSR of `+layout.svelte`, triggering a relative-URL fetch on the server. The template is a pure CSR SPA (runtime curriculum fetch, IndexedDB, sql.js/WASM) and was never intended to render on the server.
15
+ - `src/learningfoundry/sveltekit_template/src/routes/+layout.ts` — new file exporting `ssr = false` and `prerender = false`. With `adapter-static` + `fallback: 'index.html'` already configured in `svelte.config.js`, the SPA fallback handles every route client-side; prerendering is not needed and was failing on dynamic `[module]/[lesson]` routes anyway.
16
+
17
+ ## [0.29.0] - 2026-04-27
18
+
19
+ ### Fixed
20
+
21
+ - **Lesson content never rendered after clicking "Start module" / Next / Previous.** `navigateTo`, `navigateNext`, and `navigatePrev` in the SvelteKit template only updated the `currentPosition` Svelte store; they never changed the URL. Because lesson content is mounted by the dynamic route `/[module]/[lesson]/+page.svelte`, the route was never visited and `LessonView` (which renders the inlined markdown) never mounted — the left nav title updated, but the content area stayed on the home dashboard.
22
+ - `src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.ts` — `navigateTo` now also calls `goto('/${moduleId}/${lessonId}')`; `navigateNext` and `navigatePrev` refactored to compute the target and delegate to `navigateTo` so URL navigation happens for them too.
23
+
24
+ ### Added
25
+
26
+ - **Frontend unit-test infrastructure** for the SvelteKit template (the navigation regression went uncaught because the template had no test suite):
27
+ - `sveltekit_template/package.json` — added `jsdom` to devDependencies (`vitest` was already present)
28
+ - `sveltekit_template/vite.config.ts` — added vitest config block (`environment: 'jsdom'`, `include: src/**/*.{test,spec}.{js,ts}`)
29
+ - `sveltekit_template/src/lib/stores/curriculum.test.ts` — 9 cases covering `navigateTo` (URL + store update), `navigateNext` (within module / across modules / final-lesson no-op / null position), `navigatePrev` (within module / across modules / first-lesson no-op / null position); mocks `$app/navigation`'s `goto` via `vi.mock` and stubs global `fetch` to seed the curriculum readable
30
+ - `tests/test_smoke_sveltekit.py::test_pnpm_test_passes` — runs `pnpm test` (vitest) inside the installed template so the smoke run catches future frontend regressions
31
+
32
+ ### Verified
33
+
34
+ - `pyve test -m smoke` — 7/7 passed (Python build + vitest)
35
+ - Full Python suite — 195/195 passed; ruff and mypy clean
36
+
10
37
  ## [0.28.0] - 2026-04-26
11
38
 
12
39
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learningfoundry
3
- Version: 0.28.0
3
+ Version: 0.30.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "learningfoundry"
7
- version = "0.28.0"
7
+ version = "0.30.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"
@@ -1,4 +1,4 @@
1
1
  # Copyright 2026 Pointmatic
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- __version__ = "0.28.0"
4
+ __version__ = "0.30.0"
@@ -23,6 +23,7 @@
23
23
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
24
24
  "@tailwindcss/vite": "^4.0.0",
25
25
  "@types/sql.js": "^1.4.11",
26
+ "jsdom": "^25.0.0",
26
27
  "prettier": "^3.0.0",
27
28
  "prettier-plugin-svelte": "^3.0.0",
28
29
  "svelte-check": "^4.0.0",
@@ -0,0 +1,164 @@
1
+ // Copyright 2026 Pointmatic
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { get } from 'svelte/store';
5
+ import type { Curriculum } from '$lib/types/index.js';
6
+
7
+ // Mock $app/navigation BEFORE importing the module under test so its
8
+ // top-level `import { goto }` binds to the mock.
9
+ const gotoMock = vi.fn();
10
+ vi.mock('$app/navigation', () => ({
11
+ goto: gotoMock
12
+ }));
13
+
14
+ // Two-module fixture covering: same-module advance, cross-module advance,
15
+ // final-lesson edge case, and reverse traversal across module boundaries.
16
+ const FIXTURE: Curriculum = {
17
+ version: 1,
18
+ title: 'Test Curriculum',
19
+ description: 'For navigation tests',
20
+ modules: [
21
+ {
22
+ id: 'mod-01',
23
+ title: 'Module One',
24
+ description: '',
25
+ pre_assessment: null,
26
+ post_assessment: null,
27
+ lessons: [
28
+ { id: 'lesson-01', title: 'L1', content_blocks: [] },
29
+ { id: 'lesson-02', title: 'L2', content_blocks: [] }
30
+ ]
31
+ },
32
+ {
33
+ id: 'mod-02',
34
+ title: 'Module Two',
35
+ description: '',
36
+ pre_assessment: null,
37
+ post_assessment: null,
38
+ lessons: [
39
+ { id: 'lesson-01', title: 'M2L1', content_blocks: [] },
40
+ { id: 'lesson-02', title: 'M2L2', content_blocks: [] }
41
+ ]
42
+ }
43
+ ]
44
+ } as unknown as Curriculum;
45
+
46
+ // Stub global fetch so the `curriculum` readable store loads our fixture
47
+ // instead of doing a real network call (jsdom has no /curriculum.json).
48
+ vi.stubGlobal(
49
+ 'fetch',
50
+ vi.fn().mockResolvedValue({
51
+ ok: true,
52
+ status: 200,
53
+ statusText: 'OK',
54
+ json: async () => FIXTURE
55
+ })
56
+ );
57
+
58
+ // Import AFTER vi.mock and vi.stubGlobal so they're applied to the module
59
+ // under test on first load.
60
+ const {
61
+ curriculum,
62
+ currentPosition,
63
+ navigateTo,
64
+ navigateNext,
65
+ navigatePrev
66
+ } = await import('./curriculum.js');
67
+
68
+ // Wait for the readable store's async loader to resolve and emit the
69
+ // fixture. Once `curriculum` has data, the derived `modules` store that
70
+ // `navigateNext`/`navigatePrev` consume internally will have data too.
71
+ beforeAll(async () => {
72
+ await new Promise<void>((resolve) => {
73
+ const unsub = curriculum.subscribe((c) => {
74
+ if (c) {
75
+ unsub();
76
+ resolve();
77
+ }
78
+ });
79
+ });
80
+ });
81
+
82
+ beforeEach(() => {
83
+ gotoMock.mockClear();
84
+ currentPosition.set(null);
85
+ });
86
+
87
+ describe('navigateTo', () => {
88
+ it('updates currentPosition and calls goto with /{moduleId}/{lessonId}', () => {
89
+ navigateTo('mod-01', 'lesson-01');
90
+ expect(get(currentPosition)).toEqual({ moduleId: 'mod-01', lessonId: 'lesson-01' });
91
+ expect(gotoMock).toHaveBeenCalledTimes(1);
92
+ expect(gotoMock).toHaveBeenCalledWith('/mod-01/lesson-01');
93
+ });
94
+ });
95
+
96
+ describe('navigateNext', () => {
97
+ it('advances to the next lesson within the same module', () => {
98
+ navigateTo('mod-01', 'lesson-01');
99
+ gotoMock.mockClear();
100
+
101
+ navigateNext();
102
+ expect(get(currentPosition)).toEqual({ moduleId: 'mod-01', lessonId: 'lesson-02' });
103
+ expect(gotoMock).toHaveBeenCalledWith('/mod-01/lesson-02');
104
+ });
105
+
106
+ it('crosses module boundaries to the first lesson of the next module', () => {
107
+ navigateTo('mod-01', 'lesson-02');
108
+ gotoMock.mockClear();
109
+
110
+ navigateNext();
111
+ expect(get(currentPosition)).toEqual({ moduleId: 'mod-02', lessonId: 'lesson-01' });
112
+ expect(gotoMock).toHaveBeenCalledWith('/mod-02/lesson-01');
113
+ });
114
+
115
+ it('is a no-op past the final lesson', () => {
116
+ navigateTo('mod-02', 'lesson-02');
117
+ gotoMock.mockClear();
118
+
119
+ navigateNext();
120
+ expect(get(currentPosition)).toEqual({ moduleId: 'mod-02', lessonId: 'lesson-02' });
121
+ expect(gotoMock).not.toHaveBeenCalled();
122
+ });
123
+
124
+ it('is a no-op when currentPosition is null', () => {
125
+ navigateNext();
126
+ expect(get(currentPosition)).toBeNull();
127
+ expect(gotoMock).not.toHaveBeenCalled();
128
+ });
129
+ });
130
+
131
+ describe('navigatePrev', () => {
132
+ it('moves to the previous lesson within the same module', () => {
133
+ navigateTo('mod-01', 'lesson-02');
134
+ gotoMock.mockClear();
135
+
136
+ navigatePrev();
137
+ expect(get(currentPosition)).toEqual({ moduleId: 'mod-01', lessonId: 'lesson-01' });
138
+ expect(gotoMock).toHaveBeenCalledWith('/mod-01/lesson-01');
139
+ });
140
+
141
+ it('crosses module boundaries backward', () => {
142
+ navigateTo('mod-02', 'lesson-01');
143
+ gotoMock.mockClear();
144
+
145
+ navigatePrev();
146
+ expect(get(currentPosition)).toEqual({ moduleId: 'mod-01', lessonId: 'lesson-02' });
147
+ expect(gotoMock).toHaveBeenCalledWith('/mod-01/lesson-02');
148
+ });
149
+
150
+ it('is a no-op before the first lesson', () => {
151
+ navigateTo('mod-01', 'lesson-01');
152
+ gotoMock.mockClear();
153
+
154
+ navigatePrev();
155
+ expect(get(currentPosition)).toEqual({ moduleId: 'mod-01', lessonId: 'lesson-01' });
156
+ expect(gotoMock).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it('is a no-op when currentPosition is null', () => {
160
+ navigatePrev();
161
+ expect(get(currentPosition)).toBeNull();
162
+ expect(gotoMock).not.toHaveBeenCalled();
163
+ });
164
+ });
@@ -1,6 +1,7 @@
1
1
  // Copyright 2026 Pointmatic
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { derived, readable, writable } from 'svelte/store';
4
+ import { goto } from '$app/navigation';
4
5
  import type { Curriculum, Lesson, Module } from '$lib/types/index.js';
5
6
 
6
7
  // ---------------------------------------------------------------------------
@@ -87,36 +88,37 @@ export const nextLesson = derived(
87
88
 
88
89
  export function navigateTo(moduleId: string, lessonId: string): void {
89
90
  currentPosition.set({ moduleId, lessonId });
91
+ void goto(`/${moduleId}/${lessonId}`);
90
92
  }
91
93
 
92
94
  export function navigateNext(): void {
93
- currentPosition.update(($pos) => {
94
- if (!$pos) return $pos;
95
- let found = false;
96
- for (const mod of get(modules) as Module[]) {
97
- for (const lesson of mod.lessons as Lesson[]) {
98
- if (found) return { moduleId: mod.id, lessonId: lesson.id };
99
- if (mod.id === $pos.moduleId && lesson.id === $pos.lessonId) found = true;
95
+ const pos = get(currentPosition);
96
+ if (!pos) return;
97
+ let found = false;
98
+ for (const mod of get(modules) as Module[]) {
99
+ for (const lesson of mod.lessons as Lesson[]) {
100
+ if (found) {
101
+ navigateTo(mod.id, lesson.id);
102
+ return;
100
103
  }
104
+ if (mod.id === pos.moduleId && lesson.id === pos.lessonId) found = true;
101
105
  }
102
- return $pos;
103
- });
106
+ }
104
107
  }
105
108
 
106
109
  export function navigatePrev(): void {
107
- currentPosition.update(($pos) => {
108
- if (!$pos) return $pos;
109
- let prev: NavPosition | null = null;
110
- for (const mod of get(modules) as Module[]) {
111
- for (const lesson of mod.lessons as Lesson[]) {
112
- if (mod.id === $pos.moduleId && lesson.id === $pos.lessonId) {
113
- return prev ?? $pos;
114
- }
115
- prev = { moduleId: mod.id, lessonId: lesson.id };
110
+ const pos = get(currentPosition);
111
+ if (!pos) return;
112
+ let prev: NavPosition | null = null;
113
+ for (const mod of get(modules) as Module[]) {
114
+ for (const lesson of mod.lessons as Lesson[]) {
115
+ if (mod.id === pos.moduleId && lesson.id === pos.lessonId) {
116
+ if (prev) navigateTo(prev.moduleId, prev.lessonId);
117
+ return;
116
118
  }
119
+ prev = { moduleId: mod.id, lessonId: lesson.id };
117
120
  }
118
- return $pos;
119
- });
121
+ }
120
122
  }
121
123
 
122
124
  // get() helper for non-reactive reads inside update callbacks
@@ -0,0 +1,10 @@
1
+ // Copyright 2026 Pointmatic
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // This app is a client-side SPA: it loads `curriculum.json` at runtime,
5
+ // persists progress in IndexedDB, and uses sql.js (WASM) — none of which
6
+ // can run during server-side rendering. Disable SSR so the prerender
7
+ // pass does not subscribe to the curriculum store and trigger a relative
8
+ // `fetch('/curriculum.json')` on the server.
9
+ export const ssr = false;
10
+ export const prerender = false;
@@ -1,9 +1,15 @@
1
1
  // Copyright 2026 Pointmatic
2
2
  // SPDX-License-Identifier: Apache-2.0
3
+ /// <reference types="vitest" />
3
4
  import { sveltekit } from '@sveltejs/kit/vite';
4
5
  import tailwindcss from '@tailwindcss/vite';
5
6
  import { defineConfig } from 'vite';
6
7
 
7
8
  export default defineConfig({
8
- plugins: [tailwindcss(), sveltekit()]
9
+ plugins: [tailwindcss(), sveltekit()],
10
+ test: {
11
+ environment: 'jsdom',
12
+ include: ['src/**/*.{test,spec}.{js,ts}'],
13
+ globals: false
14
+ }
9
15
  });