@lessonkit/react 0.6.0 → 0.8.0

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.
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # `@lessonkit/react`
2
2
 
3
3
  [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
4
+ [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
4
5
  [![npm](https://img.shields.io/npm/v/@lessonkit/react.svg)](https://www.npmjs.com/package/@lessonkit/react)
5
- [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](../../LICENSE)
6
+ [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
6
7
 
7
8
  React components and hooks for building learning experiences in LessonKit.
8
9
 
10
+ **Docs:** [Components & hooks](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/components-and-hooks.html) · [Block catalog](https://lessonkit.readthedocs.io/en/latest/reference/block-catalog.html) · [Quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html) · [Theming & accessibility](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/theming-and-accessibility.html)
11
+
9
12
  ## Install
10
13
 
11
14
  ```bash
@@ -56,7 +59,14 @@ export default function App() {
56
59
  }
57
60
  ```
58
61
 
59
- ## API (0.6.0)
62
+ ## API (0.8.0)
63
+
64
+ ### Block catalog
65
+
66
+ - **JSON:** `@lessonkit/react/block-catalog.v1.json`
67
+ - **Schema:** `@lessonkit/react/block-contract.v1.json`
68
+ - **API:** `buildBlockCatalog()`, `getBlockCatalogEntry(type)`, `BLOCK_CATALOG`, `blockCatalogVersion`
69
+ - [Block catalog reference](https://lessonkit.readthedocs.io/en/latest/reference/block-catalog.html)
60
70
 
61
71
  ### Components
62
72
 
@@ -77,7 +87,7 @@ export default function App() {
77
87
 
78
88
  ### Theming
79
89
 
80
- - `ThemeProvider` — injects `--lk-*` CSS variables (see [`docs/THEMING.md`](../../docs/THEMING.md))
90
+ - `ThemeProvider` — injects `--lk-*` CSS variables ([theming reference](https://lessonkit.readthedocs.io/en/latest/reference/theming.html))
81
91
  - Props: `preset`, `mode` (`light` | `dark` | `system`), `theme` (partial override), `target` (`document` | `element`)
82
92
 
83
93
  ## Notes
@@ -97,7 +107,6 @@ export default function App() {
97
107
  - If you omit `session.sessionId`, the provider reuses a tab-scoped id via `sessionStorage` so React
98
108
  Strict Mode remounts do not split analytics sessions in development.
99
109
  - In development, invalid `courseId` / `lessonId` / `checkId` values log a one-time `console.warn`.
100
- - Accessibility guidance lives in [`docs/ACCESSIBILITY.md`](../../docs/ACCESSIBILITY.md).
101
- - Theming and token catalog: [`docs/THEMING.md`](../../docs/THEMING.md).
102
- - Identity and telemetry: [`docs/IDENTITY.md`](../../docs/IDENTITY.md), [`docs/TELEMETRY.md`](../../docs/TELEMETRY.md).
103
-
110
+ - [Accessibility reference](https://lessonkit.readthedocs.io/en/latest/reference/accessibility.html) — keyboard and screen-reader guidance.
111
+ - [Theming reference](https://lessonkit.readthedocs.io/en/latest/reference/theming.html) token catalog and overrides.
112
+ - [Identity](https://lessonkit.readthedocs.io/en/latest/reference/identity.html) · [Telemetry](https://lessonkit.readthedocs.io/en/latest/reference/telemetry.html) · [Block catalog](https://lessonkit.readthedocs.io/en/latest/reference/block-catalog.html) — IDs, events, and supported blocks.
@@ -0,0 +1,279 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "entries": [
4
+ {
5
+ "type": "Course",
6
+ "category": "container",
7
+ "description": "Top-level course shell; wraps LessonkitProvider and emits course lifecycle telemetry.",
8
+ "props": [
9
+ {
10
+ "name": "title",
11
+ "type": "string",
12
+ "required": true,
13
+ "description": "Course title shown in the h1."
14
+ },
15
+ {
16
+ "name": "courseId",
17
+ "type": "CourseId",
18
+ "required": true,
19
+ "description": "Stable course identifier for telemetry and packaging."
20
+ },
21
+ {
22
+ "name": "config",
23
+ "type": "Omit<LessonkitConfig, 'courseId'>",
24
+ "required": false,
25
+ "description": "Runtime config (tracking, xAPI, session, lxpack bridge). courseId is merged from props."
26
+ },
27
+ {
28
+ "name": "children",
29
+ "type": "ReactNode",
30
+ "required": true,
31
+ "description": "Lessons and course chrome."
32
+ }
33
+ ],
34
+ "requiredIds": [
35
+ "courseId"
36
+ ],
37
+ "a11y": {
38
+ "element": "section",
39
+ "ariaLabel": "title prop",
40
+ "keyboard": "No block-specific keyboard behavior; focus flows to child content.",
41
+ "notes": "Renders h1 with course title. Wrap with ThemeProvider at app root for theming."
42
+ },
43
+ "theming": {
44
+ "surface": "global-inherit",
45
+ "stylingNotes": "Inherits --lk-* CSS variables from ThemeProvider on document or scoped host."
46
+ },
47
+ "telemetry": {
48
+ "emits": [
49
+ "course_started",
50
+ "course_completed"
51
+ ]
52
+ }
53
+ },
54
+ {
55
+ "type": "Lesson",
56
+ "category": "container",
57
+ "description": "Lesson container; sets active lesson on mount and completes on unmount.",
58
+ "props": [
59
+ {
60
+ "name": "title",
61
+ "type": "string",
62
+ "required": true,
63
+ "description": "Lesson title shown in the h2."
64
+ },
65
+ {
66
+ "name": "lessonId",
67
+ "type": "LessonId",
68
+ "required": true,
69
+ "description": "Stable lesson identifier for telemetry and packaging."
70
+ },
71
+ {
72
+ "name": "children",
73
+ "type": "ReactNode",
74
+ "required": true,
75
+ "description": "Scenario, Quiz, Reflection, and other blocks."
76
+ }
77
+ ],
78
+ "requiredIds": [
79
+ "lessonId"
80
+ ],
81
+ "parentConstraints": [
82
+ "Course"
83
+ ],
84
+ "a11y": {
85
+ "element": "article",
86
+ "ariaLabel": "title prop",
87
+ "keyboard": "No block-specific keyboard behavior; focus flows to child content.",
88
+ "notes": "Renders h2 with lesson title. Only one Lesson should be mounted as active at a time in typical SPA layouts."
89
+ },
90
+ "theming": {
91
+ "surface": "global-inherit",
92
+ "stylingNotes": "Inherits --lk-* CSS variables from ThemeProvider."
93
+ },
94
+ "telemetry": {
95
+ "emits": [
96
+ "lesson_started",
97
+ "lesson_completed",
98
+ "lesson_time_on_task"
99
+ ]
100
+ }
101
+ },
102
+ {
103
+ "type": "Scenario",
104
+ "category": "content",
105
+ "description": "Scenario or narrative content region for branching stories and situational context.",
106
+ "props": [
107
+ {
108
+ "name": "blockId",
109
+ "type": "BlockId",
110
+ "required": false,
111
+ "description": "Optional stable block id for interaction telemetry URNs."
112
+ },
113
+ {
114
+ "name": "children",
115
+ "type": "ReactNode",
116
+ "required": true,
117
+ "description": "Scenario narrative and custom UI."
118
+ }
119
+ ],
120
+ "requiredIds": [],
121
+ "optionalIds": [
122
+ "blockId"
123
+ ],
124
+ "parentConstraints": [
125
+ "Lesson"
126
+ ],
127
+ "a11y": {
128
+ "element": "section",
129
+ "ariaLabel": "Scenario",
130
+ "keyboard": "No block-specific keyboard behavior; custom children may define their own.",
131
+ "notes": "Use for situational framing. Pair with useTracking() for branching interactions."
132
+ },
133
+ "theming": {
134
+ "surface": "global-inherit",
135
+ "dataAttributes": [
136
+ "data-lk-block-id"
137
+ ],
138
+ "stylingNotes": "Optional data-lk-block-id when blockId is set. Style via app CSS using --lk-* tokens."
139
+ },
140
+ "telemetry": {
141
+ "emits": [],
142
+ "manualTracking": "useTracking().track('interaction', { kind, blockId, payload })"
143
+ }
144
+ },
145
+ {
146
+ "type": "Reflection",
147
+ "category": "content",
148
+ "description": "Reflection prompt with a textarea for learner free-text responses.",
149
+ "props": [
150
+ {
151
+ "name": "blockId",
152
+ "type": "BlockId",
153
+ "required": false,
154
+ "description": "Optional stable block id for interaction telemetry URNs."
155
+ },
156
+ {
157
+ "name": "prompt",
158
+ "type": "string",
159
+ "required": false,
160
+ "description": "Reflection question or instruction."
161
+ },
162
+ {
163
+ "name": "children",
164
+ "type": "ReactNode",
165
+ "required": false,
166
+ "description": "Optional content above the textarea."
167
+ }
168
+ ],
169
+ "requiredIds": [],
170
+ "optionalIds": [
171
+ "blockId"
172
+ ],
173
+ "parentConstraints": [
174
+ "Lesson"
175
+ ],
176
+ "a11y": {
177
+ "element": "section",
178
+ "ariaLabel": "Reflection",
179
+ "keyboard": "Textarea is keyboard-focusable; standard text entry.",
180
+ "notes": "When prompt is set, textarea uses aria-labelledby; otherwise aria-label='Reflection response'."
181
+ },
182
+ "theming": {
183
+ "surface": "global-inherit",
184
+ "dataAttributes": [
185
+ "data-lk-block-id"
186
+ ],
187
+ "stylingNotes": "Optional data-lk-block-id when blockId is set. Style textarea via app CSS."
188
+ },
189
+ "telemetry": {
190
+ "emits": [],
191
+ "manualTracking": "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
192
+ }
193
+ },
194
+ {
195
+ "type": "Quiz",
196
+ "aliases": [
197
+ "KnowledgeCheck"
198
+ ],
199
+ "category": "assessment",
200
+ "description": "Single-question multiple-choice assessment with automatic answer and completion telemetry.",
201
+ "props": [
202
+ {
203
+ "name": "checkId",
204
+ "type": "CheckId",
205
+ "required": true,
206
+ "description": "Stable check identifier for telemetry and LXPack assessments."
207
+ },
208
+ {
209
+ "name": "question",
210
+ "type": "string",
211
+ "required": true,
212
+ "description": "Question text shown above choices."
213
+ },
214
+ {
215
+ "name": "choices",
216
+ "type": "string[]",
217
+ "required": true,
218
+ "description": "Radio button choice labels."
219
+ },
220
+ {
221
+ "name": "answer",
222
+ "type": "string",
223
+ "required": true,
224
+ "description": "Correct choice value (must match one choice)."
225
+ }
226
+ ],
227
+ "requiredIds": [
228
+ "checkId"
229
+ ],
230
+ "parentConstraints": [
231
+ "Lesson"
232
+ ],
233
+ "a11y": {
234
+ "element": "section",
235
+ "ariaLabel": "Quiz",
236
+ "keyboard": "Radio group navigable with arrow keys; one choice per question.",
237
+ "liveRegions": "role='status' aria-live='polite' for Correct / Try again feedback.",
238
+ "notes": "Fieldset with visually hidden legend. KnowledgeCheck is an alias that renders Quiz with identical behavior."
239
+ },
240
+ "theming": {
241
+ "surface": "global-inherit",
242
+ "dataAttributes": [
243
+ "data-lk-check-id"
244
+ ],
245
+ "stylingNotes": "data-lk-check-id set from checkId. Style labels and feedback via app CSS."
246
+ },
247
+ "telemetry": {
248
+ "emits": [
249
+ "quiz_answered",
250
+ "quiz_completed"
251
+ ],
252
+ "requiresActiveLesson": true
253
+ }
254
+ },
255
+ {
256
+ "type": "ProgressTracker",
257
+ "category": "chrome",
258
+ "description": "Displays count of completed lessons from runtime progress state.",
259
+ "props": [],
260
+ "requiredIds": [],
261
+ "parentConstraints": [
262
+ "Course"
263
+ ],
264
+ "a11y": {
265
+ "element": "aside",
266
+ "ariaLabel": "Progress",
267
+ "keyboard": "Presentational; no interactive elements.",
268
+ "notes": "Shows 'Lessons completed: N' from progress.completedLessonIds."
269
+ },
270
+ "theming": {
271
+ "surface": "global-inherit",
272
+ "stylingNotes": "Inherits --lk-* CSS variables; style via app CSS."
273
+ },
274
+ "telemetry": {
275
+ "emits": []
276
+ }
277
+ }
278
+ ]
279
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lessonkit.dev/schemas/block-contract.v1.json",
4
+ "title": "LessonKit Block Catalog v1",
5
+ "description": "Machine-readable runtime block catalog for @lessonkit/react primitives.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "entries"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "entries": {
12
+ "type": "array",
13
+ "minItems": 1,
14
+ "items": { "$ref": "#/$defs/blockCatalogEntry" }
15
+ }
16
+ },
17
+ "$defs": {
18
+ "blockPropSpec": {
19
+ "type": "object",
20
+ "additionalProperties": false,
21
+ "required": ["name", "type", "required", "description"],
22
+ "properties": {
23
+ "name": { "type": "string", "minLength": 1 },
24
+ "type": { "type": "string", "minLength": 1 },
25
+ "required": { "type": "boolean" },
26
+ "description": { "type": "string", "minLength": 1 }
27
+ }
28
+ },
29
+ "blockCatalogEntry": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": ["type", "category", "description", "props", "requiredIds", "a11y", "theming", "telemetry"],
33
+ "properties": {
34
+ "type": { "type": "string", "minLength": 1 },
35
+ "aliases": {
36
+ "type": "array",
37
+ "items": { "type": "string", "minLength": 1 }
38
+ },
39
+ "category": {
40
+ "type": "string",
41
+ "enum": ["container", "content", "assessment", "chrome"]
42
+ },
43
+ "description": { "type": "string", "minLength": 1 },
44
+ "props": {
45
+ "type": "array",
46
+ "items": { "$ref": "#/$defs/blockPropSpec" }
47
+ },
48
+ "requiredIds": {
49
+ "type": "array",
50
+ "items": { "type": "string" }
51
+ },
52
+ "optionalIds": {
53
+ "type": "array",
54
+ "items": { "type": "string", "minLength": 1 }
55
+ },
56
+ "parentConstraints": {
57
+ "type": "array",
58
+ "items": { "type": "string", "minLength": 1 }
59
+ },
60
+ "a11y": {
61
+ "type": "object",
62
+ "additionalProperties": false,
63
+ "required": ["element", "ariaLabel", "keyboard", "notes"],
64
+ "properties": {
65
+ "element": { "type": "string", "minLength": 1 },
66
+ "ariaLabel": { "type": "string", "minLength": 1 },
67
+ "keyboard": { "type": "string", "minLength": 1 },
68
+ "liveRegions": { "type": "string" },
69
+ "notes": { "type": "string", "minLength": 1 }
70
+ }
71
+ },
72
+ "theming": {
73
+ "type": "object",
74
+ "additionalProperties": false,
75
+ "required": ["surface", "stylingNotes"],
76
+ "properties": {
77
+ "surface": { "const": "global-inherit" },
78
+ "dataAttributes": {
79
+ "type": "array",
80
+ "items": { "type": "string", "minLength": 1 }
81
+ },
82
+ "stylingNotes": { "type": "string", "minLength": 1 }
83
+ }
84
+ },
85
+ "telemetry": {
86
+ "type": "object",
87
+ "additionalProperties": false,
88
+ "required": ["emits"],
89
+ "properties": {
90
+ "emits": {
91
+ "type": "array",
92
+ "items": { "type": "string" }
93
+ },
94
+ "requiresActiveLesson": { "type": "boolean" },
95
+ "manualTracking": { "type": "string", "minLength": 1 }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BLOCK_CATALOG: () => BLOCK_CATALOG,
33
34
  Course: () => Course,
34
35
  KnowledgeCheck: () => KnowledgeCheck,
35
36
  Lesson: () => Lesson,
@@ -39,6 +40,9 @@ __export(index_exports, {
39
40
  Reflection: () => Reflection,
40
41
  Scenario: () => Scenario,
41
42
  ThemeProvider: () => ThemeProvider,
43
+ blockCatalogVersion: () => blockCatalogVersion,
44
+ buildBlockCatalog: () => buildBlockCatalog,
45
+ getBlockCatalogEntry: () => getBlockCatalogEntry,
42
46
  useCompletion: () => useCompletion,
43
47
  useLessonkit: () => useLessonkit,
44
48
  useProgress: () => useProgress,
@@ -250,6 +254,9 @@ function createProgressController() {
250
254
  completeLesson: (lessonId, completedAtMs) => {
251
255
  if (completedLessonIds.has(lessonId)) return { didComplete: false };
252
256
  completedLessonIds = new Set(completedLessonIds).add(lessonId);
257
+ if (activeLessonId === lessonId) {
258
+ activeLessonId = void 0;
259
+ }
253
260
  const startedAt = lessonStartTimes.get(lessonId);
254
261
  lessonStartTimes.delete(lessonId);
255
262
  const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
@@ -416,7 +423,9 @@ function LessonkitProvider(props) {
416
423
  );
417
424
  }
418
425
  return () => {
419
- disposeTrackingClient(prev);
426
+ if (prev !== trackingRef.current) {
427
+ disposeTrackingClient(prev);
428
+ }
420
429
  };
421
430
  }, [
422
431
  trackingEnabled,
@@ -453,9 +462,15 @@ function LessonkitProvider(props) {
453
462
  const prevCourseIdRef = (0, import_react.useRef)(config.courseId);
454
463
  (0, import_react.useEffect)(() => {
455
464
  if (prevCourseIdRef.current === config.courseId) return;
465
+ const previousActiveLesson = progressRef.current.getState().activeLessonId;
456
466
  prevCourseIdRef.current = config.courseId;
457
467
  progressRef.current = createProgressController();
458
468
  syncProgress();
469
+ if (previousActiveLesson) {
470
+ progressRef.current.setActiveLesson(previousActiveLesson, Date.now());
471
+ syncProgress();
472
+ track("lesson_started", { lessonId: previousActiveLesson }, { lessonId: previousActiveLesson });
473
+ }
459
474
  const sessionId = sessionIdRef.current;
460
475
  const cid = config.courseId;
461
476
  if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
@@ -473,13 +488,7 @@ function LessonkitProvider(props) {
473
488
  { lxpackBridge: lxpackBridgeModeRef.current }
474
489
  );
475
490
  }
476
- }, [config.courseId, syncProgress]);
477
- (0, import_react.useEffect)(() => {
478
- return () => {
479
- trackingRef.current?.flush?.();
480
- void xapiRef.current?.flush();
481
- };
482
- }, []);
491
+ }, [config.courseId, syncProgress, track]);
483
492
  const emitLessonCompleted = (0, import_react.useCallback)(
484
493
  (lessonId, durationMs) => {
485
494
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -495,9 +504,22 @@ function LessonkitProvider(props) {
495
504
  if (!result.didComplete) return;
496
505
  syncProgress();
497
506
  emitLessonCompleted(lessonId, result.durationMs);
507
+ void trackingRef.current?.flush?.();
498
508
  },
499
509
  [syncProgress, emitLessonCompleted]
500
510
  );
511
+ (0, import_react.useEffect)(() => {
512
+ return () => {
513
+ const client = trackingRef.current;
514
+ void xapiRef.current?.flush();
515
+ setTimeout(() => {
516
+ client?.flush?.();
517
+ setTimeout(() => {
518
+ client?.dispose?.();
519
+ }, 0);
520
+ }, 0);
521
+ };
522
+ }, []);
501
523
  const setActiveLesson = (0, import_react.useCallback)(
502
524
  (lessonId) => {
503
525
  const current = progressRef.current.getState();
@@ -620,24 +642,21 @@ function Course(props) {
620
642
  }
621
643
  function Lesson(props) {
622
644
  warnInvalidComponentId(props.lessonId, "lessonId");
623
- const { setActiveLesson } = useLessonkit();
645
+ const { setActiveLesson, config } = useLessonkit();
624
646
  const { completeLesson } = useCompletion();
625
647
  const id = props.lessonId;
626
- const pendingCompleteRef = (0, import_react3.useRef)(null);
648
+ const lessonMountGenerationRef = (0, import_react3.useRef)(0);
627
649
  (0, import_react3.useEffect)(() => {
628
- if (pendingCompleteRef.current !== null) {
629
- clearTimeout(pendingCompleteRef.current);
630
- pendingCompleteRef.current = null;
631
- }
650
+ const generation = ++lessonMountGenerationRef.current;
632
651
  setActiveLesson(id);
633
652
  return () => {
634
653
  const lessonId = id;
635
- pendingCompleteRef.current = setTimeout(() => {
636
- pendingCompleteRef.current = null;
654
+ queueMicrotask(() => {
655
+ if (lessonMountGenerationRef.current !== generation) return;
637
656
  completeLesson(lessonId);
638
- }, 0);
657
+ });
639
658
  };
640
- }, [id, setActiveLesson, completeLesson]);
659
+ }, [id, config.courseId, setActiveLesson, completeLesson]);
641
660
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
642
661
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: props.title }),
643
662
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
@@ -829,8 +848,196 @@ function useTheme() {
829
848
  }
830
849
  return ctx;
831
850
  }
851
+
852
+ // src/blockCatalog.ts
853
+ var blockCatalogVersion = 1;
854
+ var BLOCK_CATALOG = [
855
+ {
856
+ type: "Course",
857
+ category: "container",
858
+ description: "Top-level course shell; wraps LessonkitProvider and emits course lifecycle telemetry.",
859
+ props: [
860
+ { name: "title", type: "string", required: true, description: "Course title shown in the h1." },
861
+ { name: "courseId", type: "CourseId", required: true, description: "Stable course identifier for telemetry and packaging." },
862
+ {
863
+ name: "config",
864
+ type: "Omit<LessonkitConfig, 'courseId'>",
865
+ required: false,
866
+ description: "Runtime config (tracking, xAPI, session, lxpack bridge). courseId is merged from props."
867
+ },
868
+ { name: "children", type: "ReactNode", required: true, description: "Lessons and course chrome." }
869
+ ],
870
+ requiredIds: ["courseId"],
871
+ a11y: {
872
+ element: "section",
873
+ ariaLabel: "title prop",
874
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
875
+ notes: "Renders h1 with course title. Wrap with ThemeProvider at app root for theming."
876
+ },
877
+ theming: {
878
+ surface: "global-inherit",
879
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider on document or scoped host."
880
+ },
881
+ telemetry: {
882
+ emits: ["course_started", "course_completed"]
883
+ }
884
+ },
885
+ {
886
+ type: "Lesson",
887
+ category: "container",
888
+ description: "Lesson container; sets active lesson on mount and completes on unmount.",
889
+ props: [
890
+ { name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
891
+ { name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
892
+ { name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
893
+ ],
894
+ requiredIds: ["lessonId"],
895
+ parentConstraints: ["Course"],
896
+ a11y: {
897
+ element: "article",
898
+ ariaLabel: "title prop",
899
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
900
+ notes: "Renders h2 with lesson title. Only one Lesson should be mounted as active at a time in typical SPA layouts."
901
+ },
902
+ theming: {
903
+ surface: "global-inherit",
904
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider."
905
+ },
906
+ telemetry: {
907
+ emits: ["lesson_started", "lesson_completed", "lesson_time_on_task"]
908
+ }
909
+ },
910
+ {
911
+ type: "Scenario",
912
+ category: "content",
913
+ description: "Scenario or narrative content region for branching stories and situational context.",
914
+ props: [
915
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
916
+ { name: "children", type: "ReactNode", required: true, description: "Scenario narrative and custom UI." }
917
+ ],
918
+ requiredIds: [],
919
+ optionalIds: ["blockId"],
920
+ parentConstraints: ["Lesson"],
921
+ a11y: {
922
+ element: "section",
923
+ ariaLabel: "Scenario",
924
+ keyboard: "No block-specific keyboard behavior; custom children may define their own.",
925
+ notes: "Use for situational framing. Pair with useTracking() for branching interactions."
926
+ },
927
+ theming: {
928
+ surface: "global-inherit",
929
+ dataAttributes: ["data-lk-block-id"],
930
+ stylingNotes: "Optional data-lk-block-id when blockId is set. Style via app CSS using --lk-* tokens."
931
+ },
932
+ telemetry: {
933
+ emits: [],
934
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload })"
935
+ }
936
+ },
937
+ {
938
+ type: "Reflection",
939
+ category: "content",
940
+ description: "Reflection prompt with a textarea for learner free-text responses.",
941
+ props: [
942
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
943
+ { name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
944
+ { name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
945
+ ],
946
+ requiredIds: [],
947
+ optionalIds: ["blockId"],
948
+ parentConstraints: ["Lesson"],
949
+ a11y: {
950
+ element: "section",
951
+ ariaLabel: "Reflection",
952
+ keyboard: "Textarea is keyboard-focusable; standard text entry.",
953
+ notes: "When prompt is set, textarea uses aria-labelledby; otherwise aria-label='Reflection response'."
954
+ },
955
+ theming: {
956
+ surface: "global-inherit",
957
+ dataAttributes: ["data-lk-block-id"],
958
+ stylingNotes: "Optional data-lk-block-id when blockId is set. Style textarea via app CSS."
959
+ },
960
+ telemetry: {
961
+ emits: [],
962
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
963
+ }
964
+ },
965
+ {
966
+ type: "Quiz",
967
+ aliases: ["KnowledgeCheck"],
968
+ category: "assessment",
969
+ description: "Single-question multiple-choice assessment with automatic answer and completion telemetry.",
970
+ props: [
971
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
972
+ { name: "question", type: "string", required: true, description: "Question text shown above choices." },
973
+ { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
974
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
975
+ ],
976
+ requiredIds: ["checkId"],
977
+ parentConstraints: ["Lesson"],
978
+ a11y: {
979
+ element: "section",
980
+ ariaLabel: "Quiz",
981
+ keyboard: "Radio group navigable with arrow keys; one choice per question.",
982
+ liveRegions: "role='status' aria-live='polite' for Correct / Try again feedback.",
983
+ notes: "Fieldset with visually hidden legend. KnowledgeCheck is an alias that renders Quiz with identical behavior."
984
+ },
985
+ theming: {
986
+ surface: "global-inherit",
987
+ dataAttributes: ["data-lk-check-id"],
988
+ stylingNotes: "data-lk-check-id set from checkId. Style labels and feedback via app CSS."
989
+ },
990
+ telemetry: {
991
+ emits: ["quiz_answered", "quiz_completed"],
992
+ requiresActiveLesson: true
993
+ }
994
+ },
995
+ {
996
+ type: "ProgressTracker",
997
+ category: "chrome",
998
+ description: "Displays count of completed lessons from runtime progress state.",
999
+ props: [],
1000
+ requiredIds: [],
1001
+ parentConstraints: ["Course"],
1002
+ a11y: {
1003
+ element: "aside",
1004
+ ariaLabel: "Progress",
1005
+ keyboard: "Presentational; no interactive elements.",
1006
+ notes: "Shows 'Lessons completed: N' from progress.completedLessonIds."
1007
+ },
1008
+ theming: {
1009
+ surface: "global-inherit",
1010
+ stylingNotes: "Inherits --lk-* CSS variables; style via app CSS."
1011
+ },
1012
+ telemetry: {
1013
+ emits: []
1014
+ }
1015
+ }
1016
+ ];
1017
+ function buildBlockCatalog() {
1018
+ return BLOCK_CATALOG.map((entry) => ({
1019
+ ...entry,
1020
+ props: entry.props.map((p) => ({ ...p })),
1021
+ aliases: entry.aliases ? [...entry.aliases] : void 0,
1022
+ optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
1023
+ parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
1024
+ a11y: { ...entry.a11y },
1025
+ theming: {
1026
+ ...entry.theming,
1027
+ dataAttributes: entry.theming.dataAttributes ? [...entry.theming.dataAttributes] : void 0
1028
+ },
1029
+ telemetry: {
1030
+ ...entry.telemetry,
1031
+ emits: [...entry.telemetry.emits]
1032
+ }
1033
+ }));
1034
+ }
1035
+ function getBlockCatalogEntry(type) {
1036
+ return BLOCK_CATALOG.find((entry) => entry.type === type || entry.aliases?.includes(type));
1037
+ }
832
1038
  // Annotate the CommonJS export names for ESM import in node:
833
1039
  0 && (module.exports = {
1040
+ BLOCK_CATALOG,
834
1041
  Course,
835
1042
  KnowledgeCheck,
836
1043
  Lesson,
@@ -840,6 +1047,9 @@ function useTheme() {
840
1047
  Reflection,
841
1048
  Scenario,
842
1049
  ThemeProvider,
1050
+ blockCatalogVersion,
1051
+ buildBlockCatalog,
1052
+ getBlockCatalogEntry,
843
1053
  useCompletion,
844
1054
  useLessonkit,
845
1055
  useProgress,
package/dist/index.d.cts CHANGED
@@ -142,4 +142,42 @@ type ThemeContextValue = {
142
142
  declare function ThemeProvider(props: ThemeProviderProps): react_jsx_runtime.JSX.Element;
143
143
  declare function useTheme(): ThemeContextValue;
144
144
 
145
- export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
145
+ declare const blockCatalogVersion: 1;
146
+ type BlockPropSpec = {
147
+ name: string;
148
+ type: string;
149
+ required: boolean;
150
+ description: string;
151
+ };
152
+ type BlockCatalogEntry = {
153
+ type: string;
154
+ aliases?: string[];
155
+ category: "container" | "content" | "assessment" | "chrome";
156
+ description: string;
157
+ props: BlockPropSpec[];
158
+ requiredIds: string[];
159
+ optionalIds?: string[];
160
+ parentConstraints?: string[];
161
+ a11y: {
162
+ element: string;
163
+ ariaLabel: string;
164
+ keyboard: string;
165
+ liveRegions?: string;
166
+ notes: string;
167
+ };
168
+ theming: {
169
+ surface: "global-inherit";
170
+ dataAttributes?: string[];
171
+ stylingNotes: string;
172
+ };
173
+ telemetry: {
174
+ emits: string[];
175
+ requiresActiveLesson?: boolean;
176
+ manualTracking?: string;
177
+ };
178
+ };
179
+ declare const BLOCK_CATALOG: BlockCatalogEntry[];
180
+ declare function buildBlockCatalog(): BlockCatalogEntry[];
181
+ declare function getBlockCatalogEntry(type: string): BlockCatalogEntry | undefined;
182
+
183
+ export { BLOCK_CATALOG, type BlockCatalogEntry, type BlockPropSpec, Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, blockCatalogVersion, buildBlockCatalog, getBlockCatalogEntry, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
package/dist/index.d.ts CHANGED
@@ -142,4 +142,42 @@ type ThemeContextValue = {
142
142
  declare function ThemeProvider(props: ThemeProviderProps): react_jsx_runtime.JSX.Element;
143
143
  declare function useTheme(): ThemeContextValue;
144
144
 
145
- export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
145
+ declare const blockCatalogVersion: 1;
146
+ type BlockPropSpec = {
147
+ name: string;
148
+ type: string;
149
+ required: boolean;
150
+ description: string;
151
+ };
152
+ type BlockCatalogEntry = {
153
+ type: string;
154
+ aliases?: string[];
155
+ category: "container" | "content" | "assessment" | "chrome";
156
+ description: string;
157
+ props: BlockPropSpec[];
158
+ requiredIds: string[];
159
+ optionalIds?: string[];
160
+ parentConstraints?: string[];
161
+ a11y: {
162
+ element: string;
163
+ ariaLabel: string;
164
+ keyboard: string;
165
+ liveRegions?: string;
166
+ notes: string;
167
+ };
168
+ theming: {
169
+ surface: "global-inherit";
170
+ dataAttributes?: string[];
171
+ stylingNotes: string;
172
+ };
173
+ telemetry: {
174
+ emits: string[];
175
+ requiresActiveLesson?: boolean;
176
+ manualTracking?: string;
177
+ };
178
+ };
179
+ declare const BLOCK_CATALOG: BlockCatalogEntry[];
180
+ declare function buildBlockCatalog(): BlockCatalogEntry[];
181
+ declare function getBlockCatalogEntry(type: string): BlockCatalogEntry | undefined;
182
+
183
+ export { BLOCK_CATALOG, type BlockCatalogEntry, type BlockPropSpec, Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, blockCatalogVersion, buildBlockCatalog, getBlockCatalogEntry, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
package/dist/index.js CHANGED
@@ -211,6 +211,9 @@ function createProgressController() {
211
211
  completeLesson: (lessonId, completedAtMs) => {
212
212
  if (completedLessonIds.has(lessonId)) return { didComplete: false };
213
213
  completedLessonIds = new Set(completedLessonIds).add(lessonId);
214
+ if (activeLessonId === lessonId) {
215
+ activeLessonId = void 0;
216
+ }
214
217
  const startedAt = lessonStartTimes.get(lessonId);
215
218
  lessonStartTimes.delete(lessonId);
216
219
  const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
@@ -377,7 +380,9 @@ function LessonkitProvider(props) {
377
380
  );
378
381
  }
379
382
  return () => {
380
- disposeTrackingClient(prev);
383
+ if (prev !== trackingRef.current) {
384
+ disposeTrackingClient(prev);
385
+ }
381
386
  };
382
387
  }, [
383
388
  trackingEnabled,
@@ -414,9 +419,15 @@ function LessonkitProvider(props) {
414
419
  const prevCourseIdRef = useRef(config.courseId);
415
420
  useEffect(() => {
416
421
  if (prevCourseIdRef.current === config.courseId) return;
422
+ const previousActiveLesson = progressRef.current.getState().activeLessonId;
417
423
  prevCourseIdRef.current = config.courseId;
418
424
  progressRef.current = createProgressController();
419
425
  syncProgress();
426
+ if (previousActiveLesson) {
427
+ progressRef.current.setActiveLesson(previousActiveLesson, Date.now());
428
+ syncProgress();
429
+ track("lesson_started", { lessonId: previousActiveLesson }, { lessonId: previousActiveLesson });
430
+ }
420
431
  const sessionId = sessionIdRef.current;
421
432
  const cid = config.courseId;
422
433
  if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
@@ -434,13 +445,7 @@ function LessonkitProvider(props) {
434
445
  { lxpackBridge: lxpackBridgeModeRef.current }
435
446
  );
436
447
  }
437
- }, [config.courseId, syncProgress]);
438
- useEffect(() => {
439
- return () => {
440
- trackingRef.current?.flush?.();
441
- void xapiRef.current?.flush();
442
- };
443
- }, []);
448
+ }, [config.courseId, syncProgress, track]);
444
449
  const emitLessonCompleted = useCallback(
445
450
  (lessonId, durationMs) => {
446
451
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -456,9 +461,22 @@ function LessonkitProvider(props) {
456
461
  if (!result.didComplete) return;
457
462
  syncProgress();
458
463
  emitLessonCompleted(lessonId, result.durationMs);
464
+ void trackingRef.current?.flush?.();
459
465
  },
460
466
  [syncProgress, emitLessonCompleted]
461
467
  );
468
+ useEffect(() => {
469
+ return () => {
470
+ const client = trackingRef.current;
471
+ void xapiRef.current?.flush();
472
+ setTimeout(() => {
473
+ client?.flush?.();
474
+ setTimeout(() => {
475
+ client?.dispose?.();
476
+ }, 0);
477
+ }, 0);
478
+ };
479
+ }, []);
462
480
  const setActiveLesson = useCallback(
463
481
  (lessonId) => {
464
482
  const current = progressRef.current.getState();
@@ -581,24 +599,21 @@ function Course(props) {
581
599
  }
582
600
  function Lesson(props) {
583
601
  warnInvalidComponentId(props.lessonId, "lessonId");
584
- const { setActiveLesson } = useLessonkit();
602
+ const { setActiveLesson, config } = useLessonkit();
585
603
  const { completeLesson } = useCompletion();
586
604
  const id = props.lessonId;
587
- const pendingCompleteRef = useRef2(null);
605
+ const lessonMountGenerationRef = useRef2(0);
588
606
  useEffect2(() => {
589
- if (pendingCompleteRef.current !== null) {
590
- clearTimeout(pendingCompleteRef.current);
591
- pendingCompleteRef.current = null;
592
- }
607
+ const generation = ++lessonMountGenerationRef.current;
593
608
  setActiveLesson(id);
594
609
  return () => {
595
610
  const lessonId = id;
596
- pendingCompleteRef.current = setTimeout(() => {
597
- pendingCompleteRef.current = null;
611
+ queueMicrotask(() => {
612
+ if (lessonMountGenerationRef.current !== generation) return;
598
613
  completeLesson(lessonId);
599
- }, 0);
614
+ });
600
615
  };
601
- }, [id, setActiveLesson, completeLesson]);
616
+ }, [id, config.courseId, setActiveLesson, completeLesson]);
602
617
  return /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
603
618
  /* @__PURE__ */ jsx2("h2", { children: props.title }),
604
619
  /* @__PURE__ */ jsx2("div", { children: props.children })
@@ -805,7 +820,195 @@ function useTheme() {
805
820
  }
806
821
  return ctx;
807
822
  }
823
+
824
+ // src/blockCatalog.ts
825
+ var blockCatalogVersion = 1;
826
+ var BLOCK_CATALOG = [
827
+ {
828
+ type: "Course",
829
+ category: "container",
830
+ description: "Top-level course shell; wraps LessonkitProvider and emits course lifecycle telemetry.",
831
+ props: [
832
+ { name: "title", type: "string", required: true, description: "Course title shown in the h1." },
833
+ { name: "courseId", type: "CourseId", required: true, description: "Stable course identifier for telemetry and packaging." },
834
+ {
835
+ name: "config",
836
+ type: "Omit<LessonkitConfig, 'courseId'>",
837
+ required: false,
838
+ description: "Runtime config (tracking, xAPI, session, lxpack bridge). courseId is merged from props."
839
+ },
840
+ { name: "children", type: "ReactNode", required: true, description: "Lessons and course chrome." }
841
+ ],
842
+ requiredIds: ["courseId"],
843
+ a11y: {
844
+ element: "section",
845
+ ariaLabel: "title prop",
846
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
847
+ notes: "Renders h1 with course title. Wrap with ThemeProvider at app root for theming."
848
+ },
849
+ theming: {
850
+ surface: "global-inherit",
851
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider on document or scoped host."
852
+ },
853
+ telemetry: {
854
+ emits: ["course_started", "course_completed"]
855
+ }
856
+ },
857
+ {
858
+ type: "Lesson",
859
+ category: "container",
860
+ description: "Lesson container; sets active lesson on mount and completes on unmount.",
861
+ props: [
862
+ { name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
863
+ { name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
864
+ { name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
865
+ ],
866
+ requiredIds: ["lessonId"],
867
+ parentConstraints: ["Course"],
868
+ a11y: {
869
+ element: "article",
870
+ ariaLabel: "title prop",
871
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
872
+ notes: "Renders h2 with lesson title. Only one Lesson should be mounted as active at a time in typical SPA layouts."
873
+ },
874
+ theming: {
875
+ surface: "global-inherit",
876
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider."
877
+ },
878
+ telemetry: {
879
+ emits: ["lesson_started", "lesson_completed", "lesson_time_on_task"]
880
+ }
881
+ },
882
+ {
883
+ type: "Scenario",
884
+ category: "content",
885
+ description: "Scenario or narrative content region for branching stories and situational context.",
886
+ props: [
887
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
888
+ { name: "children", type: "ReactNode", required: true, description: "Scenario narrative and custom UI." }
889
+ ],
890
+ requiredIds: [],
891
+ optionalIds: ["blockId"],
892
+ parentConstraints: ["Lesson"],
893
+ a11y: {
894
+ element: "section",
895
+ ariaLabel: "Scenario",
896
+ keyboard: "No block-specific keyboard behavior; custom children may define their own.",
897
+ notes: "Use for situational framing. Pair with useTracking() for branching interactions."
898
+ },
899
+ theming: {
900
+ surface: "global-inherit",
901
+ dataAttributes: ["data-lk-block-id"],
902
+ stylingNotes: "Optional data-lk-block-id when blockId is set. Style via app CSS using --lk-* tokens."
903
+ },
904
+ telemetry: {
905
+ emits: [],
906
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload })"
907
+ }
908
+ },
909
+ {
910
+ type: "Reflection",
911
+ category: "content",
912
+ description: "Reflection prompt with a textarea for learner free-text responses.",
913
+ props: [
914
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
915
+ { name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
916
+ { name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
917
+ ],
918
+ requiredIds: [],
919
+ optionalIds: ["blockId"],
920
+ parentConstraints: ["Lesson"],
921
+ a11y: {
922
+ element: "section",
923
+ ariaLabel: "Reflection",
924
+ keyboard: "Textarea is keyboard-focusable; standard text entry.",
925
+ notes: "When prompt is set, textarea uses aria-labelledby; otherwise aria-label='Reflection response'."
926
+ },
927
+ theming: {
928
+ surface: "global-inherit",
929
+ dataAttributes: ["data-lk-block-id"],
930
+ stylingNotes: "Optional data-lk-block-id when blockId is set. Style textarea via app CSS."
931
+ },
932
+ telemetry: {
933
+ emits: [],
934
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
935
+ }
936
+ },
937
+ {
938
+ type: "Quiz",
939
+ aliases: ["KnowledgeCheck"],
940
+ category: "assessment",
941
+ description: "Single-question multiple-choice assessment with automatic answer and completion telemetry.",
942
+ props: [
943
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
944
+ { name: "question", type: "string", required: true, description: "Question text shown above choices." },
945
+ { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
946
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
947
+ ],
948
+ requiredIds: ["checkId"],
949
+ parentConstraints: ["Lesson"],
950
+ a11y: {
951
+ element: "section",
952
+ ariaLabel: "Quiz",
953
+ keyboard: "Radio group navigable with arrow keys; one choice per question.",
954
+ liveRegions: "role='status' aria-live='polite' for Correct / Try again feedback.",
955
+ notes: "Fieldset with visually hidden legend. KnowledgeCheck is an alias that renders Quiz with identical behavior."
956
+ },
957
+ theming: {
958
+ surface: "global-inherit",
959
+ dataAttributes: ["data-lk-check-id"],
960
+ stylingNotes: "data-lk-check-id set from checkId. Style labels and feedback via app CSS."
961
+ },
962
+ telemetry: {
963
+ emits: ["quiz_answered", "quiz_completed"],
964
+ requiresActiveLesson: true
965
+ }
966
+ },
967
+ {
968
+ type: "ProgressTracker",
969
+ category: "chrome",
970
+ description: "Displays count of completed lessons from runtime progress state.",
971
+ props: [],
972
+ requiredIds: [],
973
+ parentConstraints: ["Course"],
974
+ a11y: {
975
+ element: "aside",
976
+ ariaLabel: "Progress",
977
+ keyboard: "Presentational; no interactive elements.",
978
+ notes: "Shows 'Lessons completed: N' from progress.completedLessonIds."
979
+ },
980
+ theming: {
981
+ surface: "global-inherit",
982
+ stylingNotes: "Inherits --lk-* CSS variables; style via app CSS."
983
+ },
984
+ telemetry: {
985
+ emits: []
986
+ }
987
+ }
988
+ ];
989
+ function buildBlockCatalog() {
990
+ return BLOCK_CATALOG.map((entry) => ({
991
+ ...entry,
992
+ props: entry.props.map((p) => ({ ...p })),
993
+ aliases: entry.aliases ? [...entry.aliases] : void 0,
994
+ optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
995
+ parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
996
+ a11y: { ...entry.a11y },
997
+ theming: {
998
+ ...entry.theming,
999
+ dataAttributes: entry.theming.dataAttributes ? [...entry.theming.dataAttributes] : void 0
1000
+ },
1001
+ telemetry: {
1002
+ ...entry.telemetry,
1003
+ emits: [...entry.telemetry.emits]
1004
+ }
1005
+ }));
1006
+ }
1007
+ function getBlockCatalogEntry(type) {
1008
+ return BLOCK_CATALOG.find((entry) => entry.type === type || entry.aliases?.includes(type));
1009
+ }
808
1010
  export {
1011
+ BLOCK_CATALOG,
809
1012
  Course,
810
1013
  KnowledgeCheck,
811
1014
  Lesson,
@@ -815,6 +1018,9 @@ export {
815
1018
  Reflection,
816
1019
  Scenario,
817
1020
  ThemeProvider,
1021
+ blockCatalogVersion,
1022
+ buildBlockCatalog,
1023
+ getBlockCatalogEntry,
818
1024
  useCompletion,
819
1025
  useLessonkit,
820
1026
  useProgress,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -31,10 +31,14 @@
31
31
  "types": "./dist/index.d.ts",
32
32
  "import": "./dist/index.js",
33
33
  "require": "./dist/index.cjs"
34
- }
34
+ },
35
+ "./block-catalog.v1.json": "./block-catalog.v1.json",
36
+ "./block-contract.v1.json": "./block-contract.v1.json"
35
37
  },
36
38
  "files": [
37
- "dist"
39
+ "dist",
40
+ "block-catalog.v1.json",
41
+ "block-contract.v1.json"
38
42
  ],
39
43
  "scripts": {
40
44
  "build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
@@ -50,11 +54,11 @@
50
54
  "react-dom": ">=18"
51
55
  },
52
56
  "dependencies": {
53
- "@lessonkit/accessibility": "0.6.0",
54
- "@lessonkit/core": "0.6.0",
55
- "@lessonkit/lxpack": "0.6.0",
56
- "@lessonkit/themes": "0.6.0",
57
- "@lessonkit/xapi": "0.6.0"
57
+ "@lessonkit/accessibility": "0.8.0",
58
+ "@lessonkit/core": "0.8.0",
59
+ "@lessonkit/lxpack": "0.8.0",
60
+ "@lessonkit/themes": "0.8.0",
61
+ "@lessonkit/xapi": "0.8.0"
58
62
  },
59
63
  "devDependencies": {
60
64
  "@testing-library/react": "^16.3.0",