@lessonkit/react 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -12,8 +12,156 @@ import {
12
12
  useRef,
13
13
  useState
14
14
  } from "react";
15
- import { createTrackingClient, nowIso } from "@lessonkit/core";
15
+ import { createTrackingClient } from "@lessonkit/core";
16
16
  import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
17
+ import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
18
+
19
+ // src/runtime/emitTelemetry.ts
20
+ import { nowIso } from "@lessonkit/core";
21
+ import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
22
+
23
+ // src/runtime/lxpackBridge.ts
24
+ import {
25
+ normalizeAssessmentPassingScore,
26
+ normalizeAssessmentScore
27
+ } from "@lessonkit/lxpack/bridge";
28
+ function getBridge() {
29
+ if (typeof window === "undefined") return null;
30
+ const parent = window.parent;
31
+ if (!parent || parent === window) return null;
32
+ return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
33
+ }
34
+ function forwardTelemetryToLxpack(event, mode = "auto") {
35
+ if (mode === "off") return;
36
+ const bridge = getBridge();
37
+ if (!bridge) return;
38
+ switch (event.name) {
39
+ case "lesson_completed": {
40
+ const lessonId = event.lessonId;
41
+ if (lessonId) bridge.completeLesson?.(lessonId);
42
+ return;
43
+ }
44
+ case "course_completed":
45
+ bridge.completeCourse?.();
46
+ return;
47
+ case "quiz_completed": {
48
+ const data = event.data;
49
+ if (!data?.checkId) return;
50
+ const scaled = normalizeAssessmentScore({
51
+ score: data.score,
52
+ maxScore: data.maxScore
53
+ });
54
+ if (scaled === null) return;
55
+ bridge.submitAssessment?.({
56
+ id: data.checkId,
57
+ score: scaled,
58
+ passingScore: normalizeAssessmentPassingScore(data.passingScore)
59
+ });
60
+ return;
61
+ }
62
+ default:
63
+ return;
64
+ }
65
+ }
66
+
67
+ // src/runtime/emitTelemetry.ts
68
+ var warnedMissingCourseId = false;
69
+ var warnedMissingQuizLesson = false;
70
+ function isDevEnvironment() {
71
+ const g = globalThis;
72
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
73
+ }
74
+ function emitTelemetry(tracking, xapi, event, opts) {
75
+ if (!event.courseId) {
76
+ if (isDevEnvironment() && !warnedMissingCourseId) {
77
+ warnedMissingCourseId = true;
78
+ console.warn("[lessonkit] telemetry event missing courseId");
79
+ }
80
+ return;
81
+ }
82
+ tracking.track(event);
83
+ try {
84
+ const statement = telemetryEventToXAPIStatement(event);
85
+ if (statement) xapi?.send(statement);
86
+ } catch (err) {
87
+ if (isDevEnvironment()) {
88
+ console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
89
+ }
90
+ }
91
+ forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
92
+ }
93
+ function buildTrackEvent(opts) {
94
+ const base = {
95
+ timestamp: nowIso(),
96
+ courseId: opts.courseId,
97
+ sessionId: opts.sessionId,
98
+ attemptId: opts.attemptId,
99
+ user: opts.user
100
+ };
101
+ switch (opts.name) {
102
+ case "course_started":
103
+ return { name: "course_started", ...base };
104
+ case "course_completed":
105
+ return { name: "course_completed", ...base };
106
+ case "lesson_started": {
107
+ const data = opts.data;
108
+ const lessonId = opts.lessonId ?? data?.lessonId;
109
+ if (!lessonId) throw new Error("lesson_started requires lessonId");
110
+ return {
111
+ name: "lesson_started",
112
+ ...base,
113
+ lessonId,
114
+ data: { ...data, lessonId }
115
+ };
116
+ }
117
+ case "lesson_completed":
118
+ case "lesson_time_on_task": {
119
+ const data = opts.data;
120
+ const lessonId = opts.lessonId ?? data?.lessonId;
121
+ if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
122
+ return {
123
+ name: opts.name,
124
+ ...base,
125
+ lessonId,
126
+ data: { ...data, lessonId }
127
+ };
128
+ }
129
+ case "quiz_answered": {
130
+ const data = opts.data;
131
+ const lessonId = opts.lessonId;
132
+ if (!lessonId) throw new Error("quiz_answered requires active lessonId");
133
+ return { name: "quiz_answered", ...base, lessonId, data };
134
+ }
135
+ case "quiz_completed": {
136
+ const data = opts.data;
137
+ const lessonId = opts.lessonId;
138
+ if (!lessonId) throw new Error("quiz_completed requires active lessonId");
139
+ return { name: "quiz_completed", ...base, lessonId, data };
140
+ }
141
+ case "interaction":
142
+ return {
143
+ name: "interaction",
144
+ ...base,
145
+ lessonId: opts.lessonId,
146
+ data: opts.data
147
+ };
148
+ default:
149
+ return { name: opts.name, ...base };
150
+ }
151
+ }
152
+ function tryBuildTrackEvent(opts) {
153
+ const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
154
+ if (isQuiz && !opts.lessonId) {
155
+ if (isDevEnvironment() && !warnedMissingQuizLesson) {
156
+ warnedMissingQuizLesson = true;
157
+ console.warn(
158
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
159
+ );
160
+ }
161
+ return null;
162
+ }
163
+ return buildTrackEvent(opts);
164
+ }
17
165
 
