@lessonkit/react 1.0.0 → 1.0.2

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.
@@ -247,6 +247,12 @@
247
247
  "type": "string",
248
248
  "required": true,
249
249
  "description": "Correct choice value (must match one choice)."
250
+ },
251
+ {
252
+ "name": "passingScore",
253
+ "type": "number",
254
+ "required": false,
255
+ "description": "Minimum score required to pass (defaults to maxScore when omitted)."
250
256
  }
251
257
  ],
252
258
  "requiredIds": [
package/dist/index.cjs CHANGED
@@ -70,8 +70,8 @@ var import_react2 = require("react");
70
70
  // src/provider/useLessonkitProviderRuntime.ts
71
71
  var import_react = require("react");
72
72
  var import_core8 = require("@lessonkit/core");
73
- var import_xapi3 = require("@lessonkit/xapi");
74
73
  var import_xapi4 = require("@lessonkit/xapi");
74
+ var import_xapi5 = require("@lessonkit/xapi");
75
75
 
76
76
  // src/runtime/emitTelemetry.ts
77
77
  var import_core2 = require("@lessonkit/core");
@@ -169,6 +169,60 @@ function createXapiClientFromConfig(config, queue) {
169
169
  // src/runtime/session.ts
170
170
  var import_core5 = require("@lessonkit/core");
171
171
 
172
+ // src/runtime/courseStartedPipeline.ts
173
+ var import_xapi3 = require("@lessonkit/xapi");
174
+ function isDevEnvironment3() {
175
+ const g = globalThis;
176
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
177
+ }
178
+ function warnExtraSinkFailure(sinkId, err) {
179
+ if (isDevEnvironment3()) {
180
+ console.warn(
181
+ `[lessonkit] course_started extra sink "${sinkId}" failed:`,
182
+ err instanceof Error ? err.message : err
183
+ );
184
+ }
185
+ }
186
+ async function emitExtraSinks(sinks, event, emitCtx) {
187
+ await Promise.all(
188
+ sinks.map(async (sink) => {
189
+ let result;
190
+ try {
191
+ result = sink.emit(event, emitCtx);
192
+ } catch (err) {
193
+ warnExtraSinkFailure(sink.id, err);
194
+ throw err;
195
+ }
196
+ if (result != null && typeof result.then === "function") {
197
+ try {
198
+ await result;
199
+ } catch (err) {
200
+ warnExtraSinkFailure(sink.id, err);
201
+ throw err;
202
+ }
203
+ }
204
+ })
205
+ );
206
+ }
207
+ async function emitCourseStartedNonTrackingPipeline(opts) {
208
+ let xapiStatementSent = false;
209
+ if (!opts.skipXapi && opts.xapi) {
210
+ const statement = (0, import_xapi3.telemetryEventToXAPIStatement)(opts.event);
211
+ if (statement) {
212
+ opts.xapi.send(statement);
213
+ xapiStatementSent = true;
214
+ }
215
+ }
216
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
217
+ const emitCtx = {
218
+ courseId: opts.event.courseId,
219
+ sessionId: opts.event.sessionId,
220
+ attemptId: opts.event.attemptId
221
+ };
222
+ await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
223
+ return { xapiStatementSent };
224
+ }
225
+
172
226
  // src/runtime/plugins.ts
173
227
  var import_core6 = require("@lessonkit/core");
174
228
  function createReactPluginHost(plugins) {
@@ -217,11 +271,13 @@ async function disposeTrackingClient(client) {
217
271
  // src/provider/useLessonkitProviderRuntime.ts
218
272
  var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
219
273
  var defaultStorage = (0, import_core3.createSessionStoragePort)();
274
+ var courseStartedTrackingFlightKey = null;
220
275
  function isTrackingActive(tracking) {
221
276
  return tracking?.enabled !== false;
222
277
  }
223
- var noopTrackingClient = { track: () => {
224
- } };
278
+ function isCourseStartedSinkSettled(result) {
279
+ return result === "emitted";
280
+ }
225
281
  function buildCourseStartedEvent(opts) {
226
282
  const pluginCtx = buildPluginContext({
227
283
  courseId: opts.courseId,
@@ -238,85 +294,113 @@ function buildCourseStartedEvent(opts) {
238
294
  });
239
295
  return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
240
296
  }
241
- function emitCourseStartedPipelineOnly(opts) {
242
- const pluginCtx = buildPluginContext({
243
- courseId: opts.courseId,
244
- sessionId: opts.sessionId,
245
- attemptId: opts.attemptId,
246
- user: opts.user
247
- });
297
+ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
298
+ const flightKey = `${sessionId}:${courseId}`;
299
+ if ((0, import_core5.hasCourseStartedEmittedToTracking)(storage, sessionId, courseId)) {
300
+ return true;
301
+ }
302
+ if (courseStartedTrackingFlightKey === flightKey) {
303
+ return false;
304
+ }
305
+ courseStartedTrackingFlightKey = flightKey;
248
306
  try {
249
- emitTelemetryWithPlugins({
250
- pluginHost: null,
251
- tracking: noopTrackingClient,
252
- xapi: opts.xapi,
307
+ if (shouldCommit && !shouldCommit()) return false;
308
+ tracking.track(event);
309
+ await tracking.flush?.();
310
+ if (shouldCommit && !shouldCommit()) return false;
311
+ (0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId);
312
+ return true;
313
+ } catch {
314
+ return false;
315
+ } finally {
316
+ if (courseStartedTrackingFlightKey === flightKey) {
317
+ courseStartedTrackingFlightKey = null;
318
+ }
319
+ }
320
+ }
321
+ async function emitCourseStartedPipelineOnly(opts) {
322
+ try {
323
+ if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
324
+ const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
253
325
  event: opts.event,
254
- pluginCtx,
326
+ xapi: opts.xapi,
255
327
  lxpackBridge: opts.lxpackBridge,
256
- extraSinks: opts.extraSinks
328
+ extraSinks: opts.extraSinks,
329
+ skipXapi: opts.skipXapi
257
330
  });
331
+ if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
258
332
  (0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
259
- return true;
333
+ (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
334
+ if (xapiStatementSent) {
335
+ opts.onXapiStatementSent?.();
336
+ }
337
+ return "emitted";
260
338
  } catch {
261
- return false;
339
+ return "failed";
262
340
  }
263
341
  }
264
- function emitCourseStarted(opts) {
342
+ async function emitCourseStarted(opts) {
265
343
  const event = buildCourseStartedEvent(opts);
266
- if (event === null) return true;
344
+ if (event === null) return "filtered";
267
345
  const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
268
346
  opts.storage,
269
347
  opts.sessionId,
270
348
  opts.courseId
271
349
  );
272
350
  if (!trackingAlreadyEmitted) {
273
- try {
274
- opts.tracking.track(event);
275
- (0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
276
- } catch {
277
- return false;
278
- }
351
+ const tracked = await emitCourseStartedToTracking(
352
+ opts.tracking,
353
+ opts.storage,
354
+ opts.sessionId,
355
+ opts.courseId,
356
+ event,
357
+ opts.shouldCommit
358
+ );
359
+ if (!tracked) return "failed";
279
360
  }
280
- return emitCourseStartedPipelineOnly({ ...opts, event });
361
+ return emitCourseStartedPipelineOnly({
362
+ ...opts,
363
+ event,
364
+ skipXapi: opts.skipXapi,
365
+ onXapiStatementSent: opts.onXapiStatementSent,
366
+ shouldCommit: opts.shouldCommit
367
+ });
281
368
  }
282
- function emitCourseStartedToTrackingOnly(opts) {
369
+ async function emitCourseStartedToTrackingOnly(opts) {
283
370
  const event = buildCourseStartedEvent(opts);
284
- if (event === null) return true;
371
+ if (event === null) return "filtered";
285
372
  const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
286
373
  opts.storage,
287
374
  opts.sessionId,
288
375
  opts.courseId
289
376
  );
290
377
  if (!trackingAlreadyEmitted) {
291
- try {
292
- opts.tracking.track(event);
293
- (0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
294
- } catch {
295
- return false;
296
- }
378
+ const tracked = await emitCourseStartedToTracking(
379
+ opts.tracking,
380
+ opts.storage,
381
+ opts.sessionId,
382
+ opts.courseId,
383
+ event,
384
+ opts.shouldCommit
385
+ );
386
+ if (!tracked) return "failed";
297
387
  }
298
- const pluginCtx = buildPluginContext({
299
- courseId: opts.courseId,
300
- sessionId: opts.sessionId,
301
- attemptId: opts.attemptId,
302
- user: opts.user
303
- });
304
388
  try {
305
- emitTelemetryWithPlugins({
306
- pluginHost: null,
307
- tracking: noopTrackingClient,
308
- xapi: null,
389
+ if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
390
+ await emitCourseStartedNonTrackingPipeline({
309
391
  event,
310
- pluginCtx,
392
+ xapi: null,
311
393
  lxpackBridge: opts.lxpackBridge,
312
- extraSinks: opts.extraSinks
394
+ extraSinks: opts.extraSinks,
395
+ skipXapi: true
313
396
  });
314
- return true;
397
+ (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
398
+ return "emitted";
315
399
  } catch {
316
- return false;
400
+ return "failed";
317
401
  }
318
402
  }
319
- function emitPendingCourseStarted(opts) {
403
+ async function emitPendingCourseStarted(opts) {
320
404
  const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
321
405
  opts.storage,
322
406
  opts.sessionId,
@@ -328,13 +412,28 @@ function emitPendingCourseStarted(opts) {
328
412
  }
329
413
  if (trackingEmitted && !sessionStarted) {
330
414
  const event = buildCourseStartedEvent(opts);
331
- if (event === null) return true;
415
+ if (event === null) return "filtered";
332
416
  return emitCourseStartedPipelineOnly({ ...opts, event });
333
417
  }
334
418
  if (!trackingEmitted && !sessionStarted) {
335
419
  return emitCourseStarted(opts);
336
420
  }
337
- return true;
421
+ const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
422
+ opts.storage,
423
+ opts.sessionId,
424
+ opts.courseId
425
+ );
426
+ if (sessionStarted && trackingEmitted && !pipelineDelivered) {
427
+ const event = buildCourseStartedEvent(opts);
428
+ if (event === null) return "filtered";
429
+ return emitCourseStartedPipelineOnly({
430
+ ...opts,
431
+ event,
432
+ skipXapi: opts.skipXapi,
433
+ onXapiStatementSent: opts.onXapiStatementSent
434
+ });
435
+ }
436
+ return "emitted";
338
437
  }
339
438
  function assertTrackingSinkConfig(tracking) {
340
439
  if (!tracking?.sink || !tracking?.batchSink) return;
@@ -375,6 +474,7 @@ function useLessonkitProviderRuntime(config) {
375
474
  pluginHostRef.current = pluginHost;
376
475
  const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
377
476
  const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
477
+ const courseStartedEmitGenerationRef = (0, import_react.useRef)(0);
378
478
  const prevCourseIdForProgressRef = (0, import_react.useRef)(normalizedCourseId);
379
479
  const pendingCourseIdResetRef = (0, import_react.useRef)(false);
380
480
  const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
@@ -394,6 +494,7 @@ function useLessonkitProviderRuntime(config) {
394
494
  }
395
495
  pendingCourseIdResetRef.current = true;
396
496
  courseStartedEmittedToSinkRef.current = false;
497
+ courseStartedEmitGenerationRef.current += 1;
397
498
  } else if (useV2Runtime && !headlessRef.current) {
398
499
  headlessRef.current = (0, import_core8.createLessonkitRuntime)({
399
500
  courseId: normalizedCourseId,
@@ -411,6 +512,7 @@ function useLessonkitProviderRuntime(config) {
411
512
  }
412
513
  pendingCourseIdResetRef.current = true;
413
514
  courseStartedEmittedToSinkRef.current = false;
515
+ courseStartedEmitGenerationRef.current += 1;
414
516
  }
415
517
  if (useV2Runtime && headlessRef.current) {
416
518
  progressRef.current = headlessRef.current.progress;
@@ -421,7 +523,7 @@ function useLessonkitProviderRuntime(config) {
421
523
  }, []);
422
524
  const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
423
525
  activeLessonIdRef.current = progress.activeLessonId;
424
- const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
526
+ const xapiQueueRef = (0, import_react.useRef)((0, import_xapi4.createInMemoryXAPIQueue)());
425
527
  const xapiRef = (0, import_react.useRef)(null);
426
528
  const [xapi, setXapi] = (0, import_react.useState)(null);
427
529
  const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
@@ -442,7 +544,7 @@ function useLessonkitProviderRuntime(config) {
442
544
  }
443
545
  void xapiRef.current?.flush();
444
546
  }
445
- xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
547
+ xapiQueueRef.current = (0, import_xapi4.createInMemoryXAPIQueue)();
446
548
  prevXapiCourseIdRef.current = courseId;
447
549
  xapiCourseStartedSentOnClientRef.current = false;
448
550
  }
@@ -460,21 +562,24 @@ function useLessonkitProviderRuntime(config) {
460
562
  const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
461
563
  if (needsBootstrap) {
462
564
  try {
463
- const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
464
- (0, import_core2.buildTelemetryEvent)({
465
- name: "course_started",
466
- courseId: cid,
467
- sessionId,
468
- attemptId: attemptIdRef.current,
469
- user: userRef.current
470
- })
471
- );
472
- if (statement) {
473
- next.send(statement);
474
- if (!alreadyStarted) {
475
- (0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
565
+ const event = buildCourseStartedEvent({
566
+ pluginHost: pluginHostRef.current,
567
+ courseId: cid,
568
+ sessionId,
569
+ attemptId: attemptIdRef.current,
570
+ user: userRef.current,
571
+ lxpackBridge: lxpackBridgeModeRef.current
572
+ });
573
+ if (event === null) {
574
+ } else {
575
+ const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
576
+ if (statement) {
577
+ next.send(statement);
578
+ if (!alreadyStarted) {
579
+ (0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
580
+ }
581
+ xapiCourseStartedSentOnClientRef.current = true;
476
582
  }
477
- xapiCourseStartedSentOnClientRef.current = true;
478
583
  }
479
584
  } catch {
480
585
  }
@@ -545,29 +650,39 @@ function useLessonkitProviderRuntime(config) {
545
650
  const sessionId = sessionIdRef.current;
546
651
  const cid = courseIdRef.current;
547
652
  const trackingActive = isTrackingActive(normalizedConfig.tracking);
653
+ const courseStartedFullySettled = (0, import_core5.hasCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid) && (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid) && (0, import_core5.hasCourseStartedPipelineDelivered)(defaultStorage, sessionId, cid);
548
654
  if (!trackingActive) {
549
655
  courseStartedEmittedToSinkRef.current = false;
550
- } else if (!courseStartedEmittedToSinkRef.current) {
551
- const emitted = emitPendingCourseStarted({
552
- pluginHost: pluginHostRef.current,
553
- tracking: next,
554
- xapi: xapiRef.current,
555
- storage: defaultStorage,
556
- sessionId,
557
- courseId: cid,
558
- attemptId: attemptIdRef.current,
559
- user: userRef.current,
560
- lxpackBridge: lxpackBridgeModeRef.current,
561
- extraSinks: extraSinksRef.current
562
- });
563
- if (emitted) {
564
- (0, import_core5.markCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid);
565
- }
566
- courseStartedEmittedToSinkRef.current = emitted;
567
- } else if (trackingActive) {
656
+ } else if (courseStartedFullySettled) {
568
657
  courseStartedEmittedToSinkRef.current = true;
658
+ } else if (!courseStartedEmittedToSinkRef.current) {
659
+ const generation = ++courseStartedEmitGenerationRef.current;
660
+ const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
661
+ void (async () => {
662
+ if (generation !== courseStartedEmitGenerationRef.current) return;
663
+ const result = await emitPendingCourseStarted({
664
+ pluginHost: pluginHostRef.current,
665
+ tracking: next,
666
+ xapi: xapiRef.current,
667
+ storage: defaultStorage,
668
+ sessionId,
669
+ courseId: cid,
670
+ attemptId: attemptIdRef.current,
671
+ user: userRef.current,
672
+ lxpackBridge: lxpackBridgeModeRef.current,
673
+ extraSinks: extraSinksRef.current,
674
+ skipXapi: xapiCourseStartedSentOnClientRef.current,
675
+ onXapiStatementSent: () => {
676
+ xapiCourseStartedSentOnClientRef.current = true;
677
+ },
678
+ shouldCommit
679
+ });
680
+ if (generation !== courseStartedEmitGenerationRef.current) return;
681
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
682
+ })();
569
683
  }
570
684
  return () => {
685
+ courseStartedEmitGenerationRef.current += 1;
571
686
  if (prev !== trackingRef.current) {
572
687
  void disposeTrackingClient(prev);
573
688
  }
@@ -644,7 +759,7 @@ function useLessonkitProviderRuntime(config) {
644
759
  } catch {
645
760
  }
646
761
  if (!courseStartedEmittedToSinkRef.current) {
647
- const emitted = emitPendingCourseStarted({
762
+ const result = await emitPendingCourseStarted({
648
763
  pluginHost: pluginHostRef.current,
649
764
  tracking: trackingRef.current,
650
765
  xapi: xapiRef.current,
@@ -656,7 +771,7 @@ function useLessonkitProviderRuntime(config) {
656
771
  lxpackBridge: lxpackBridgeModeRef.current,
657
772
  extraSinks: extraSinksRef.current
658
773
  });
659
- courseStartedEmittedToSinkRef.current = emitted;
774
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
660
775
  }
661
776
  })();
662
777
  }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
@@ -670,7 +785,10 @@ function useLessonkitProviderRuntime(config) {
670
785
  [track]
671
786
  );
672
787
  const completeLesson = (0, import_react.useCallback)(
673
- (lessonId) => {
788
+ (lessonId, opts) => {
789
+ if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
790
+ return;
791
+ }
674
792
  if (useV2Runtime && headlessRef.current) {
675
793
  headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
676
794
  syncProgress();
@@ -888,11 +1006,15 @@ function useEnclosingLessonId() {
888
1006
 
889
1007
  // src/runtime/validateComponentId.ts
890
1008
  var import_core9 = require("@lessonkit/core");
891
- function isDevEnvironment3() {
1009
+ function isDevEnvironment4() {
892
1010
  const g = globalThis;
893
1011
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
894
1012
  }
895
1013
  function normalizeComponentId(id, path) {
1014
+ if (path === "courseId") return (0, import_core9.assertValidId)(id, "courseId");
1015
+ if (path === "lessonId") return (0, import_core9.assertValidId)(id, "lessonId");
1016
+ if (path === "checkId") return (0, import_core9.assertValidId)(id, "checkId");
1017
+ if (path === "blockId") return (0, import_core9.assertValidId)(id, "blockId");
896
1018
  return (0, import_core9.assertValidId)(id, path);
897
1019
  }
898
1020
 
@@ -900,7 +1022,7 @@ function normalizeComponentId(id, path) {
900
1022
  var mountCounts = /* @__PURE__ */ new Map();
901
1023
  var warnedConcurrentLessons = false;
902
1024
  function registerLessonMount(lessonId) {
903
- if (isDevEnvironment3() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
1025
+ if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
904
1026
  warnedConcurrentLessons = true;
905
1027
  console.warn(
906
1028
  "[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
@@ -943,9 +1065,18 @@ function Lesson(props) {
943
1065
  const { setActiveLesson, config } = useLessonkit();
944
1066
  const { completeLesson } = useCompletion();
945
1067
  const lessonMountGenerationRef = (0, import_react5.useRef)(0);
1068
+ const liveCourseIdRef = (0, import_react5.useRef)(config.courseId);
1069
+ liveCourseIdRef.current = config.courseId;
946
1070
  (0, import_react5.useEffect)(() => {
947
1071
  const unregister = registerLessonMount(lessonId);
948
1072
  const generation = ++lessonMountGenerationRef.current;
1073
+ const mountedCourseId = config.courseId;
1074
+ let effectSurvivedTick = false;
1075
+ queueMicrotask(() => {
1076
+ queueMicrotask(() => {
1077
+ effectSurvivedTick = true;
1078
+ });
1079
+ });
949
1080
  setActiveLesson(lessonId);
950
1081
  return () => {
951
1082
  unregister();
@@ -954,8 +1085,10 @@ function Lesson(props) {
954
1085
  }
955
1086
  if (!autoComplete) return;
956
1087
  queueMicrotask(() => {
1088
+ if (!effectSurvivedTick) return;
957
1089
  if (lessonMountGenerationRef.current !== generation) return;
958
- completeLesson(lessonId);
1090
+ if (liveCourseIdRef.current !== mountedCourseId) return;
1091
+ completeLesson(lessonId, { courseId: mountedCourseId });
959
1092
  });
960
1093
  };
961
1094
  }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
@@ -1014,11 +1147,10 @@ function KnowledgeCheck(props) {
1014
1147
  );
1015
1148
  }
1016
1149
  function Quiz(props) {
1017
- const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1018
1150
  const enclosingLessonId = useEnclosingLessonId();
1019
1151
  const missingLesson = enclosingLessonId === void 0;
1020
1152
  (0, import_react5.useEffect)(() => {
1021
- if (!missingLesson || isDevEnvironment3()) return;
1153
+ if (!missingLesson || isDevEnvironment4()) return;
1022
1154
  if (!warnedQuizOutsideLesson) {
1023
1155
  warnedQuizOutsideLesson = true;
1024
1156
  console.error(
@@ -1026,9 +1158,17 @@ function Quiz(props) {
1026
1158
  );
1027
1159
  }
1028
1160
  }, [missingLesson]);
1029
- if (missingLesson && isDevEnvironment3()) {
1161
+ if (missingLesson && isDevEnvironment4()) {
1030
1162
  throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1031
1163
  }
1164
+ if (missingLesson) {
1165
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
1166
+ }
1167
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(QuizInner, { ...props, enclosingLessonId });
1168
+ }
1169
+ function QuizInner(props) {
1170
+ const { enclosingLessonId } = props;
1171
+ const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1032
1172
  const quiz = useQuizState(enclosingLessonId);
1033
1173
  const { plugins, config, session } = useLessonkit();
1034
1174
  const [selected, setSelected] = (0, import_react5.useState)(null);
@@ -1051,9 +1191,6 @@ function Quiz(props) {
1051
1191
  }
1052
1192
  return choice === props.answer;
1053
1193
  };
1054
- if (missingLesson) {
1055
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
1056
- }
1057
1194
  const passed = quizPassed;
1058
1195
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1059
1196
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
@@ -1386,7 +1523,13 @@ var BLOCK_CATALOG = [
1386
1523
  { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
1387
1524
  { name: "question", type: "string", required: true, description: "Question text shown above choices." },
1388
1525
  { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
1389
- { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
1526
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." },
1527
+ {
1528
+ name: "passingScore",
1529
+ type: "number",
1530
+ required: false,
1531
+ description: "Minimum score required to pass (defaults to maxScore when omitted)."
1532
+ }
1390
1533
  ],
1391
1534
  requiredIds: ["checkId"],
1392
1535
  parentConstraints: ["Lesson"],