@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.
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "react";
17
17
  import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
18
18
  import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
19
- import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
19
+ import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
20
20
 
21
21
  // src/runtime/emitTelemetry.ts
22
22
  import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
@@ -135,9 +135,65 @@ import {
135
135
  markCourseStarted,
136
136
  hasCourseStartedEmittedToTracking,
137
137
  markCourseStartedEmittedToTracking,
138
+ hasCourseStartedPipelineDelivered,
139
+ markCourseStartedPipelineDelivered,
138
140
  migrateCourseStartedMark
139
141
  } from "@lessonkit/core";
140
142
 
143
+ // src/runtime/courseStartedPipeline.ts
144
+ import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
145
+ function isDevEnvironment3() {
146
+ const g = globalThis;
147
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
148
+ }
149
+ function warnExtraSinkFailure(sinkId, err) {
150
+ if (isDevEnvironment3()) {
151
+ console.warn(
152
+ `[lessonkit] course_started extra sink "${sinkId}" failed:`,
153
+ err instanceof Error ? err.message : err
154
+ );
155
+ }
156
+ }
157
+ async function emitExtraSinks(sinks, event, emitCtx) {
158
+ await Promise.all(
159
+ sinks.map(async (sink) => {
160
+ let result;
161
+ try {
162
+ result = sink.emit(event, emitCtx);
163
+ } catch (err) {
164
+ warnExtraSinkFailure(sink.id, err);
165
+ throw err;
166
+ }
167
+ if (result != null && typeof result.then === "function") {
168
+ try {
169
+ await result;
170
+ } catch (err) {
171
+ warnExtraSinkFailure(sink.id, err);
172
+ throw err;
173
+ }
174
+ }
175
+ })
176
+ );
177
+ }
178
+ async function emitCourseStartedNonTrackingPipeline(opts) {
179
+ let xapiStatementSent = false;
180
+ if (!opts.skipXapi && opts.xapi) {
181
+ const statement = telemetryEventToXAPIStatement2(opts.event);
182
+ if (statement) {
183
+ opts.xapi.send(statement);
184
+ xapiStatementSent = true;
185
+ }
186
+ }
187
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
188
+ const emitCtx = {
189
+ courseId: opts.event.courseId,
190
+ sessionId: opts.event.sessionId,
191
+ attemptId: opts.event.attemptId
192
+ };
193
+ await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
194
+ return { xapiStatementSent };
195
+ }
196
+
141
197
  // src/runtime/plugins.ts
142
198
  import { createPluginRegistry } from "@lessonkit/core";