18
166
  // src/runtime/ports.ts
19
167
  function createNoopStorage() {
@@ -42,13 +190,51 @@ function createSessionStoragePort() {
42
190
  };
43
191
  }
44
192
 
193
+ // src/runtime/progress.ts
194
+ function createProgressController() {
195
+ let activeLessonId;
196
+ let completedLessonIds = /* @__PURE__ */ new Set();
197
+ let courseCompleted = false;
198
+ const lessonStartTimes = /* @__PURE__ */ new Map();
199
+ return {
200
+ getState: () => ({
201
+ activeLessonId,
202
+ completedLessonIds: new Set(completedLessonIds),
203
+ courseCompleted
204
+ }),
205
+ setActiveLesson: (lessonId, startedAtMs) => {
206
+ const previousLessonId = activeLessonId;
207
+ activeLessonId = lessonId;
208
+ lessonStartTimes.set(lessonId, startedAtMs);
209
+ return { previousLessonId };
210
+ },
211
+ completeLesson: (lessonId, completedAtMs) => {
212
+ if (completedLessonIds.has(lessonId)) return { didComplete: false };
213
+ completedLessonIds = new Set(completedLessonIds).add(lessonId);
214
+ const startedAt = lessonStartTimes.get(lessonId);
215
+ lessonStartTimes.delete(lessonId);
216
+ const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
217
+ return { durationMs, didComplete: true };
218
+ },
219
+ completeCourse: () => {
220
+ if (courseCompleted) return { didComplete: false };
221
+ courseCompleted = true;
222
+ return { didComplete: true };
223
+ }
224
+ };
225
+ }
226
+
45
227
  // src/runtime/xapi.ts
46
228
  import { createXAPIClient } from "@lessonkit/xapi";
47
229
  function createXapiClientFromConfig(config, queue) {
48
230
  if (config.xapi?.enabled === false) return null;
49
231
  if (config.xapi?.client) return config.xapi.client;
50
- const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
51
- return createXAPIClient({ baseId, transport: config.xapi?.transport, queue });
232
+ if (!config.courseId) return null;
233
+ return createXAPIClient({
234
+ courseId: config.courseId,
235
+ transport: config.xapi?.transport,
236
+ queue
237
+ });
52
238
  }
53
239
 
54
240
  // src/runtime/session.ts
@@ -95,7 +281,7 @@ function createTrackingClientFromConfig(config) {
95
281
  });
96
282
  }