143
199
  function createReactPluginHost(plugins) {
@@ -186,11 +242,13 @@ async function disposeTrackingClient(client) {
186
242
  // src/provider/useLessonkitProviderRuntime.ts
187
243
  var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
188
244
  var defaultStorage = createSessionStoragePort();
245
+ var courseStartedTrackingFlightKey = null;
189
246
  function isTrackingActive(tracking) {
190
247
  return tracking?.enabled !== false;
191
248
  }
192
- var noopTrackingClient = { track: () => {
193
- } };
249
+ function isCourseStartedSinkSettled(result) {
250
+ return result === "emitted";
251
+ }
194
252
  function buildCourseStartedEvent(opts) {
195
253
  const pluginCtx = buildPluginContext({
196
254
  courseId: opts.courseId,
@@ -207,85 +265,113 @@ function buildCourseStartedEvent(opts) {
207
265
  });
208
266
  return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
209
267
  }
210
- function emitCourseStartedPipelineOnly(opts) {
211
- const pluginCtx = buildPluginContext({
212
- courseId: opts.courseId,
213
- sessionId: opts.sessionId,
214
- attemptId: opts.attemptId,
215
- user: opts.user
216
- });
268
+ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
269
+ const flightKey = `${sessionId}:${courseId}`;
270
+ if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
271
+ return true;
272
+ }
273
+ if (courseStartedTrackingFlightKey === flightKey) {
274
+ return false;
275
+ }
276
+ courseStartedTrackingFlightKey = flightKey;
217
277
  try {
218
- emitTelemetryWithPlugins({
219
- pluginHost: null,
220
- tracking: noopTrackingClient,
221
- xapi: opts.xapi,
278
+ if (shouldCommit && !shouldCommit()) return false;
279
+ tracking.track(event);
280
+ await tracking.flush?.();
281
+ if (shouldCommit && !shouldCommit()) return false;
282
+ markCourseStartedEmittedToTracking(storage, sessionId, courseId);
283
+ return true;
284
+ } catch {
285
+ return false;
286
+ } finally {
287
+ if (courseStartedTrackingFlightKey === flightKey) {
288
+ courseStartedTrackingFlightKey = null;
289
+ }
290
+ }
291
+ }
292
+ async function emitCourseStartedPipelineOnly(opts) {
293
+ try {
294
+ if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
295
+ const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
222
296
  event: opts.event,
223
- pluginCtx,
297
+ xapi: opts.xapi,
224
298
  lxpackBridge: opts.lxpackBridge,
225
- extraSinks: opts.extraSinks
299
+ extraSinks: opts.extraSinks,
300
+ skipXapi: opts.skipXapi
226
301
  });
302
+ if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
227
303
  markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
228
- return true;
304
+ markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
305
+ if (xapiStatementSent) {
306
+ opts.onXapiStatementSent?.();
307
+ }
308
+ return "emitted";
229
309
  } catch {
230
- return false;
310
+ return "failed";
231
311
  }
232
312
  }
233
- function emitCourseStarted(opts) {
313
+ async function emitCourseStarted(opts) {
234
314
  const event = buildCourseStartedEvent(opts);
235
- if (event === null) return true;
315
+ if (event === null) return "filtered";
236
316
  const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
237
317
  opts.storage,
238
318
  opts.sessionId,
239
319
  opts.courseId
240
320
  );
241
321
  if (!trackingAlreadyEmitted) {
242
- try {
243
- opts.tracking.track(event);
244
- markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
245
- } catch {
246
- return false;
247
- }
322
+ const tracked = await emitCourseStartedToTracking(
323
+ opts.tracking,
324
+ opts.storage,
325
+ opts.sessionId,
326
+ opts.courseId,
327
+ event,
328
+ opts.shouldCommit
329
+ );
330
+ if (!tracked) return "failed";
248
331
  }
249
- return emitCourseStartedPipelineOnly({ ...opts, event });
332
+ return emitCourseStartedPipelineOnly({
333
+ ...opts,
334
+ event,
335
+ skipXapi: opts.skipXapi,
336
+ onXapiStatementSent: opts.onXapiStatementSent,
337
+ shouldCommit: opts.shouldCommit
338
+ });
250
339
  }
251
- function emitCourseStartedToTrackingOnly(opts) {
340
+ async function emitCourseStartedToTrackingOnly(opts) {
252
341
  const event = buildCourseStartedEvent(opts);
253
- if (event === null) return true;
342
+ if (event === null) return "filtered";
254
343
  const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
255
344
  opts.storage,
256
345
  opts.sessionId,
257
346
  opts.courseId
258
347
  );
259
348
  if (!trackingAlreadyEmitted) {
260
- try {
261
- opts.tracking.track(event);
262
- markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
263
- } catch {
264
- return false;
265
- }
349
+ const tracked = await emitCourseStartedToTracking(
350
+ opts.tracking,
351
+ opts.storage,
352
+ opts.sessionId,
353
+ opts.courseId,
354
+ event,
355
+ opts.shouldCommit
356
+ );
357
+ if (!tracked) return "failed";
266
358
  }
267
- const pluginCtx = buildPluginContext({
268
- courseId: opts.courseId,
269
- sessionId: opts.sessionId,
270
- attemptId: opts.attemptId,
271
- user: opts.user
272
- });
273
359
  try {
274
- emitTelemetryWithPlugins({
275
- pluginHost: null,
276
- tracking: noopTrackingClient,
277
- xapi: null,
360
+ if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
361
+ await emitCourseStartedNonTrackingPipeline({
278
362
  event,
279
- pluginCtx,
363
+ xapi: null,
280
364
  lxpackBridge: opts.lxpackBridge,
281
- extraSinks: opts.extraSinks
365
+ extraSinks: opts.extraSinks,
366
+ skipXapi: true
282
367
  });
283
- return true;
368
+ markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
369
+ return "emitted";
284
370
  } catch {
285
- return false;
371
+ return "failed";
286
372
  }
287
373
  }
288
- function emitPendingCourseStarted(opts) {
374
+ async function emitPendingCourseStarted(opts) {
289
375
  const trackingEmitted = hasCourseStartedEmittedToTracking(
290
376
  opts.storage,
291
377
  opts.sessionId,
@@ -297,13 +383,28 @@ function emitPendingCourseStarted(opts) {
297
383
  }
298
384
  if (trackingEmitted && !sessionStarted) {
299
385
  const event = buildCourseStartedEvent(opts);
300
- if (event === null) return true;
386
+ if (event === null) return "filtered";
301
387
  return emitCourseStartedPipelineOnly({ ...opts, event });
302
388
  }
303
389
  if (!trackingEmitted && !sessionStarted) {
304
390
  return emitCourseStarted(opts);
305
391
  }
306
- return true;
392
+ const pipelineDelivered = hasCourseStartedPipelineDelivered(
393
+ opts.storage,
394
+ opts.sessionId,
395
+ opts.courseId
396
+ );
397
+ if (sessionStarted && trackingEmitted && !pipelineDelivered) {
398
+ const event = buildCourseStartedEvent(opts);
399
+ if (event === null) return "filtered";
400
+ return emitCourseStartedPipelineOnly({
401
+ ...opts,
402
+ event,
403
+ skipXapi: opts.skipXapi,
404
+ onXapiStatementSent: opts.onXapiStatementSent
405
+ });
406
+ }
407
+ return "emitted";
307
408
  }
308
409
  function assertTrackingSinkConfig(tracking) {
309
410
  if (!tracking?.sink || !tracking?.batchSink) return;
@@ -344,6 +445,7 @@ function useLessonkitProviderRuntime(config) {
344
445
  pluginHostRef.current = pluginHost;
345
446
  const progressRef = useRef(createProgressController());
346
447
  const courseStartedEmittedToSinkRef = useRef(false);
448
+ const courseStartedEmitGenerationRef = useRef(0);
347
449
  const prevCourseIdForProgressRef = useRef(normalizedCourseId);
348
450
  const pendingCourseIdResetRef = useRef(false);
349
451
  const prevUseV2RuntimeRef = useRef(useV2Runtime);
@@ -363,6 +465,7 @@ function useLessonkitProviderRuntime(config) {
363
465
  }
364
466
  pendingCourseIdResetRef.current = true;
365
467
  courseStartedEmittedToSinkRef.current = false;
468
+ courseStartedEmitGenerationRef.current += 1;
366
469
  } else if (useV2Runtime && !headlessRef.current) {
367
470
  headlessRef.current = createLessonkitRuntime({
368
471
  courseId: normalizedCourseId,
@@ -380,6 +483,7 @@ function useLessonkitProviderRuntime(config) {
380
483
  }
381
484
  pendingCourseIdResetRef.current = true;
382
485
  courseStartedEmittedToSinkRef.current = false;
486
+ courseStartedEmitGenerationRef.current += 1;
383
487
  }
384
488
  if (useV2Runtime && headlessRef.current) {
385
489
  progressRef.current = headlessRef.current.progress;
@@ -429,21 +533,24 @@ function useLessonkitProviderRuntime(config) {
429
533
  const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
430
534
  if (needsBootstrap) {
431
535
  try {
432
- const statement = telemetryEventToXAPIStatement2(
433
- buildTelemetryEvent({
434
- name: "course_started",
435
- courseId: cid,
436
- sessionId,
437
- attemptId: attemptIdRef.current,
438
- user: userRef.current
439
- })
440
- );
441
- if (statement) {
442
- next.send(statement);
443
- if (!alreadyStarted) {
444
- markCourseStarted(defaultStorage, sessionId, cid);
536
+ const event = buildCourseStartedEvent({
537
+ pluginHost: pluginHostRef.current,
538
+ courseId: cid,
539
+ sessionId,
540
+ attemptId: attemptIdRef.current,
541
+ user: userRef.current,
542
+ lxpackBridge: lxpackBridgeModeRef.current
543
+ });
544
+ if (event === null) {
545
+ } else {
546
+ const statement = telemetryEventToXAPIStatement3(event);
547
+ if (statement) {
548
+ next.send(statement);
549
+ if (!alreadyStarted) {
550
+ markCourseStarted(defaultStorage, sessionId, cid);
551
+ }
552
+ xapiCourseStartedSentOnClientRef.current = true;
445
553
  }
446
- xapiCourseStartedSentOnClientRef.current = true;
447
554
  }
448
555
  } catch {
449
556
  }
@@ -514,29 +621,39 @@ function useLessonkitProviderRuntime(config) {
514
621
  const sessionId = sessionIdRef.current;
515
622
  const cid = courseIdRef.current;
516
623
  const trackingActive = isTrackingActive(normalizedConfig.tracking);
624
+ const courseStartedFullySettled = hasCourseStartedEmittedToTracking(defaultStorage, sessionId, cid) && hasCourseStarted(defaultStorage, sessionId, cid) && hasCourseStartedPipelineDelivered(defaultStorage, sessionId, cid);
517
625
  if (!trackingActive) {
518
626
  courseStartedEmittedToSinkRef.current = false;
519
- } else if (!courseStartedEmittedToSinkRef.current) {
520
- const emitted = emitPendingCourseStarted({
521
- pluginHost: pluginHostRef.current,
522
- tracking: next,
523
- xapi: xapiRef.current,
524
- storage: defaultStorage,
525
- sessionId,
526
- courseId: cid,
527
- attemptId: attemptIdRef.current,
528
- user: userRef.current,
529
- lxpackBridge: lxpackBridgeModeRef.current,
530
- extraSinks: extraSinksRef.current
531
- });
532
- if (emitted) {
533
- markCourseStartedEmittedToTracking(defaultStorage, sessionId, cid);
534
- }
535
- courseStartedEmittedToSinkRef.current = emitted;
536
- } else if (trackingActive) {
627
+ } else if (courseStartedFullySettled) {
537
628
  courseStartedEmittedToSinkRef.current = true;
629
+ } else if (!courseStartedEmittedToSinkRef.current) {
630
+ const generation = ++courseStartedEmitGenerationRef.current;
631
+ const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
632
+ void (async () => {
633
+ if (generation !== courseStartedEmitGenerationRef.current) return;
634
+ const result = await emitPendingCourseStarted({
635
+ pluginHost: pluginHostRef.current,
636
+ tracking: next,
637
+ xapi: xapiRef.current,
638
+ storage: defaultStorage,
639
+ sessionId,
640
+ courseId: cid,
641
+ attemptId: attemptIdRef.current,
642
+ user: userRef.current,
643
+ lxpackBridge: lxpackBridgeModeRef.current,
644
+ extraSinks: extraSinksRef.current,
645
+ skipXapi: xapiCourseStartedSentOnClientRef.current,
646
+ onXapiStatementSent: () => {
647
+ xapiCourseStartedSentOnClientRef.current = true;
648
+ },
649
+ shouldCommit
650
+ });
651
+ if (generation !== courseStartedEmitGenerationRef.current) return;
652
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
653
+ })();
538
654
  }
539
655
  return () => {
656
+ courseStartedEmitGenerationRef.current += 1;
540
657
  if (prev !== trackingRef.current) {
541
658
  void disposeTrackingClient(prev);
542
659
  }
@@ -613,7 +730,7 @@ function useLessonkitProviderRuntime(config) {
613
730
  } catch {
614
731
  }
615
732
  if (!courseStartedEmittedToSinkRef.current) {
616
- const emitted = emitPendingCourseStarted({
733
+ const result = await emitPendingCourseStarted({
617
734
  pluginHost: pluginHostRef.current,
618
735
  tracking: trackingRef.current,
619
736
  xapi: xapiRef.current,
@@ -625,7 +742,7 @@ function useLessonkitProviderRuntime(config) {
625
742
  lxpackBridge: lxpackBridgeModeRef.current,
626
743
  extraSinks: extraSinksRef.current
627
744
  });
628
- courseStartedEmittedToSinkRef.current = emitted;
745
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
629
746
  }
630
747
  })();
631
748
  }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
@@ -639,7 +756,10 @@ function useLessonkitProviderRuntime(config) {
639
756
  [track]
640
757
  );
641
758
  const completeLesson = useCallback(
642
- (lessonId) => {
759
+ (lessonId, opts) => {
760
+ if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
761
+ return;
762
+ }
643
763
  if (useV2Runtime && headlessRef.current) {
644
764
  headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
645
765
  syncProgress();
@@ -857,11 +977,15 @@ function useEnclosingLessonId() {
857
977
 
858
978
  // src/runtime/validateComponentId.ts
859
979
  import { assertValidId as assertValidId2 } from "@lessonkit/core";
860
- function isDevEnvironment3() {
980
+ function isDevEnvironment4() {
861
981
  const g = globalThis;
862
982
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
863
983
  }
864
984
  function normalizeComponentId(id, path) {
985
+ if (path === "courseId") return assertValidId2(id, "courseId");
986
+ if (path === "lessonId") return assertValidId2(id, "lessonId");
987
+ if (path === "checkId") return assertValidId2(id, "checkId");
988
+ if (path === "blockId") return assertValidId2(id, "blockId");
865
989
  return assertValidId2(id, path);
866
990
  }
867
991
 
@@ -869,7 +993,7 @@ function normalizeComponentId(id, path) {
869
993
  var mountCounts = /* @__PURE__ */ new Map();
870
994
  var warnedConcurrentLessons = false;
871
995
  function registerLessonMount(lessonId) {
872
- if (isDevEnvironment3() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
996
+ if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
873
997
  warnedConcurrentLessons = true;
874
998
  console.warn(
875
999
  "[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."
@@ -912,9 +1036,18 @@ function Lesson(props) {
912
1036
  const { setActiveLesson, config } = useLessonkit();
913
1037
  const { completeLesson } = useCompletion();
914
1038
  const lessonMountGenerationRef = useRef2(0);
1039
+ const liveCourseIdRef = useRef2(config.courseId);
1040
+ liveCourseIdRef.current = config.courseId;
915
1041
  useEffect2(() => {
916
1042
  const unregister = registerLessonMount(lessonId);
917
1043
  const generation = ++lessonMountGenerationRef.current;
1044
+ const mountedCourseId = config.courseId;
1045
+ let effectSurvivedTick = false;
1046
+ queueMicrotask(() => {
1047
+ queueMicrotask(() => {
1048
+ effectSurvivedTick = true;
1049
+ });
1050
+ });
918
1051
  setActiveLesson(lessonId);
919
1052
  return () => {
920
1053
  unregister();
@@ -923,8 +1056,10 @@ function Lesson(props) {
923
1056
  }
924
1057
  if (!autoComplete) return;
925
1058
  queueMicrotask(() => {
1059
+ if (!effectSurvivedTick) return;
926
1060
  if (lessonMountGenerationRef.current !== generation) return;
927
- completeLesson(lessonId);
1061
+ if (liveCourseIdRef.current !== mountedCourseId) return;
1062
+ completeLesson(lessonId, { courseId: mountedCourseId });
928
1063
  });
929
1064
  };
930
1065
  }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
@@ -983,11 +1118,10 @@ function KnowledgeCheck(props) {
983
1118
  );
984
1119
  }
985
1120
  function Quiz(props) {
986
- const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
987
1121
  const enclosingLessonId = useEnclosingLessonId();
988
1122
  const missingLesson = enclosingLessonId === void 0;
989
1123
  useEffect2(() => {
990
- if (!missingLesson || isDevEnvironment3()) return;
1124
+ if (!missingLesson || isDevEnvironment4()) return;
991
1125
  if (!warnedQuizOutsideLesson) {
992
1126
  warnedQuizOutsideLesson = true;
993
1127
  console.error(
@@ -995,9 +1129,17 @@ function Quiz(props) {
995
1129
  );
996
1130
  }
997
1131
  }, [missingLesson]);
998
- if (missingLesson && isDevEnvironment3()) {
1132
+ if (missingLesson && isDevEnvironment4()) {
999
1133
  throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1000
1134
  }
1135
+ if (missingLesson) {
1136
+ return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
1137
+ }
1138
+ return /* @__PURE__ */ jsx2(QuizInner, { ...props, enclosingLessonId });
1139
+ }
1140
+ function QuizInner(props) {
1141
+ const { enclosingLessonId } = props;
1142
+ const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1001
1143
  const quiz = useQuizState(enclosingLessonId);
1002
1144
  const { plugins, config, session } = useLessonkit();
1003
1145
  const [selected, setSelected] = useState2(null);
@@ -1020,9 +1162,6 @@ function Quiz(props) {
1020
1162
  }
1021
1163
  return choice === props.answer;
1022
1164
  };
1023
- if (missingLesson) {
1024
- return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
1025
- }
1026
1165
  const passed = quizPassed;
1027
1166
  return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1028
1167
  /* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
@@ -1378,7 +1517,13 @@ var BLOCK_CATALOG = [
1378
1517
  { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
1379
1518
  { name: "question", type: "string", required: true, description: "Question text shown above choices." },
1380
1519
  { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
1381
- { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
1520
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." },
1521
+ {
1522
+ name: "passingScore",
1523
+ type: "number",
1524
+ required: false,
1525
+ description: "Minimum score required to pass (defaults to maxScore when omitted)."
1526
+ }
1382
1527
  ],
1383
1528
  requiredIds: ["checkId"],
1384
1529
  parentConstraints: ["Lesson"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -56,11 +56,11 @@
56
56
  "react-dom": ">=18"
57
57
  },
58
58
  "dependencies": {
59
- "@lessonkit/accessibility": "1.0.0",
60
- "@lessonkit/core": "1.0.0",
61
- "@lessonkit/lxpack": "1.0.0",
62
- "@lessonkit/themes": "1.0.0",
63
- "@lessonkit/xapi": "1.0.0"
59
+ "@lessonkit/accessibility": "1.0.2",
60
+ "@lessonkit/core": "1.0.2",
61
+ "@lessonkit/lxpack": "1.0.2",
62
+ "@lessonkit/themes": "1.0.2",
63
+ "@lessonkit/xapi": "1.0.2"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@storybook/addon-essentials": "8.6.18",
@@ -80,6 +80,6 @@
80
80
  "tsup": "^8.5.0",
81
81
  "typescript": "^5.8.3",
82
82
  "vite": "^6.3.5",
83
- "vitest": "^3.2.4"
83
+ "vitest": "^4.1.8"
84
84
  }
85
85
  }