97
283
  function LessonkitProvider(props) {
98
- const config = props.config ?? {};
284
+ const config = props.config;
99
285
  const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
100
286
  if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
101
287
  const attemptIdRef = useRef(config.session?.attemptId);
@@ -104,49 +290,15 @@ function LessonkitProvider(props) {
104
290
  userRef.current = config.session?.user;
105
291
  const courseIdRef = useRef(config.courseId);
106
292
  courseIdRef.current = config.courseId;
107
- const trackingRef = useRef(createTrackingClient());
108
- const [tracking, setTracking] = useState(() => trackingRef.current);
109
- const courseStartedInProviderRef = useRef(false);
110
- const trackingEnabled = config.tracking?.enabled;
111
- const trackingSink = config.tracking?.sink;
112
- const trackingBatchSink = config.tracking?.batchSink;
113
- const batchEnabled = config.tracking?.batch?.enabled;
114
- const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
115
- const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
116
- useIsoLayoutEffect(() => {
117
- const prev = trackingRef.current;
118
- const next = createTrackingClientFromConfig(config);
119
- trackingRef.current = next;
120
- setTracking(next);
121
- const sessionId = sessionIdRef.current;
122
- const cid = courseIdRef.current;
123
- const shouldEmitCourseStarted = cid ? !hasCourseStarted(defaultStorage, sessionId, cid) : !courseStartedInProviderRef.current;
124
- if (shouldEmitCourseStarted) {
125
- if (cid) {
126
- markCourseStarted(defaultStorage, sessionId, cid);
127
- } else {
128
- courseStartedInProviderRef.current = true;
129
- }
130
- next.track({
131
- name: "course_started",
132
- timestamp: nowIso(),
133
- courseId: cid,
134
- sessionId,
135
- attemptId: attemptIdRef.current,
136
- user: userRef.current
137
- });
138
- }
139
- return () => {
140
- disposeTrackingClient(prev);
141
- };
142
- }, [
143
- trackingEnabled,
144
- trackingSink,
145
- trackingBatchSink,
146
- batchEnabled,
147
- batchFlushIntervalMs,
148
- batchMaxBatchSize
149
- ]);
293
+ const lxpackBridgeModeRef = useRef(config.lxpack?.bridge ?? "auto");
294
+ lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
295
+ const progressRef = useRef(createProgressController());
296
+ const [progress, setProgress] = useState(() => progressRef.current.getState());
297
+ const syncProgress = useCallback(() => {
298
+ setProgress(progressRef.current.getState());
299
+ }, []);
300
+ const activeLessonIdRef = useRef(progress.activeLessonId);
301
+ activeLessonIdRef.current = progress.activeLessonId;
150
302
  const xapiQueueRef = useRef(createInMemoryXAPIQueue());
151
303
  const xapiRef = useRef(null);
152
304
  const [xapi, setXapi] = useState(null);
@@ -159,6 +311,25 @@ function LessonkitProvider(props) {
159
311
  const next = createXapiClientFromConfig(config, xapiQueueRef.current);
160
312
  xapiRef.current = next;
161
313
  setXapi(next);
314
+ if (next && !prev) {
315
+ const sessionId = sessionIdRef.current;
316
+ const cid = courseIdRef.current;
317
+ if (hasCourseStarted(defaultStorage, sessionId, cid)) {
318
+ try {
319
+ const statement = telemetryEventToXAPIStatement2(
320
+ buildTrackEvent({
321
+ name: "course_started",
322
+ courseId: cid,
323
+ sessionId,
324
+ attemptId: attemptIdRef.current,
325
+ user: userRef.current
326
+ })
327
+ );
328
+ if (statement) next.send(statement);
329
+ } catch {
330
+ }
331
+ }
332
+ }
162
333
  void (async () => {
163
334
  if (prev) {
164
335
  try {
@@ -175,21 +346,59 @@ function LessonkitProvider(props) {
175
346
  void prev?.flush();
176
347
  };
177
348
  }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
178
- const [completedLessonIds, setCompletedLessonIds] = useState(() => /* @__PURE__ */ new Set());
179
- const completedLessonIdsRef = useRef(completedLessonIds);
180
- completedLessonIdsRef.current = completedLessonIds;
181
- const [activeLessonId, setActiveLessonId] = useState(void 0);
182
- const [courseCompleted, setCourseCompleted] = useState(false);
183
- const courseCompletedRef = useRef(false);
184
- courseCompletedRef.current = courseCompleted;
185
- const activeLessonIdRef = useRef(void 0);
186
- activeLessonIdRef.current = activeLessonId;
187
- const lessonStartTimesRef = useRef(/* @__PURE__ */ new Map());
349
+ const trackingRef = useRef(createTrackingClient());
350
+ const [tracking, setTracking] = useState(() => trackingRef.current);
351
+ const trackingEnabled = config.tracking?.enabled;
352
+ const trackingSink = config.tracking?.sink;
353
+ const trackingBatchSink = config.tracking?.batchSink;
354
+ const batchEnabled = config.tracking?.batch?.enabled;
355
+ const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
356
+ const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
357
+ useIsoLayoutEffect(() => {
358
+ const prev = trackingRef.current;
359
+ const next = createTrackingClientFromConfig(config);
360
+ trackingRef.current = next;
361
+ setTracking(next);
362
+ const sessionId = sessionIdRef.current;
363
+ const cid = courseIdRef.current;
364
+ if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
365
+ markCourseStarted(defaultStorage, sessionId, cid);
366
+ emitTelemetry(
367
+ next,
368
+ xapiRef.current,
369
+ buildTrackEvent({
370
+ name: "course_started",
371
+ courseId: cid,
372
+ sessionId,
373
+ attemptId: attemptIdRef.current,
374
+ user: userRef.current
375
+ }),
376
+ { lxpackBridge: lxpackBridgeModeRef.current }
377
+ );
378
+ }
379
+ return () => {
380
+ disposeTrackingClient(prev);
381
+ };
382
+ }, [
383
+ trackingEnabled,
384
+ trackingSink,
385
+ trackingBatchSink,
386
+ batchEnabled,
387
+ batchFlushIntervalMs,
388
+ batchMaxBatchSize
389
+ ]);
390
+ const emitWithBridge = useCallback(
391
+ (trackingClient, event) => {
392
+ emitTelemetry(trackingClient, xapiRef.current, event, {
393
+ lxpackBridge: lxpackBridgeModeRef.current
394
+ });
395
+ },
396
+ []
397
+ );
188
398
  const track = useCallback(
189
399
  (name, data, opts) => {
190
- trackingRef.current?.track({
400
+ const event = tryBuildTrackEvent({
191
401
  name,
192
- timestamp: nowIso(),
193
402
  courseId: courseIdRef.current,
194
403
  lessonId: opts?.lessonId ?? activeLessonIdRef.current,
195
404
  sessionId: sessionIdRef.current,
@@ -197,54 +406,85 @@ function LessonkitProvider(props) {
197
406
  user: userRef.current,
198
407
  data
199
408
  });
409
+ if (!event) return;
410
+ emitWithBridge(trackingRef.current, event);
200
411
  },
201
- []
412
+ [emitWithBridge]
202
413
  );
414
+ const prevCourseIdRef = useRef(config.courseId);
415
+ useEffect(() => {
416
+ if (prevCourseIdRef.current === config.courseId) return;
417
+ prevCourseIdRef.current = config.courseId;
418
+ progressRef.current = createProgressController();
419
+ syncProgress();
420
+ const sessionId = sessionIdRef.current;
421
+ const cid = config.courseId;
422
+ if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
423
+ markCourseStarted(defaultStorage, sessionId, cid);
424
+ emitTelemetry(
425
+ trackingRef.current,
426
+ xapiRef.current,
427
+ buildTrackEvent({
428
+ name: "course_started",
429
+ courseId: cid,
430
+ sessionId,
431
+ attemptId: attemptIdRef.current,
432
+ user: userRef.current
433
+ }),
434
+ { lxpackBridge: lxpackBridgeModeRef.current }
435
+ );
436
+ }
437
+ }, [config.courseId, syncProgress]);
203
438
  useEffect(() => {
204
439
  return () => {
205
440
  trackingRef.current?.flush?.();
206
441
  void xapiRef.current?.flush();
207
442
  };
208
443
  }, []);
209
- const setActiveLesson = useCallback((lessonId) => {
210
- if (activeLessonIdRef.current === lessonId) return;
211
- activeLessonIdRef.current = lessonId;
212
- setActiveLessonId(lessonId);
213
- lessonStartTimesRef.current.set(lessonId, Date.now());
214
- track("lesson_started", { lessonId }, { lessonId });
215
- xapiRef.current?.startedLesson({ lessonId });
216
- }, [track]);
217
- const completeLesson = useCallback(
218
- (lessonId) => {
219
- if (completedLessonIdsRef.current.has(lessonId)) return;
220
- completedLessonIdsRef.current = new Set(completedLessonIdsRef.current).add(lessonId);
221
- setCompletedLessonIds(completedLessonIdsRef.current);
222
- const startedAt = lessonStartTimesRef.current.get(lessonId);
223
- lessonStartTimesRef.current.delete(lessonId);
224
- const durationMs = typeof startedAt === "number" ? Math.max(0, Date.now() - startedAt) : void 0;
444
+ const emitLessonCompleted = useCallback(
445
+ (lessonId, durationMs) => {
225
446
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
226
447
  if (durationMs !== void 0) {
227
448
  track("lesson_time_on_task", { lessonId, durationMs }, { lessonId });
228
449
  }
229
- xapiRef.current?.completeLesson({ lessonId, durationMs });
230
450
  },
231
451
  [track]
232
452
  );
453
+ const completeLesson = useCallback(
454
+ (lessonId) => {
455
+ const result = progressRef.current.completeLesson(lessonId, Date.now());
456
+ if (!result.didComplete) return;
457
+ syncProgress();
458
+ emitLessonCompleted(lessonId, result.durationMs);
459
+ },
460
+ [syncProgress, emitLessonCompleted]
461
+ );
462
+ const setActiveLesson = useCallback(
463
+ (lessonId) => {
464
+ const current = progressRef.current.getState();
465
+ if (current.activeLessonId === lessonId) return;
466
+ const previous = current.activeLessonId;
467
+ if (previous && previous !== lessonId) {
468
+ const completed = progressRef.current.completeLesson(previous, Date.now());
469
+ if (completed.didComplete) {
470
+ emitLessonCompleted(previous, completed.durationMs);
471
+ }
472
+ }
473
+ progressRef.current.setActiveLesson(lessonId, Date.now());
474
+ syncProgress();
475
+ track("lesson_started", { lessonId }, { lessonId });
476
+ },
477
+ [track, syncProgress, emitLessonCompleted]
478
+ );
233
479
  const completeCourse = useCallback(() => {
234
- if (courseCompletedRef.current) return;
235
- courseCompletedRef.current = true;
236
- setCourseCompleted(true);
480
+ const result = progressRef.current.completeCourse();
481
+ if (!result.didComplete) return;
482
+ syncProgress();
237
483
  track("course_completed");
238
- xapiRef.current?.completeCourse();
239
- }, [track]);
240
- const progress = useMemo(
241
- () => ({
242
- activeLessonId,
243
- completedLessonIds: new Set(completedLessonIds),
244
- courseCompleted
245
- }),
246
- [activeLessonId, completedLessonIds, courseCompleted]
247
- );
484
+ }, [track, syncProgress]);
485
+ const sessionUser = config.session?.user;
486
+ const sessionAttemptId = config.session?.attemptId;
487
+ const sessionConfiguredId = config.session?.sessionId;
248
488
  const runtime = useMemo(
249
489
  () => ({
250
490
  config,
@@ -257,7 +497,19 @@ function LessonkitProvider(props) {
257
497
  completeCourse,
258
498
  track
259
499
  }),
260
- [config, tracking, xapi, progress, setActiveLesson, completeLesson, completeCourse, track]
500
+ [
501
+ config,
502
+ tracking,
503
+ xapi,
504
+ progress,
505
+ setActiveLesson,
506
+ completeLesson,
507
+ completeCourse,
508
+ track,
509
+ sessionUser,
510
+ sessionAttemptId,
511
+ sessionConfiguredId
512
+ ]
261
513
  );
262
514
  return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
263
515
  }
@@ -296,9 +548,28 @@ function useQuizState() {
296
548
  );
297
549
  }
298
550
 
551
+ // src/runtime/validateComponentId.ts
552
+ import { validateId } from "@lessonkit/core";
553
+ var warnedPaths = /* @__PURE__ */ new Set();
554
+ function isDevEnvironment2() {
555
+ const g = globalThis;
556
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
557
+ }
558
+ function warnInvalidComponentId(id, path) {
559
+ if (!isDevEnvironment2()) return;
560
+ const key = `${path}:${String(id)}`;
561
+ if (warnedPaths.has(key)) return;
562
+ const result = validateId(id, path);
563
+ if (result.ok) return;
564
+ warnedPaths.add(key);
565
+ const detail = result.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
566
+ console.warn(`[lessonkit] invalid ${path} \u2014 ${detail}`);
567
+ }
568
+
299
569
  // src/components.tsx
300
570
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
301
571
  function Course(props) {
572
+ warnInvalidComponentId(props.courseId, "courseId");
302
573
  const providerConfig = useMemo3(
303
574
  () => ({ ...props.config, courseId: props.courseId }),
304
575
  [props.config, props.courseId]
@@ -309,15 +580,23 @@ function Course(props) {
309
580
  ] }) });
310
581
  }
311
582
  function Lesson(props) {
583
+ warnInvalidComponentId(props.lessonId, "lessonId");
312
584
  const { setActiveLesson } = useLessonkit();
313
585
  const { completeLesson } = useCompletion();
314
- const reactId = useId();
315
- const generatedId = useMemo3(() => `lesson-${sanitizeLessonId(reactId)}`, [reactId]);
316
- const id = props.lessonId ?? generatedId;
586
+ const id = props.lessonId;
587
+ const pendingCompleteRef = useRef2(null);
317
588
  useEffect2(() => {
589
+ if (pendingCompleteRef.current !== null) {
590
+ clearTimeout(pendingCompleteRef.current);
591
+ pendingCompleteRef.current = null;
592
+ }
318
593
  setActiveLesson(id);
319
594
  return () => {
320
- completeLesson(id);
595
+ const lessonId = id;
596
+ pendingCompleteRef.current = setTimeout(() => {
597
+ pendingCompleteRef.current = null;
598
+ completeLesson(lessonId);
599
+ }, 0);
321
600
  };
322
601
  }, [id, setActiveLesson, completeLesson]);
323
602
  return /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
@@ -326,11 +605,13 @@ function Lesson(props) {
326
605
  ] });
327
606
  }
328
607
  function Scenario(props) {
329
- return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", children: props.children });
608
+ if (props.blockId !== void 0) warnInvalidComponentId(props.blockId, "blockId");
609
+ return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": props.blockId, children: props.children });
330
610
  }
331
611
  function Reflection(props) {
612
+ if (props.blockId !== void 0) warnInvalidComponentId(props.blockId, "blockId");
332
613
  const promptId = useId();
333
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", children: [
614
+ return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": props.blockId, children: [
334
615
  props.prompt ? /* @__PURE__ */ jsx2("p", { id: promptId, children: props.prompt }) : null,
335
616
  props.children,
336
617
  /* @__PURE__ */ jsx2(
@@ -343,14 +624,23 @@ function Reflection(props) {
343
624
  ] });
344
625
  }
345
626
  function KnowledgeCheck(props) {
346
- return /* @__PURE__ */ jsx2(Quiz, { question: props.question, choices: props.choices, answer: props.answer });
627
+ return /* @__PURE__ */ jsx2(
628
+ Quiz,
629
+ {
630
+ checkId: props.checkId,
631
+ question: props.question,
632
+ choices: props.choices,
633
+ answer: props.answer
634
+ }
635
+ );
347
636
  }
348
637
  function Quiz(props) {
638
+ warnInvalidComponentId(props.checkId, "checkId");
349
639
  const quiz = useQuizState();
350
640
  const [selected, setSelected] = useState2(null);
351
641
  const completedRef = useRef2(false);
352
642
  const questionId = useId();
353
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", children: [
643
+ return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
354
644
  /* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
355
645
  /* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
356
646
  /* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
@@ -365,10 +655,15 @@ function Quiz(props) {
365
655
  onChange: () => {
366
656
  setSelected(c);
367
657
  const correct = c === props.answer;
368
- quiz.answer({ question: props.question, choice: c, correct });
658
+ quiz.answer({
659
+ checkId: props.checkId,
660
+ question: props.question,
661
+ choice: c,
662
+ correct
663
+ });
369
664
  if (correct && !completedRef.current) {
370
665
  completedRef.current = true;
371
- quiz.complete({ score: 1, maxScore: 1 });
666
+ quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1, passingScore: 1 });
372
667
  }
373
668
  }
374
669
  }
@@ -387,10 +682,6 @@ function ProgressTracker() {
387
682
  completed
388
683
  ] }) });
389
684
  }
390
- function sanitizeLessonId(id) {
391
- const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
392
- return s.length ? s : "id";
393
- }
394
685
 
395
686
  // src/theme/ThemeProvider.tsx
396
687
  import React3, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -37,8 +37,8 @@
37
37
  "dist"
38
38
  ],
39
39
  "scripts": {
40
- "build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
41
- "dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
40
+ "build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
41
+ "dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
42
42
  "prepublishOnly": "npm run build",
43
43
  "typecheck": "tsc -p tsconfig.json",
44
44
  "test": "vitest run --passWithNoTests",
@@ -50,10 +50,11 @@
50
50
  "react-dom": ">=18"
51
51
  },
52
52
  "dependencies": {
53
- "@lessonkit/accessibility": "0.4.0",
54
- "@lessonkit/core": "0.4.0",
55
- "@lessonkit/themes": "0.4.0",
56
- "@lessonkit/xapi": "0.4.0"
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
58
  },
58
59
  "devDependencies": {
59
60
  "@testing-library/react": "^16.3.0",