@lessonkit/react 1.2.0 → 1.3.1

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.cjs CHANGED
@@ -57,6 +57,8 @@ __export(index_exports, {
57
57
  Quiz: () => Quiz,
58
58
  Reflection: () => Reflection,
59
59
  Scenario: () => Scenario,
60
+ Slide: () => Slide,
61
+ SlideDeck: () => SlideDeck,
60
62
  Text: () => Text,
61
63
  ThemeProvider: () => ThemeProvider,
62
64
  TrueFalse: () => TrueFalse,
@@ -64,13 +66,13 @@ __export(index_exports, {
64
66
  blockCatalogV3Version: () => blockCatalogV3Version,
65
67
  blockCatalogVersion: () => blockCatalogVersion,
66
68
  buildBlockCatalog: () => buildBlockCatalog,
67
- buildTelemetryEvent: () => import_core17.buildTelemetryEvent,
68
- createLessonkitRuntime: () => import_core17.createLessonkitRuntime,
69
- createPluginRegistry: () => import_core17.createPluginRegistry,
70
- createTelemetryPipeline: () => import_core17.createTelemetryPipeline,
71
- defineAssessmentPlugin: () => import_core17.defineAssessmentPlugin,
72
- defineLifecyclePlugin: () => import_core17.defineLifecyclePlugin,
73
- defineTelemetryPlugin: () => import_core17.defineTelemetryPlugin,
69
+ buildTelemetryEvent: () => import_core19.buildTelemetryEvent,
70
+ createLessonkitRuntime: () => import_core19.createLessonkitRuntime,
71
+ createPluginRegistry: () => import_core19.createPluginRegistry,
72
+ createTelemetryPipeline: () => import_core19.createTelemetryPipeline,
73
+ defineAssessmentPlugin: () => import_core19.defineAssessmentPlugin,
74
+ defineLifecyclePlugin: () => import_core19.defineLifecyclePlugin,
75
+ defineTelemetryPlugin: () => import_core19.defineTelemetryPlugin,
74
76
  getBlockCatalogEntry: () => getBlockCatalogEntry,
75
77
  resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
76
78
  resetQuizWarningsForTests: () => resetQuizWarningsForTests,
@@ -85,7 +87,7 @@ __export(index_exports, {
85
87
  module.exports = __toCommonJS(index_exports);
86
88
 
87
89
  // src/components.tsx
88
- var import_react11 = require("react");
90
+ var import_react13 = require("react");
89
91
  var import_accessibility2 = require("@lessonkit/accessibility");
90
92
 
91
93
  // src/context.tsx
@@ -97,6 +99,7 @@ var import_core8 = require("@lessonkit/core");
97
99
 
98
100
  // src/runtime/observability.ts
99
101
  var import_xapi = require("@lessonkit/xapi");
102
+ var import_meta = {};
100
103
  function createXapiQueueFromObservability(observability) {
101
104
  const opts = {};
102
105
  if (observability?.onXapiQueueDepth) {
@@ -107,6 +110,44 @@ function createXapiQueueFromObservability(observability) {
107
110
  }
108
111
  return (0, import_xapi.createInMemoryXAPIQueue)(opts);
109
112
  }
113
+ function wrapBatchSink(batchSink, observability) {
114
+ if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
115
+ const onError = observability.onTelemetrySinkError;
116
+ return async (events) => {
117
+ try {
118
+ await batchSink(events);
119
+ } catch (err) {
120
+ onError(err, { sinkId: "tracking-batch" });
121
+ throw err;
122
+ }
123
+ };
124
+ }
125
+ function warnMissingProductionObservability(observability, opts) {
126
+ let isProduction = false;
127
+ try {
128
+ isProduction = import_meta.env?.PROD === true;
129
+ } catch {
130
+ }
131
+ if (!isProduction) {
132
+ const g = globalThis;
133
+ isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
134
+ }
135
+ if (!isProduction) return;
136
+ if (!opts.trackingEnabled && !opts.xapiEnabled) return;
137
+ const hooks = [
138
+ observability?.onTelemetrySinkError,
139
+ observability?.onTelemetryBufferDrop,
140
+ observability?.onXapiQueueDepth,
141
+ observability?.onXapiQueueCap,
142
+ observability?.onLxpackBridgeMiss
143
+ ];
144
+ if (hooks.some(Boolean)) return;
145
+ if (typeof console !== "undefined") {
146
+ console.warn(
147
+ "[lessonkit] Production deployment without observability hooks \u2014 telemetry/xAPI failures and buffer drops will be silent. See https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html"
148
+ );
149
+ }
150
+ }
110
151
  function wrapTrackingSink(sink, observability) {
111
152
  if (!sink || !observability?.onTelemetrySinkError) return sink;
112
153
  const onError = observability.onTelemetrySinkError;
@@ -216,6 +257,9 @@ function emitTelemetry(tracking, xapi, event, opts) {
216
257
  // src/runtime/ports.ts
217
258
  var import_core3 = require("@lessonkit/core");
218
259
 
260
+ // src/provider/useLessonkitProviderRuntime.ts
261
+ var import_core9 = require("@lessonkit/core");
262
+
219
263
  // src/runtime/progress.ts
220
264
  var import_core4 = require("@lessonkit/core");
221
265
 
@@ -230,6 +274,7 @@ function createXapiClientFromConfig(config, queue) {
230
274
  return (0, import_xapi3.createXAPIClient)({
231
275
  courseId: config.courseId,
232
276
  transport: config.xapi?.transport,
277
+ exitTransport: config.xapi?.exitTransport,
233
278
  queue
234
279
  });
235
280
  }
@@ -278,6 +323,7 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
278
323
  const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(opts.event);
279
324
  if (statement) {
280
325
  opts.xapi.send(statement);
326
+ await opts.xapi.flush();
281
327
  xapiStatementSent = true;
282
328
  }
283
329
  }
@@ -313,7 +359,7 @@ function emitTelemetryWithPlugins(opts) {
313
359
  }
314
360
 
315
361
  // src/provider/courseStarted/emit.ts
316
- var courseStartedTrackingFlightKey = null;
362
+ var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
317
363
  function isTrackingActive(tracking) {
318
364
  return tracking?.enabled !== false;
319
365
  }
@@ -341,24 +387,40 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
341
387
  if ((0, import_core5.hasCourseStartedEmittedToTracking)(storage, sessionId, courseId)) {
342
388
  return true;
343
389
  }
344
- if (courseStartedTrackingFlightKey === flightKey) {
345
- return false;
390
+ const existing = courseStartedTrackingFlights.get(flightKey);
391
+ if (existing) {
392
+ const settled = await existing;
393
+ if (settled) return true;
346
394
  }
347
- courseStartedTrackingFlightKey = flightKey;
348
- try {
349
- if (shouldCommit && !shouldCommit()) return false;
350
- tracking.track(event);
351
- await tracking.flush?.();
352
- if (shouldCommit && !shouldCommit()) return false;
353
- (0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId);
354
- return true;
355
- } catch {
356
- return false;
357
- } finally {
358
- if (courseStartedTrackingFlightKey === flightKey) {
359
- courseStartedTrackingFlightKey = null;
395
+ let resolveFlight;
396
+ const flight = new Promise((resolve) => {
397
+ resolveFlight = resolve;
398
+ });
399
+ courseStartedTrackingFlights.set(flightKey, flight);
400
+ void (async () => {
401
+ try {
402
+ if (shouldCommit && !shouldCommit()) {
403
+ resolveFlight(false);
404
+ return;
405
+ }
406
+ tracking.track(event);
407
+ const delivered = await tracking.flush?.();
408
+ if (delivered === false) {
409
+ resolveFlight(false);
410
+ return;
411
+ }
412
+ if ((0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId) === false) {
413
+ resolveFlight(false);
414
+ return;
415
+ }
416
+ resolveFlight(true);
417
+ } catch {
418
+ resolveFlight(false);
419
+ } finally {
420
+ courseStartedTrackingFlights.delete(flightKey);
360
421
  }
361
- }
422
+ })();
423
+ return flight;
362
424
  }
363
425
  async function emitCourseStartedPipelineOnly(opts) {
364
426
  try {
@@ -372,8 +434,10 @@ async function emitCourseStartedPipelineOnly(opts) {
372
434
  skipXapi: opts.skipXapi
373
435
  });
374
436
  if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
375
- (0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
376
- (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
437
+ if ((0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
438
+ if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
439
+ return "failed";
440
+ }
377
441
  if (xapiStatementSent) {
378
442
  opts.onXapiStatementSent?.();
379
443
  }
@@ -424,7 +488,9 @@ async function emitCourseStartedToTrackingOnly(opts) {
424
488
  extraSinks: opts.extraSinks,
425
489
  skipXapi: true
426
490
  });
427
- (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
491
+ if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
492
+ return "failed";
493
+ }
428
494
  return "emitted";
429
495
  } catch {
430
496
  return "failed";
@@ -477,13 +543,15 @@ function assertTrackingSinkConfig(tracking) {
477
543
 
478
544
  // src/runtime/telemetry.ts
479
545
  var import_core7 = require("@lessonkit/core");
480
- function createTrackingClientFromConfig(config) {
546
+ function createTrackingClientFromConfig(config, observability) {
481
547
  if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
482
548
  if (config.tracking?.createClient) return config.tracking.createClient();
483
549
  return (0, import_core7.createTrackingClient)({
484
550
  sink: config.tracking?.sink,
485
551
  batchSink: config.tracking?.batchSink,
486
- batch: config.tracking?.batch
552
+ batch: config.tracking?.batch,
553
+ exitBatchSink: config.tracking?.exitBatchSink,
554
+ onBufferDrop: observability?.onTelemetryBufferDrop
487
555
  });
488
556
  }
489
557
  async function disposeTrackingClient(client) {
@@ -558,6 +626,7 @@ function useLessonkitProviderRuntime(config) {
558
626
  const pendingCourseIdResetRef = (0, import_react.useRef)(false);
559
627
  const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
560
628
  const xapiCourseStartedSentOnClientRef = (0, import_react.useRef)(false);
629
+ const xapiBootstrapSendRef = (0, import_react.useRef)(false);
561
630
  if (prevUseV2RuntimeRef.current !== useV2Runtime) {
562
631
  prevUseV2RuntimeRef.current = useV2Runtime;
563
632
  if (useV2Runtime) {
@@ -565,7 +634,8 @@ function useLessonkitProviderRuntime(config) {
565
634
  courseId: normalizedCourseId,
566
635
  runtimeVersion: "v2",
567
636
  session: normalizedConfig.session,
568
- plugins: pluginHostRef.current ?? normalizedConfig.plugins
637
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
638
+ deferPluginSetup: true
569
639
  });
570
640
  progressRef.current = headlessRef.current.progress;
571
641
  } else {
@@ -580,7 +650,8 @@ function useLessonkitProviderRuntime(config) {
580
650
  courseId: normalizedCourseId,
581
651
  runtimeVersion: "v2",
582
652
  session: normalizedConfig.session,
583
- plugins: pluginHostRef.current ?? normalizedConfig.plugins
653
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
654
+ deferPluginSetup: true
584
655
  });
585
656
  }
586
657
  if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
@@ -628,19 +699,22 @@ function useLessonkitProviderRuntime(config) {
628
699
  xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
629
700
  prevXapiCourseIdRef.current = courseId;
630
701
  xapiCourseStartedSentOnClientRef.current = false;
702
+ xapiBootstrapSendRef.current = false;
631
703
  }
632
704
  const prev = xapiRef.current;
633
705
  const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
634
706
  xapiRef.current = next;
635
707
  setXapi(next);
708
+ let bootstrapSent = false;
709
+ let bootstrapAlreadyStarted = false;
636
710
  if (next) {
637
711
  const sessionId = sessionIdRef.current;
638
712
  const cid = courseIdRef.current;
639
713
  const trackingActive = isTrackingActive(normalizedConfig.tracking);
640
- const alreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
714
+ bootstrapAlreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
641
715
  const clientChanged = !prev || prev !== next;
642
- const skipBootstrap = trackingActive && !alreadyStarted;
643
- const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
716
+ const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
717
+ const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
644
718
  if (needsBootstrap) {
645
719
  try {
646
720
  const event = buildCourseStartedEvent({
@@ -651,15 +725,12 @@ function useLessonkitProviderRuntime(config) {
651
725
  user: userRef.current,
652
726
  lxpackBridge: lxpackBridgeModeRef.current
653
727
  });
654
- if (event === null) {
655
- } else {
728
+ if (event !== null) {
656
729
  const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
657
730
  if (statement) {
658
731
  next.send(statement);
659
- if (!alreadyStarted) {
660
- (0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
661
- }
662
- xapiCourseStartedSentOnClientRef.current = true;
732
+ xapiBootstrapSendRef.current = true;
733
+ bootstrapSent = true;
663
734
  }
664
735
  }
665
736
  } catch {
@@ -677,6 +748,12 @@ function useLessonkitProviderRuntime(config) {
677
748
  if (cancelled) return;
678
749
  try {
679
750
  await next?.flush();
751
+ if (bootstrapSent && !cancelled) {
752
+ if (!bootstrapAlreadyStarted) {
753
+ (0, import_core5.markCourseStarted)(defaultStorage, sessionIdRef.current, courseIdRef.current);
754
+ }
755
+ xapiCourseStartedSentOnClientRef.current = true;
756
+ }
680
757
  } catch {
681
758
  }
682
759
  })();
@@ -705,7 +782,10 @@ function useLessonkitProviderRuntime(config) {
705
782
  useIsoLayoutEffect(() => {
706
783
  const prev = trackingRef.current;
707
784
  const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
708
- const userBatchSink = normalizedConfig.tracking?.batchSink;
785
+ const userBatchSink = wrapBatchSink(
786
+ normalizedConfig.tracking?.batchSink,
787
+ observabilityRef.current
788
+ );
709
789
  assertTrackingSinkConfig(normalizedConfig.tracking);
710
790
  const sink = pluginHostRef.current && baseSink ? (
711
791
  /* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
@@ -725,9 +805,12 @@ function useLessonkitProviderRuntime(config) {
725
805
  }
726
806
  return userBatchSink(perEventForBatch);
727
807
  } : userBatchSink;
728
- const next = createTrackingClientFromConfig({
729
- tracking: { ...normalizedConfig.tracking, sink, batchSink }
730
- });
808
+ const next = createTrackingClientFromConfig(
809
+ {
810
+ tracking: { ...normalizedConfig.tracking, sink, batchSink }
811
+ },
812
+ observabilityRef.current
813
+ );
731
814
  trackingRef.current = next;
732
815
  trackingClientForUnmountRef.current = next;
733
816
  setTracking(next);
@@ -856,7 +939,11 @@ function useLessonkitProviderRuntime(config) {
856
939
  user: userRef.current,
857
940
  lxpackBridge: lxpackBridgeModeRef.current,
858
941
  onLxpackBridgeMiss,
859
- extraSinks: extraSinksRef.current
942
+ extraSinks: extraSinksRef.current,
943
+ skipXapi: xapiCourseStartedSentOnClientRef.current,
944
+ onXapiStatementSent: () => {
945
+ xapiCourseStartedSentOnClientRef.current = true;
946
+ }
860
947
  });
861
948
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
862
949
  }
@@ -912,20 +999,39 @@ function useLessonkitProviderRuntime(config) {
912
999
  }, []);
913
1000
  (0, import_react.useEffect)(() => {
914
1001
  if (typeof document === "undefined") return;
915
- const flushOnExit = () => {
916
- void xapiRef.current?.flush();
917
- void trackingRef.current?.flush?.();
1002
+ const flushOnPageExit = () => {
1003
+ try {
1004
+ xapiRef.current?.flushOnExit?.();
1005
+ trackingRef.current?.flushOnExit?.();
1006
+ } finally {
1007
+ void xapiRef.current?.flush();
1008
+ void trackingRef.current?.flush?.();
1009
+ }
918
1010
  };
919
1011
  const onVisibilityChange = () => {
920
- if (document.visibilityState === "hidden") flushOnExit();
1012
+ if (document.visibilityState === "hidden") flushOnPageExit();
921
1013
  };
922
1014
  document.addEventListener("visibilitychange", onVisibilityChange);
923
- window.addEventListener("pagehide", flushOnExit);
1015
+ window.addEventListener("pagehide", flushOnPageExit);
924
1016
  return () => {
925
1017
  document.removeEventListener("visibilitychange", onVisibilityChange);
926
- window.removeEventListener("pagehide", flushOnExit);
1018
+ window.removeEventListener("pagehide", flushOnPageExit);
927
1019
  };
928
1020
  }, []);
1021
+ (0, import_react.useEffect)(() => {
1022
+ warnMissingProductionObservability(observabilityRef.current, {
1023
+ trackingEnabled: isTrackingActive(normalizedConfig.tracking),
1024
+ xapiEnabled: normalizedConfig.xapi?.enabled !== false && Boolean(
1025
+ normalizedConfig.xapi?.client || normalizedConfig.xapi?.transport || normalizedConfig.xapi?.enabled === true
1026
+ )
1027
+ });
1028
+ }, [
1029
+ normalizedConfig.tracking,
1030
+ normalizedConfig.xapi?.enabled,
1031
+ normalizedConfig.xapi?.client,
1032
+ normalizedConfig.xapi?.transport,
1033
+ normalizedConfig.observability
1034
+ ]);
929
1035
  const setActiveLesson = (0, import_react.useCallback)(
930
1036
  (lessonId) => {
931
1037
  if (useV2Runtime && headlessRef.current) {
@@ -1045,6 +1151,7 @@ function useLessonkitProviderRuntime(config) {
1045
1151
  config: normalizedConfig,
1046
1152
  tracking,
1047
1153
  xapi,
1154
+ storage: defaultStorage,
1048
1155
  session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
1049
1156
  progress,
1050
1157
  setActiveLesson,
@@ -1142,17 +1249,17 @@ function useEnclosingLessonId() {
1142
1249
  }
1143
1250
 
1144
1251
  // src/runtime/validateComponentId.ts
1145
- var import_core9 = require("@lessonkit/core");
1252
+ var import_core10 = require("@lessonkit/core");
1146
1253
  function isDevEnvironment4() {
1147
1254
  const g = globalThis;
1148
1255
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
1149
1256
  }
1150
1257
  function normalizeComponentId(id, path) {
1151
- if (path === "courseId") return (0, import_core9.assertValidId)(id, "courseId");
1152
- if (path === "lessonId") return (0, import_core9.assertValidId)(id, "lessonId");
1153
- if (path === "checkId") return (0, import_core9.assertValidId)(id, "checkId");
1154
- if (path === "blockId") return (0, import_core9.assertValidId)(id, "blockId");
1155
- return (0, import_core9.assertValidId)(id, path);
1258
+ if (path === "courseId") return (0, import_core10.assertValidId)(id, "courseId");
1259
+ if (path === "lessonId") return (0, import_core10.assertValidId)(id, "lessonId");
1260
+ if (path === "checkId") return (0, import_core10.assertValidId)(id, "checkId");
1261
+ if (path === "blockId") return (0, import_core10.assertValidId)(id, "blockId");
1262
+ return (0, import_core10.assertValidId)(id, path);
1156
1263
  }
1157
1264
 
1158
1265
  // src/runtime/lessonMountRegistry.ts
@@ -1180,7 +1287,7 @@ function getLessonMountCount(lessonId) {
1180
1287
  }
1181
1288
 
1182
1289
  // src/components/Quiz.tsx
1183
- var import_react10 = require("react");
1290
+ var import_react12 = require("react");
1184
1291
  var import_accessibility = require("@lessonkit/accessibility");
1185
1292
 
1186
1293
  // src/assessment/AssessmentLessonGuard.tsx
@@ -1239,140 +1346,184 @@ function readStringField(state, key) {
1239
1346
  if (typeof value === "string" || value === null) return value;
1240
1347
  return void 0;
1241
1348
  }
1349
+ function readNumberField(state, key) {
1350
+ const value = state[key];
1351
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1352
+ if (value === null) return null;
1353
+ return void 0;
1354
+ }
1242
1355
  function readBooleanStateField(state, key, apply) {
1243
1356
  const value = state[key];
1244
1357
  if (typeof value === "boolean") apply(value);
1245
1358
  }
1246
1359
 
1247
1360
  // src/assessment/internal/useAssessmentHandleRegistration.ts
1248
- var import_react8 = require("react");
1361
+ var import_react10 = require("react");
1249
1362
 
1250
1363
  // src/compound/CompoundProvider.tsx
1251
- var import_react7 = __toESM(require("react"), 1);
1252
- var import_core10 = require("@lessonkit/core");
1364
+ var import_react9 = __toESM(require("react"), 1);
1365
+ var import_core11 = require("@lessonkit/core");
1253
1366
 
1254
1367
  // src/compound/aggregateScores.ts
1255
- function aggregateAssessmentScores(handles) {
1368
+ function aggregateAssessmentScores(handles, opts) {
1256
1369
  let score = 0;
1257
1370
  let maxScore = 0;
1258
1371
  let allAnswered = true;
1259
- for (const handle of handles) {
1372
+ for (const entry of handles) {
1373
+ const handle = "handle" in entry ? entry.handle : entry;
1374
+ const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
1260
1375
  score += handle.getScore();
1261
1376
  maxScore += handle.getMaxScore();
1262
- if (!handle.getAnswerGiven()) allAnswered = false;
1377
+ const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
1378
+ if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
1263
1379
  }
1264
1380
  return { score, maxScore, allAnswered };
1265
1381
  }
1266
1382
 
1267
- // src/compound/resumeChildHandles.ts
1268
- function resumeChildHandles(handles, childStates, opts) {
1269
- if (opts?.waitForHandles && handles.size === 0 && Object.keys(childStates).length > 0) {
1270
- return false;
1271
- }
1272
- for (const [checkId, handle] of handles) {
1273
- const child = childStates[checkId];
1274
- if (child && handle.resume) handle.resume(child);
1275
- }
1276
- return true;
1383
+ // src/compound/CompoundHydrationBridge.tsx
1384
+ var import_react7 = require("react");
1385
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1386
+ var CompoundHydrationBridgeContext = (0, import_react7.createContext)(
1387
+ null
1388
+ );
1389
+ function CompoundHydrationBridgeProvider({ children }) {
1390
+ const bridgeRef = (0, import_react7.useRef)(null);
1391
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
1392
+ }
1393
+ function useCompoundHydrationBridgeRef() {
1394
+ return (0, import_react7.useContext)(CompoundHydrationBridgeContext);
1395
+ }
1396
+
1397
+ // src/compound/CompoundPageIndexContext.tsx
1398
+ var import_react8 = require("react");
1399
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1400
+ var CompoundPageIndexContext = (0, import_react8.createContext)(void 0);
1401
+ function CompoundPageIndexProvider({
1402
+ pageIndex,
1403
+ children
1404
+ }) {
1405
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CompoundPageIndexContext.Provider, { value: pageIndex, children });
1406
+ }
1407
+ function useCompoundPageIndex() {
1408
+ return (0, import_react8.useContext)(CompoundPageIndexContext);
1277
1409
  }
1278
1410
 
1279
1411
  // src/compound/CompoundProvider.tsx
1280
- var import_jsx_runtime3 = require("react/jsx-runtime");
1281
- var CompoundRegistryContext = (0, import_react7.createContext)(null);
1282
- var CompoundHandlesVersionContext = (0, import_react7.createContext)(0);
1412
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1413
+ var CompoundRegistryContext = (0, import_react9.createContext)(null);
1414
+ var CompoundHandlesVersionContext = (0, import_react9.createContext)(0);
1283
1415
  function CompoundProvider({
1284
1416
  children,
1285
1417
  activePageIndex: _activePageIndex,
1286
1418
  onActivePageIndexChange: _onActivePageIndexChange
1287
1419
  }) {
1288
- const registryRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
1289
- const [handlesVersion, setHandlesVersion] = (0, import_react7.useState)(0);
1290
- const register = (0, import_react7.useCallback)((checkId, handle) => {
1420
+ const registryRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
1421
+ const [handlesVersion, setHandlesVersion] = (0, import_react9.useState)(0);
1422
+ const register = (0, import_react9.useCallback)((checkId, handle, pageIndex) => {
1291
1423
  const prev = registryRef.current.get(checkId);
1292
- registryRef.current.set(checkId, handle);
1293
- if (prev !== handle) {
1424
+ if (prev && prev.handle !== handle) {
1425
+ const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
1426
+ if (isDevEnvironment4()) {
1427
+ console.error(message);
1428
+ } else {
1429
+ console.warn(message);
1430
+ }
1431
+ }
1432
+ registryRef.current.set(checkId, { handle, pageIndex });
1433
+ if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
1294
1434
  setHandlesVersion((v) => v + 1);
1295
1435
  }
1296
1436
  return () => {
1297
- if (registryRef.current.get(checkId) === handle) {
1437
+ const current = registryRef.current.get(checkId);
1438
+ if (current?.handle === handle) {
1298
1439
  registryRef.current.delete(checkId);
1299
1440
  setHandlesVersion((v) => v + 1);
1300
1441
  }
1301
1442
  };
1302
1443
  }, []);
1303
- const registryValue = (0, import_react7.useMemo)(
1444
+ const registryValue = (0, import_react9.useMemo)(
1304
1445
  () => ({
1305
1446
  register,
1306
- getHandles: () => registryRef.current
1447
+ getHandles: () => {
1448
+ const handles = /* @__PURE__ */ new Map();
1449
+ for (const [checkId, entry] of registryRef.current) {
1450
+ handles.set(checkId, entry.handle);
1451
+ }
1452
+ return handles;
1453
+ },
1454
+ getRegisteredHandles: () => registryRef.current
1307
1455
  }),
1308
1456
  [register]
1309
1457
  );
1310
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) });
1458
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
1311
1459
  }
1312
1460
  function useCompoundRegistry() {
1313
- const registry = (0, import_react7.useContext)(CompoundRegistryContext);
1314
- const handlesVersion = (0, import_react7.useContext)(CompoundHandlesVersionContext);
1461
+ const registry = (0, import_react9.useContext)(CompoundRegistryContext);
1462
+ const handlesVersion = (0, import_react9.useContext)(CompoundHandlesVersionContext);
1315
1463
  if (!registry) return null;
1316
1464
  return { ...registry, handlesVersion };
1317
1465
  }
1318
1466
  function useCompoundHandlesVersion() {
1319
- return (0, import_react7.useContext)(CompoundHandlesVersionContext);
1467
+ return (0, import_react9.useContext)(CompoundHandlesVersionContext);
1320
1468
  }
1321
1469
  function useRegisterAssessmentHandle(checkId, handle) {
1322
- const registry = (0, import_react7.useContext)(CompoundRegistryContext);
1323
- import_react7.default.useEffect(() => {
1470
+ const registry = (0, import_react9.useContext)(CompoundRegistryContext);
1471
+ const pageIndex = useCompoundPageIndex();
1472
+ import_react9.default.useLayoutEffect(() => {
1324
1473
  if (!registry || !handle) return;
1325
- return registry.register(checkId, handle);
1326
- }, [registry, checkId, handle]);
1474
+ return registry.register(checkId, handle, pageIndex);
1475
+ }, [registry, checkId, handle, pageIndex]);
1327
1476
  }
1328
1477
  function useCompoundHandleRef(ref, opts) {
1329
- const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
1330
- const setIndexClamped = (0, import_react7.useCallback)(
1478
+ const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
1479
+ const bridgeRef = useCompoundHydrationBridgeRef();
1480
+ const setIndexClamped = (0, import_react9.useCallback)(
1331
1481
  (index) => {
1332
- const next = pageCount !== void 0 ? (0, import_core10.clampCompoundPageIndex)(index, pageCount) : Math.max(0, Math.floor(index));
1482
+ const next = pageCount !== void 0 ? (0, import_core11.clampCompoundPageIndex)(index, pageCount) : Math.max(0, Math.floor(index));
1333
1483
  setActivePageIndex(next);
1334
1484
  },
1335
1485
  [pageCount, setActivePageIndex]
1336
1486
  );
1337
- (0, import_react7.useImperativeHandle)(
1487
+ (0, import_react9.useImperativeHandle)(
1338
1488
  ref,
1339
1489
  () => ({
1340
- getScore: () => aggregateAssessmentScores(getHandles().values()).score,
1341
- getMaxScore: () => aggregateAssessmentScores(getHandles().values()).maxScore,
1342
- getAnswerGiven: () => aggregateAssessmentScores(getHandles().values()).allAnswered,
1490
+ getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
1491
+ getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
1492
+ getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
1493
+ answerPageIndex: activePageIndex
1494
+ }).allAnswered,
1343
1495
  resetTask: () => {
1344
- for (const handle of getHandles().values()) handle.resetTask();
1496
+ for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
1345
1497
  },
1346
1498
  showSolutions: () => {
1347
1499
  if (!opts.enableSolutionsButton) return;
1348
- for (const handle of getHandles().values()) handle.showSolutions();
1500
+ for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
1349
1501
  },
1350
1502
  getCurrentState: () => {
1351
1503
  const childStates = {};
1352
- for (const [checkId, handle] of getHandles()) {
1353
- if (handle.getCurrentState) {
1354
- childStates[checkId] = handle.getCurrentState();
1504
+ for (const [checkId, entry] of getRegisteredHandles()) {
1505
+ if (entry.handle.getCurrentState) {
1506
+ childStates[checkId] = entry.handle.getCurrentState();
1355
1507
  }
1356
1508
  }
1357
- return (0, import_core10.createCompoundResumeState)({ activePageIndex, childStates });
1509
+ return (0, import_core11.createCompoundResumeState)({ activePageIndex, childStates });
1358
1510
  },
1359
1511
  resume: (state) => {
1360
- setIndexClamped(state.activePageIndex);
1361
- resumeChildHandles(getHandles(), state.childStates);
1512
+ bridgeRef?.current?.notifyImperativeResume(state);
1362
1513
  }
1363
1514
  }),
1364
- [activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
1515
+ [activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
1365
1516
  );
1366
1517
  }
1367
1518
 
1368
1519
  // src/assessment/internal/useAssessmentHandleRegistration.ts
1369
1520
  function useAssessmentHandleRegistration(checkId, handle, ref) {
1370
- (0, import_react8.useImperativeHandle)(ref, () => handle, [handle]);
1521
+ (0, import_react10.useImperativeHandle)(ref, () => handle, [handle]);
1371
1522
  useRegisterAssessmentHandle(checkId, handle);
1372
1523
  }
1373
1524
 
1374
1525
  // src/assessment/internal/usePluginScoring.ts
1375
- var import_react9 = require("react");
1526
+ var import_react11 = require("react");
1376
1527
 
1377
1528
  // src/assessment/scoring.ts
1378
1529
  function resolvePassingThreshold(passingScore, maxScore) {
@@ -1400,7 +1551,7 @@ function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore)
1400
1551
  // src/assessment/internal/usePluginScoring.ts
1401
1552
  function usePluginScoring(checkId, lessonId) {
1402
1553
  const { plugins, config, session } = useLessonkit();
1403
- const getPluginScore = (0, import_react9.useCallback)(
1554
+ const getPluginScore = (0, import_react11.useCallback)(
1404
1555
  (response) => {
1405
1556
  const pluginCtx = buildPluginContext({
1406
1557
  courseId: config.courseId,
@@ -1412,11 +1563,11 @@ function usePluginScoring(checkId, lessonId) {
1412
1563
  },
1413
1564
  [checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
1414
1565
  );
1415
- const scoreResponse = (0, import_react9.useCallback)(
1566
+ const scoreResponse = (0, import_react11.useCallback)(
1416
1567
  (response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
1417
1568
  [getPluginScore]
1418
1569
  );
1419
- const isChoiceCorrect = (0, import_react9.useCallback)(
1570
+ const isChoiceCorrect = (0, import_react11.useCallback)(
1420
1571
  (choice, answer, custom, passingScore) => {
1421
1572
  if (!custom) return choice === answer;
1422
1573
  if (custom.passed !== void 0) return custom.passed;
@@ -1431,53 +1582,94 @@ function usePluginScoring(checkId, lessonId) {
1431
1582
  }
1432
1583
 
1433
1584
  // src/components/Quiz.tsx
1434
- var import_jsx_runtime4 = require("react/jsx-runtime");
1585
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1435
1586
  function QuizInner(props, ref) {
1436
1587
  const { enclosingLessonId } = props;
1437
- const checkId = (0, import_react10.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1588
+ const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1438
1589
  const quiz = useQuizState(enclosingLessonId);
1439
1590
  const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
1440
- const [selected, setSelected] = (0, import_react10.useState)(null);
1441
- const [selectionCorrect, setSelectionCorrect] = (0, import_react10.useState)(null);
1442
- const [quizPassed, setQuizPassed] = (0, import_react10.useState)(false);
1443
- const completedRef = (0, import_react10.useRef)(false);
1444
- const questionId = (0, import_react10.useId)();
1591
+ const [selected, setSelected] = (0, import_react12.useState)(null);
1592
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react12.useState)(null);
1593
+ const [quizPassed, setQuizPassed] = (0, import_react12.useState)(false);
1594
+ const [completedScore, setCompletedScore] = (0, import_react12.useState)(null);
1595
+ const [completedMaxScore, setCompletedMaxScore] = (0, import_react12.useState)(null);
1596
+ const completedRef = (0, import_react12.useRef)(false);
1597
+ const telemetryReplayedRef = (0, import_react12.useRef)(false);
1598
+ const questionId = (0, import_react12.useId)();
1445
1599
  const choicesKey = props.choices.join("\0");
1446
- (0, import_react10.useEffect)(() => {
1600
+ (0, import_react12.useEffect)(() => {
1447
1601
  completedRef.current = false;
1602
+ telemetryReplayedRef.current = false;
1448
1603
  setQuizPassed(false);
1449
1604
  setSelected(null);
1450
1605
  setSelectionCorrect(null);
1606
+ setCompletedScore(null);
1607
+ setCompletedMaxScore(null);
1451
1608
  }, [checkId, props.answer, props.question, choicesKey]);
1452
1609
  const passed = quizPassed;
1453
- const handle = (0, import_react10.useMemo)(
1610
+ const resolveScores = () => {
1611
+ const maxScore = completedMaxScore ?? 1;
1612
+ if (quizPassed) {
1613
+ return { score: completedScore ?? maxScore, maxScore };
1614
+ }
1615
+ if (selected !== null && selectionCorrect) {
1616
+ return { score: completedMaxScore ?? maxScore, maxScore };
1617
+ }
1618
+ return { score: 0, maxScore };
1619
+ };
1620
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1621
+ if (!nextPassed || telemetryReplayedRef.current) return;
1622
+ telemetryReplayedRef.current = true;
1623
+ if (nextSelected !== null) {
1624
+ quiz.answer({
1625
+ checkId,
1626
+ question: props.question,
1627
+ choice: nextSelected,
1628
+ correct: nextCorrect ?? false
1629
+ });
1630
+ }
1631
+ quiz.complete({
1632
+ checkId,
1633
+ score: nextScore,
1634
+ maxScore: nextMaxScore,
1635
+ passingScore: props.passingScore ?? nextMaxScore
1636
+ });
1637
+ };
1638
+ const handle = (0, import_react12.useMemo)(
1454
1639
  () => buildAssessmentHandle({
1455
1640
  checkId,
1456
- getScore: () => {
1457
- const maxScore = 1;
1458
- if (quizPassed && selected !== null) return maxScore;
1459
- if (selected === null) return 0;
1460
- return selectionCorrect ? maxScore : 0;
1461
- },
1462
- getMaxScore: () => 1,
1641
+ getScore: () => resolveScores().score,
1642
+ getMaxScore: () => resolveScores().maxScore,
1463
1643
  getAnswerGiven: () => selected !== null,
1464
1644
  resetTask: () => {
1465
1645
  completedRef.current = false;
1646
+ telemetryReplayedRef.current = false;
1466
1647
  setQuizPassed(false);
1467
1648
  setSelected(null);
1468
1649
  setSelectionCorrect(null);
1650
+ setCompletedScore(null);
1651
+ setCompletedMaxScore(null);
1469
1652
  },
1470
1653
  showSolutions: () => {
1471
1654
  },
1472
- getXAPIData: () => ({
1473
- checkId,
1474
- interactionType: "mcq",
1475
- response: selected ?? void 0,
1476
- correct: selectionCorrect ?? void 0,
1477
- score: quizPassed && selected !== null ? 1 : selected === null ? 0 : selectionCorrect ? 1 : 0,
1478
- maxScore: 1
1655
+ getXAPIData: () => {
1656
+ const { score, maxScore } = resolveScores();
1657
+ return {
1658
+ checkId,
1659
+ interactionType: "mcq",
1660
+ response: selected ?? void 0,
1661
+ correct: selectionCorrect ?? void 0,
1662
+ score,
1663
+ maxScore
1664
+ };
1665
+ },
1666
+ getCurrentState: () => ({
1667
+ selected,
1668
+ selectionCorrect,
1669
+ quizPassed,
1670
+ completedScore,
1671
+ completedMaxScore
1479
1672
  }),
1480
- getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
1481
1673
  resume: (state) => {
1482
1674
  const nextSelected = readStringField(state, "selected");
1483
1675
  if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
@@ -1485,21 +1677,47 @@ function QuizInner(props, ref) {
1485
1677
  if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1486
1678
  setSelectionCorrect(nextCorrect);
1487
1679
  }
1488
- readBooleanStateField(state, "quizPassed", (value) => {
1489
- setQuizPassed(value);
1490
- completedRef.current = value;
1491
- });
1680
+ const nextCompletedScore = readNumberField(state, "completedScore");
1681
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1682
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1683
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1684
+ const nextPassed = readBooleanField(state, "quizPassed");
1685
+ if (nextPassed === true || nextPassed === false) {
1686
+ setQuizPassed(nextPassed);
1687
+ completedRef.current = nextPassed;
1688
+ if (nextPassed) {
1689
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1690
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
1691
+ replayTelemetry(
1692
+ nextSelected ?? null,
1693
+ nextCorrect ?? null,
1694
+ nextPassed,
1695
+ score,
1696
+ maxScore
1697
+ );
1698
+ }
1699
+ }
1492
1700
  }
1493
1701
  }),
1494
- [checkId, quizPassed, selected, selectionCorrect]
1702
+ [
1703
+ checkId,
1704
+ completedMaxScore,
1705
+ completedScore,
1706
+ props.passingScore,
1707
+ props.question,
1708
+ quiz,
1709
+ quizPassed,
1710
+ selected,
1711
+ selectionCorrect
1712
+ ]
1495
1713
  );
1496
1714
  useAssessmentHandleRegistration(checkId, handle, ref);
1497
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1498
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { id: questionId, children: props.question }),
1499
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1500
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
1501
- props.choices.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "block" }, children: [
1502
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1715
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1716
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: questionId, children: props.question }),
1717
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1718
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
1719
+ props.choices.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { style: { display: "block" }, children: [
1720
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1503
1721
  "input",
1504
1722
  {
1505
1723
  type: "radio",
@@ -1524,9 +1742,12 @@ function QuizInner(props, ref) {
1524
1742
  completedRef.current = true;
1525
1743
  setQuizPassed(true);
1526
1744
  const maxScore = custom?.maxScore ?? 1;
1745
+ const score = custom?.score ?? maxScore;
1746
+ setCompletedScore(score);
1747
+ setCompletedMaxScore(maxScore);
1527
1748
  quiz.complete({
1528
1749
  checkId,
1529
- score: custom?.score ?? maxScore,
1750
+ score,
1530
1751
  maxScore,
1531
1752
  passingScore: props.passingScore ?? maxScore
1532
1753
  });
@@ -1537,15 +1758,15 @@ function QuizInner(props, ref) {
1537
1758
  c
1538
1759
  ] }, `${questionId}-${i}`))
1539
1760
  ] }),
1540
- selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1761
+ selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1541
1762
  ] });
1542
1763
  }
1543
- var QuizInnerForwarded = (0, import_react10.forwardRef)(QuizInner);
1544
- var Quiz = (0, import_react10.forwardRef)(function Quiz2(props, ref) {
1545
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1764
+ var QuizInnerForwarded = (0, import_react12.forwardRef)(QuizInner);
1765
+ var Quiz = (0, import_react12.forwardRef)(function Quiz2(props, ref) {
1766
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1546
1767
  });
1547
1768
  function KnowledgeCheck(props) {
1548
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1769
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1549
1770
  Quiz,
1550
1771
  {
1551
1772
  checkId: props.checkId,
@@ -1561,27 +1782,27 @@ function resetQuizWarningsForTests() {
1561
1782
  }
1562
1783
 
1563
1784
  // src/components.tsx
1564
- var import_jsx_runtime5 = require("react/jsx-runtime");
1785
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1565
1786
  function Course(props) {
1566
- const courseId = (0, import_react11.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1567
- const providerConfig = (0, import_react11.useMemo)(
1787
+ const courseId = (0, import_react13.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1788
+ const providerConfig = (0, import_react13.useMemo)(
1568
1789
  () => ({ ...props.config, courseId }),
1569
1790
  [props.config, courseId]
1570
1791
  );
1571
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": props.title, children: [
1572
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { children: props.title }),
1573
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: props.children })
1792
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": props.title, children: [
1793
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h1", { children: props.title }),
1794
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: props.children })
1574
1795
  ] }) });
1575
1796
  }
1576
1797
  function Lesson(props) {
1577
- const lessonId = (0, import_react11.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1798
+ const lessonId = (0, import_react13.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1578
1799
  const autoComplete = props.autoCompleteOnUnmount !== false;
1579
1800
  const { setActiveLesson, config } = useLessonkit();
1580
1801
  const { completeLesson } = useCompletion();
1581
- const lessonMountGenerationRef = (0, import_react11.useRef)(0);
1582
- const liveCourseIdRef = (0, import_react11.useRef)(config.courseId);
1802
+ const lessonMountGenerationRef = (0, import_react13.useRef)(0);
1803
+ const liveCourseIdRef = (0, import_react13.useRef)(config.courseId);
1583
1804
  liveCourseIdRef.current = config.courseId;
1584
- (0, import_react11.useEffect)(() => {
1805
+ (0, import_react13.useEffect)(() => {
1585
1806
  const unregister = registerLessonMount(lessonId);
1586
1807
  const generation = ++lessonMountGenerationRef.current;
1587
1808
  const mountedCourseId = config.courseId;
@@ -1606,37 +1827,37 @@ function Lesson(props) {
1606
1827
  });
1607
1828
  };
1608
1829
  }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1609
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("article", { "aria-label": props.title, children: [
1610
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { children: props.title }),
1611
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: props.children })
1830
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("article", { "aria-label": props.title, children: [
1831
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h2", { children: props.title }),
1832
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: props.children })
1612
1833
  ] }) });
1613
1834
  }
1614
1835
  function Scenario(props) {
1615
- const blockId = (0, import_react11.useMemo)(
1836
+ const blockId = (0, import_react13.useMemo)(
1616
1837
  () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1617
1838
  [props.blockId]
1618
1839
  );
1619
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1840
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1620
1841
  }
1621
1842
  function Reflection(props) {
1622
- const blockId = (0, import_react11.useMemo)(
1843
+ const blockId = (0, import_react13.useMemo)(
1623
1844
  () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1624
1845
  [props.blockId]
1625
1846
  );
1626
- const promptId = (0, import_react11.useId)();
1627
- const hintId = (0, import_react11.useId)();
1628
- const [internalValue, setInternalValue] = (0, import_react11.useState)("");
1847
+ const promptId = (0, import_react13.useId)();
1848
+ const hintId = (0, import_react13.useId)();
1849
+ const [internalValue, setInternalValue] = (0, import_react13.useState)("");
1629
1850
  const isControlled = props.value !== void 0;
1630
1851
  const value = isControlled ? props.value : internalValue;
1631
1852
  const handleChange = (event) => {
1632
1853
  if (!isControlled) setInternalValue(event.target.value);
1633
1854
  props.onChange?.(event.target.value);
1634
1855
  };
1635
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1636
- props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: promptId, children: props.prompt }) : null,
1637
- props.hint ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: hintId, style: import_accessibility2.visuallyHiddenStyle, children: props.hint }) : null,
1856
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1857
+ props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: promptId, children: props.prompt }) : null,
1858
+ props.hint ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: hintId, style: import_accessibility2.visuallyHiddenStyle, children: props.hint }) : null,
1638
1859
  props.children,
1639
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1860
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1640
1861
  "textarea",
1641
1862
  {
1642
1863
  value,
@@ -1654,7 +1875,7 @@ function ProgressTracker(props) {
1654
1875
  if (props.totalLessons != null) {
1655
1876
  const total = props.totalLessons;
1656
1877
  const displayed = Math.min(completed, total);
1657
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1878
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1658
1879
  "div",
1659
1880
  {
1660
1881
  role: "progressbar",
@@ -1662,7 +1883,7 @@ function ProgressTracker(props) {
1662
1883
  "aria-valuemax": total,
1663
1884
  "aria-valuenow": displayed,
1664
1885
  "aria-label": "Lessons completed",
1665
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
1886
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
1666
1887
  "Lessons completed: ",
1667
1888
  displayed,
1668
1889
  " of ",
@@ -1671,58 +1892,101 @@ function ProgressTracker(props) {
1671
1892
  }
1672
1893
  ) });
1673
1894
  }
1674
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
1895
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
1675
1896
  "Lessons completed: ",
1676
1897
  completed
1677
1898
  ] }) });
1678
1899
  }
1679
1900
 
1680
1901
  // src/blocks/TrueFalse.tsx
1681
- var import_react12 = __toESM(require("react"), 1);
1682
- var import_jsx_runtime6 = require("react/jsx-runtime");
1902
+ var import_react14 = __toESM(require("react"), 1);
1903
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1683
1904
  var INTERACTION = "trueFalse";
1684
1905
  function TrueFalseInner(props, ref) {
1685
1906
  const { enclosingLessonId } = props;
1686
- const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1907
+ const checkId = (0, import_react14.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1687
1908
  const assessment = useAssessmentState(enclosingLessonId);
1688
1909
  const { config } = useLessonkit();
1689
1910
  const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
1690
- const [selected, setSelected] = (0, import_react12.useState)(null);
1691
- const [selectionCorrect, setSelectionCorrect] = (0, import_react12.useState)(null);
1692
- const [showSolutions, setShowSolutions] = (0, import_react12.useState)(false);
1693
- const [passed, setPassed] = (0, import_react12.useState)(false);
1694
- const completedRef = (0, import_react12.useRef)(false);
1695
- const questionId = import_react12.default.useId();
1911
+ const [selected, setSelected] = (0, import_react14.useState)(null);
1912
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react14.useState)(null);
1913
+ const [showSolutions, setShowSolutions] = (0, import_react14.useState)(false);
1914
+ const [passed, setPassed] = (0, import_react14.useState)(false);
1915
+ const [completedScore, setCompletedScore] = (0, import_react14.useState)(null);
1916
+ const [completedMaxScore, setCompletedMaxScore] = (0, import_react14.useState)(null);
1917
+ const completedRef = (0, import_react14.useRef)(false);
1918
+ const telemetryReplayedRef = (0, import_react14.useRef)(false);
1919
+ const questionId = import_react14.default.useId();
1696
1920
  const reset = () => {
1697
1921
  completedRef.current = false;
1922
+ telemetryReplayedRef.current = false;
1698
1923
  setPassed(false);
1699
1924
  setSelected(null);
1700
1925
  setSelectionCorrect(null);
1701
1926
  setShowSolutions(false);
1927
+ setCompletedScore(null);
1928
+ setCompletedMaxScore(null);
1702
1929
  };
1703
- (0, import_react12.useEffect)(() => {
1930
+ (0, import_react14.useEffect)(() => {
1704
1931
  reset();
1705
1932
  }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1706
- const handle = (0, import_react12.useMemo)(
1933
+ const resolveScores = () => {
1934
+ const maxScore = completedMaxScore ?? 1;
1935
+ if (passed) {
1936
+ return { score: completedScore ?? maxScore, maxScore };
1937
+ }
1938
+ if (selectionCorrect) {
1939
+ return { score: completedMaxScore ?? maxScore, maxScore };
1940
+ }
1941
+ return { score: 0, maxScore };
1942
+ };
1943
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1944
+ if (!nextPassed || telemetryReplayedRef.current) return;
1945
+ telemetryReplayedRef.current = true;
1946
+ if (nextSelected !== null) {
1947
+ assessment.answer({
1948
+ checkId,
1949
+ interactionType: INTERACTION,
1950
+ question: props.question,
1951
+ response: nextSelected,
1952
+ correct: nextCorrect ?? false
1953
+ });
1954
+ }
1955
+ assessment.complete({
1956
+ checkId,
1957
+ interactionType: INTERACTION,
1958
+ score: nextScore,
1959
+ maxScore: nextMaxScore,
1960
+ passingScore: props.passingScore ?? nextMaxScore
1961
+ });
1962
+ };
1963
+ const handle = (0, import_react14.useMemo)(
1707
1964
  () => buildAssessmentHandle({
1708
1965
  checkId,
1709
- getScore: () => {
1710
- const maxScore = 1;
1711
- return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
1712
- },
1713
- getMaxScore: () => 1,
1966
+ getScore: () => resolveScores().score,
1967
+ getMaxScore: () => resolveScores().maxScore,
1714
1968
  getAnswerGiven: () => selected !== null,
1715
1969
  resetTask: reset,
1716
1970
  showSolutions: () => setShowSolutions(true),
1717
- getXAPIData: () => ({
1718
- checkId,
1719
- interactionType: INTERACTION,
1720
- response: selected ?? void 0,
1721
- correct: selected === props.answer,
1722
- score: passed ? 1 : selected === null ? 0 : selected === props.answer ? 1 : 0,
1723
- maxScore: 1
1971
+ getXAPIData: () => {
1972
+ const { score, maxScore } = resolveScores();
1973
+ return {
1974
+ checkId,
1975
+ interactionType: INTERACTION,
1976
+ response: selected ?? void 0,
1977
+ correct: selectionCorrect ?? void 0,
1978
+ score,
1979
+ maxScore
1980
+ };
1981
+ },
1982
+ getCurrentState: () => ({
1983
+ selected,
1984
+ selectionCorrect,
1985
+ passed,
1986
+ showSolutions,
1987
+ completedScore,
1988
+ completedMaxScore
1724
1989
  }),
1725
- getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
1726
1990
  resume: (state) => {
1727
1991
  const nextSelected = readBooleanField(state, "selected");
1728
1992
  if (nextSelected === true || nextSelected === false || nextSelected === null) {
@@ -1732,14 +1996,35 @@ function TrueFalseInner(props, ref) {
1732
1996
  if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1733
1997
  setSelectionCorrect(nextCorrect);
1734
1998
  }
1735
- readBooleanStateField(state, "passed", (value) => {
1736
- setPassed(value);
1737
- completedRef.current = value;
1738
- });
1999
+ const nextCompletedScore = readNumberField(state, "completedScore");
2000
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
2001
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
2002
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
2003
+ const nextPassed = readBooleanField(state, "passed");
2004
+ if (nextPassed === true || nextPassed === false) {
2005
+ setPassed(nextPassed);
2006
+ completedRef.current = nextPassed;
2007
+ if (nextPassed) {
2008
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
2009
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
2010
+ replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
2011
+ }
2012
+ }
1739
2013
  readBooleanStateField(state, "showSolutions", setShowSolutions);
1740
2014
  }
1741
2015
  }),
1742
- [checkId, passed, props.answer, selected, selectionCorrect, showSolutions]
2016
+ [
2017
+ assessment,
2018
+ checkId,
2019
+ completedMaxScore,
2020
+ completedScore,
2021
+ passed,
2022
+ props.passingScore,
2023
+ props.question,
2024
+ selected,
2025
+ selectionCorrect,
2026
+ showSolutions
2027
+ ]
1743
2028
  );
1744
2029
  useAssessmentHandleRegistration(checkId, handle, ref);
1745
2030
  const submit = (value) => {
@@ -1758,6 +2043,8 @@ function TrueFalseInner(props, ref) {
1758
2043
  if (scored.passed && !completedRef.current) {
1759
2044
  completedRef.current = true;
1760
2045
  setPassed(true);
2046
+ setCompletedScore(scored.score);
2047
+ setCompletedMaxScore(scored.maxScore);
1761
2048
  assessment.complete({
1762
2049
  checkId,
1763
2050
  interactionType: INTERACTION,
@@ -1768,12 +2055,12 @@ function TrueFalseInner(props, ref) {
1768
2055
  }
1769
2056
  };
1770
2057
  const reveal = showSolutions || passed && props.enableSolutionsButton;
1771
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1772
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: questionId, children: props.question }),
1773
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1774
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
1775
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
1776
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2058
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
2059
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { id: questionId, children: props.question }),
2060
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
2061
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
2062
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
2063
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1777
2064
  "input",
1778
2065
  {
1779
2066
  type: "radio",
@@ -1785,8 +2072,8 @@ function TrueFalseInner(props, ref) {
1785
2072
  ),
1786
2073
  "True"
1787
2074
  ] }),
1788
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { style: { display: "block" }, children: [
1789
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2075
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "block" }, children: [
2076
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1790
2077
  "input",
1791
2078
  {
1792
2079
  type: "radio",
@@ -1799,49 +2086,49 @@ function TrueFalseInner(props, ref) {
1799
2086
  "False"
1800
2087
  ] })
1801
2088
  ] }),
1802
- reveal ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { children: [
2089
+ reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
1803
2090
  "Correct answer: ",
1804
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: props.answer ? "True" : "False" })
2091
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("strong", { children: props.answer ? "True" : "False" })
1805
2092
  ] }) : null,
1806
- selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1807
- props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1808
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2093
+ selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
2094
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2095
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1809
2096
  ] });
1810
2097
  }
1811
- var TrueFalseInnerForwarded = (0, import_react12.forwardRef)(TrueFalseInner);
1812
- var TrueFalse = (0, import_react12.forwardRef)(function TrueFalse2(props, ref) {
1813
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2098
+ var TrueFalseInnerForwarded = (0, import_react14.forwardRef)(TrueFalseInner);
2099
+ var TrueFalse = (0, import_react14.forwardRef)(function TrueFalse2(props, ref) {
2100
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1814
2101
  });
1815
2102
 
1816
2103
  // src/blocks/MarkTheWords.tsx
1817
- var import_react13 = __toESM(require("react"), 1);
1818
- var import_jsx_runtime7 = require("react/jsx-runtime");
2104
+ var import_react15 = __toESM(require("react"), 1);
2105
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1819
2106
  var INTERACTION2 = "markTheWords";
1820
2107
  function tokenize(text) {
1821
2108
  return text.split(/(\s+)/).filter((t) => t.length > 0);
1822
2109
  }
1823
2110
  function MarkTheWordsInner(props, ref) {
1824
- const checkId = (0, import_react13.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2111
+ const checkId = (0, import_react15.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1825
2112
  const assessment = useAssessmentState(props.enclosingLessonId);
1826
- const tokens = (0, import_react13.useMemo)(() => tokenize(props.text), [props.text]);
1827
- const correctSet = (0, import_react13.useMemo)(
2113
+ const tokens = (0, import_react15.useMemo)(() => tokenize(props.text), [props.text]);
2114
+ const correctSet = (0, import_react15.useMemo)(
1828
2115
  () => new Set(props.correctWords.map((w) => w.toLowerCase())),
1829
2116
  [props.correctWords]
1830
2117
  );
1831
- const [marked, setMarked] = (0, import_react13.useState)(() => /* @__PURE__ */ new Set());
1832
- const [passed, setPassed] = (0, import_react13.useState)(false);
1833
- const [showSolutions, setShowSolutions] = (0, import_react13.useState)(false);
1834
- const completedRef = (0, import_react13.useRef)(false);
2118
+ const [marked, setMarked] = (0, import_react15.useState)(() => /* @__PURE__ */ new Set());
2119
+ const [passed, setPassed] = (0, import_react15.useState)(false);
2120
+ const [showSolutions, setShowSolutions] = (0, import_react15.useState)(false);
2121
+ const completedRef = (0, import_react15.useRef)(false);
1835
2122
  const reset = () => {
1836
2123
  completedRef.current = false;
1837
2124
  setPassed(false);
1838
2125
  setMarked(/* @__PURE__ */ new Set());
1839
2126
  setShowSolutions(false);
1840
2127
  };
1841
- (0, import_react13.useEffect)(() => {
2128
+ (0, import_react15.useEffect)(() => {
1842
2129
  reset();
1843
2130
  }, [checkId, props.text, props.correctWords.join("\0")]);
1844
- const selectableIndices = (0, import_react13.useMemo)(() => {
2131
+ const selectableIndices = (0, import_react15.useMemo)(() => {
1845
2132
  const indices = [];
1846
2133
  tokens.forEach((t, i) => {
1847
2134
  if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
@@ -1853,7 +2140,7 @@ function MarkTheWordsInner(props, ref) {
1853
2140
  const maxScore = selectableIndices.length;
1854
2141
  const score = allMarked ? maxScore : marked.size;
1855
2142
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1856
- const handle = (0, import_react13.useMemo)(
2143
+ const handle = (0, import_react15.useMemo)(
1857
2144
  () => buildAssessmentHandle({
1858
2145
  checkId,
1859
2146
  getScore: () => score,
@@ -1892,7 +2179,7 @@ function MarkTheWordsInner(props, ref) {
1892
2179
  return next;
1893
2180
  });
1894
2181
  };
1895
- (0, import_react13.useEffect)(() => {
2182
+ (0, import_react15.useEffect)(() => {
1896
2183
  if (!hasTargets) {
1897
2184
  if (isDevEnvironment4()) {
1898
2185
  console.warn(
@@ -1910,7 +2197,7 @@ function MarkTheWordsInner(props, ref) {
1910
2197
  interactionType: INTERACTION2,
1911
2198
  question: props.text,
1912
2199
  response: [...marked].map((i) => tokens[i]),
1913
- correct: true
2200
+ correct: passedThreshold
1914
2201
  });
1915
2202
  assessment.complete({
1916
2203
  checkId,
@@ -1932,20 +2219,20 @@ function MarkTheWordsInner(props, ref) {
1932
2219
  score,
1933
2220
  tokens
1934
2221
  ]);
1935
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1936
- !hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { role: "alert", children: [
2222
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
2223
+ !hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("p", { role: "alert", children: [
1937
2224
  "No words in this sentence match ",
1938
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { children: "correctWords" }),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: "correctWords" }),
1939
2226
  ". Check spelling and capitalization in the source text."
1940
2227
  ] }) : null,
1941
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
1942
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
2228
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
2229
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1943
2230
  const isWord = !/^\s+$/.test(token);
1944
2231
  const isTarget = isWord && correctSet.has(token.toLowerCase());
1945
- if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react13.default.Fragment, { children: token }, i);
2232
+ if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react15.default.Fragment, { children: token }, i);
1946
2233
  const selected = marked.has(i);
1947
2234
  const solution = showSolutions || passed && props.enableSolutionsButton;
1948
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2235
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1949
2236
  "button",
1950
2237
  {
1951
2238
  type: "button",
@@ -1963,18 +2250,18 @@ function MarkTheWordsInner(props, ref) {
1963
2250
  i
1964
2251
  );
1965
2252
  }) }),
1966
- allMarked ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
1967
- props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1968
- props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2253
+ allMarked ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
2254
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2255
+ props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1969
2256
  ] });
1970
2257
  }
1971
- var MarkTheWordsInnerForwarded = (0, import_react13.forwardRef)(MarkTheWordsInner);
1972
- var MarkTheWords = (0, import_react13.forwardRef)(function MarkTheWords2(props, ref) {
1973
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2258
+ var MarkTheWordsInnerForwarded = (0, import_react15.forwardRef)(MarkTheWordsInner);
2259
+ var MarkTheWords = (0, import_react15.forwardRef)(function MarkTheWords2(props, ref) {
2260
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1974
2261
  });
1975
2262
 
1976
2263
  // src/blocks/FillInTheBlanks.tsx
1977
- var import_react14 = __toESM(require("react"), 1);
2264
+ var import_react16 = __toESM(require("react"), 1);
1978
2265
 
1979
2266
  // src/assessment/internal/parseStarDelimitedTemplate.ts
1980
2267
  function parseStarDelimitedTemplate(template, idPrefix) {
@@ -1995,7 +2282,7 @@ function parseStarDelimitedTemplate(template, idPrefix) {
1995
2282
  }
1996
2283
 
1997
2284
  // src/blocks/FillInTheBlanks.tsx
1998
- var import_jsx_runtime8 = require("react/jsx-runtime");
2285
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1999
2286
  var INTERACTION3 = "fillInBlanks";
2000
2287
  function parseTemplate(template) {
2001
2288
  const { parts, values } = parseStarDelimitedTemplate(template, "blank");
@@ -2005,25 +2292,31 @@ function parseTemplate(template) {
2005
2292
  };
2006
2293
  }
2007
2294
  function FillInTheBlanksInner(props, ref) {
2008
- const checkId = (0, import_react14.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2295
+ const checkId = (0, import_react16.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2009
2296
  const assessment = useAssessmentState(props.enclosingLessonId);
2010
- const parsed = (0, import_react14.useMemo)(() => parseTemplate(props.template), [props.template]);
2297
+ const parsed = (0, import_react16.useMemo)(() => parseTemplate(props.template), [props.template]);
2011
2298
  const blanks = props.blanks ?? parsed.blanks;
2012
- const [values, setValues] = (0, import_react14.useState)(
2299
+ const [values, setValues] = (0, import_react16.useState)(
2013
2300
  () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
2014
2301
  );
2015
- const [passed, setPassed] = (0, import_react14.useState)(false);
2016
- const [showSolutions, setShowSolutions] = (0, import_react14.useState)(false);
2017
- const completedRef = (0, import_react14.useRef)(false);
2018
- const answeredRef = (0, import_react14.useRef)(false);
2302
+ const [passed, setPassed] = (0, import_react16.useState)(false);
2303
+ const [showSolutions, setShowSolutions] = (0, import_react16.useState)(false);
2304
+ const [submitted, setSubmitted] = (0, import_react16.useState)(false);
2305
+ const completedRef = (0, import_react16.useRef)(false);
2306
+ const answeredRef = (0, import_react16.useRef)(false);
2307
+ const checkSnapshotRef = (0, import_react16.useRef)(null);
2308
+ const telemetryReplayedRef = (0, import_react16.useRef)(false);
2019
2309
  const reset = () => {
2020
2310
  completedRef.current = false;
2021
2311
  answeredRef.current = false;
2312
+ checkSnapshotRef.current = null;
2313
+ telemetryReplayedRef.current = false;
2022
2314
  setPassed(false);
2023
2315
  setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
2024
2316
  setShowSolutions(false);
2317
+ setSubmitted(false);
2025
2318
  };
2026
- (0, import_react14.useEffect)(() => {
2319
+ (0, import_react16.useEffect)(() => {
2027
2320
  reset();
2028
2321
  }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
2029
2322
  const hasBlanks = blanks.length > 0;
@@ -2034,7 +2327,32 @@ function FillInTheBlanksInner(props, ref) {
2034
2327
  });
2035
2328
  const maxScore = blanks.length;
2036
2329
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2037
- const handle = (0, import_react14.useMemo)(
2330
+ const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2331
+ if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2332
+ telemetryReplayedRef.current = true;
2333
+ const nextPassedThreshold = meetsPassingThreshold(
2334
+ nextScore,
2335
+ nextMaxScore || 1,
2336
+ props.passingScore
2337
+ );
2338
+ assessment.answer({
2339
+ checkId,
2340
+ interactionType: INTERACTION3,
2341
+ question: props.template,
2342
+ response: nextValues,
2343
+ correct: nextPassedThreshold
2344
+ });
2345
+ if (nextPassed || nextPassedThreshold) {
2346
+ assessment.complete({
2347
+ checkId,
2348
+ interactionType: INTERACTION3,
2349
+ score: nextScore,
2350
+ maxScore: nextMaxScore,
2351
+ passingScore: props.passingScore ?? nextMaxScore
2352
+ });
2353
+ }
2354
+ };
2355
+ const handle = (0, import_react16.useMemo)(
2038
2356
  () => buildAssessmentHandle({
2039
2357
  checkId,
2040
2358
  getScore: () => score,
@@ -2050,19 +2368,36 @@ function FillInTheBlanksInner(props, ref) {
2050
2368
  score,
2051
2369
  maxScore: maxScore || 1
2052
2370
  }),
2053
- getCurrentState: () => ({ values, passed, showSolutions }),
2371
+ getCurrentState: () => ({ values, passed, showSolutions, submitted }),
2054
2372
  resume: (state) => {
2055
2373
  const raw = state.values;
2056
- if (raw && typeof raw === "object") setValues({ ...raw });
2374
+ let nextValues = values;
2375
+ if (raw && typeof raw === "object") {
2376
+ nextValues = { ...raw };
2377
+ setValues(nextValues);
2378
+ }
2379
+ let nextPassed = passed;
2380
+ let nextSubmitted = submitted;
2057
2381
  readBooleanStateField(state, "passed", (value) => {
2382
+ nextPassed = value;
2058
2383
  setPassed(value);
2059
2384
  completedRef.current = value;
2060
2385
  answeredRef.current = value;
2061
2386
  });
2062
2387
  readBooleanStateField(state, "showSolutions", setShowSolutions);
2388
+ readBooleanStateField(state, "submitted", (value) => {
2389
+ nextSubmitted = value;
2390
+ setSubmitted(value);
2391
+ if (value) answeredRef.current = true;
2392
+ });
2393
+ let nextScore = 0;
2394
+ blanks.forEach((b) => {
2395
+ if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
2396
+ });
2397
+ replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
2063
2398
  }
2064
2399
  }),
2065
- [allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
2400
+ [allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
2066
2401
  );
2067
2402
  useAssessmentHandleRegistration(checkId, handle, ref);
2068
2403
  const check = () => {
@@ -2073,16 +2408,19 @@ function FillInTheBlanksInner(props, ref) {
2073
2408
  return;
2074
2409
  }
2075
2410
  if (!allFilled) return;
2076
- if (!answeredRef.current) {
2077
- answeredRef.current = true;
2078
- assessment.answer({
2079
- checkId,
2080
- interactionType: INTERACTION3,
2081
- question: props.template,
2082
- response: values,
2083
- correct: passedThreshold
2084
- });
2085
- }
2411
+ if (passed) return;
2412
+ const snapshot = JSON.stringify(values);
2413
+ if (checkSnapshotRef.current === snapshot) return;
2414
+ checkSnapshotRef.current = snapshot;
2415
+ answeredRef.current = true;
2416
+ setSubmitted(true);
2417
+ assessment.answer({
2418
+ checkId,
2419
+ interactionType: INTERACTION3,
2420
+ question: props.template,
2421
+ response: values,
2422
+ correct: passedThreshold
2423
+ });
2086
2424
  if (passedThreshold && !completedRef.current) {
2087
2425
  completedRef.current = true;
2088
2426
  setPassed(true);
@@ -2095,20 +2433,24 @@ function FillInTheBlanksInner(props, ref) {
2095
2433
  });
2096
2434
  }
2097
2435
  };
2098
- (0, import_react14.useEffect)(() => {
2099
- if (!allFilled) answeredRef.current = false;
2436
+ (0, import_react16.useEffect)(() => {
2437
+ if (!allFilled) {
2438
+ answeredRef.current = false;
2439
+ checkSnapshotRef.current = null;
2440
+ setSubmitted(false);
2441
+ }
2100
2442
  }, [allFilled]);
2101
- (0, import_react14.useEffect)(() => {
2102
- if (props.autoCheck && allFilled) check();
2103
- }, [allFilled, props.autoCheck, values, passedThreshold]);
2443
+ (0, import_react16.useEffect)(() => {
2444
+ if (props.autoCheck && allFilled && !passed) check();
2445
+ }, [allFilled, props.autoCheck, values, passedThreshold, passed]);
2104
2446
  const reveal = showSolutions || passed && props.enableSolutionsButton;
2105
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2106
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: parsed.parts.map((part, i) => {
2447
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2448
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: parsed.parts.map((part, i) => {
2107
2449
  const blank = blanks.find((b) => b.id === part);
2108
- if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react14.default.Fragment, { children: part }, i);
2109
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
2110
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
2111
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2450
+ if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react16.default.Fragment, { children: part }, i);
2451
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
2452
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
2453
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2112
2454
  "input",
2113
2455
  {
2114
2456
  type: "text",
@@ -2124,49 +2466,55 @@ function FillInTheBlanksInner(props, ref) {
2124
2466
  )
2125
2467
  ] }, blank.id);
2126
2468
  }) }),
2127
- !props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2128
- !hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2129
- allFilled ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2130
- props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2131
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2469
+ !props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2470
+ !hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2471
+ submitted ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2472
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2473
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2132
2474
  ] });
2133
2475
  }
2134
- var FillInTheBlanksInnerForwarded = (0, import_react14.forwardRef)(FillInTheBlanksInner);
2135
- var FillInTheBlanks = (0, import_react14.forwardRef)(
2476
+ var FillInTheBlanksInnerForwarded = (0, import_react16.forwardRef)(FillInTheBlanksInner);
2477
+ var FillInTheBlanks = (0, import_react16.forwardRef)(
2136
2478
  function FillInTheBlanks2(props, ref) {
2137
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2479
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2138
2480
  }
2139
2481
  );
2140
2482
 
2141
2483
  // src/blocks/DragTheWords.tsx
2142
- var import_react15 = __toESM(require("react"), 1);
2143
- var import_jsx_runtime9 = require("react/jsx-runtime");
2484
+ var import_react17 = __toESM(require("react"), 1);
2485
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2144
2486
  var INTERACTION4 = "dragTheWords";
2145
2487
  function parseZones(template) {
2146
2488
  const { parts, values } = parseStarDelimitedTemplate(template, "zone");
2147
2489
  return { parts, answers: values };
2148
2490
  }
2149
2491
  function DragTheWordsInner(props, ref) {
2150
- const checkId = (0, import_react15.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2492
+ const checkId = (0, import_react17.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2151
2493
  const assessment = useAssessmentState(props.enclosingLessonId);
2152
- const { parts, answers } = (0, import_react15.useMemo)(() => parseZones(props.template), [props.template]);
2153
- const [zones, setZones] = (0, import_react15.useState)(
2494
+ const { parts, answers } = (0, import_react17.useMemo)(() => parseZones(props.template), [props.template]);
2495
+ const [zones, setZones] = (0, import_react17.useState)(
2154
2496
  () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
2155
2497
  );
2156
- const [pool, setPool] = (0, import_react15.useState)(() => [...props.words]);
2157
- const [keyboardWord, setKeyboardWord] = (0, import_react15.useState)(null);
2158
- const [passed, setPassed] = (0, import_react15.useState)(false);
2159
- const completedRef = (0, import_react15.useRef)(false);
2160
- const answeredRef = (0, import_react15.useRef)(false);
2498
+ const [pool, setPool] = (0, import_react17.useState)(() => [...props.words]);
2499
+ const [keyboardWord, setKeyboardWord] = (0, import_react17.useState)(null);
2500
+ const [passed, setPassed] = (0, import_react17.useState)(false);
2501
+ const [submitted, setSubmitted] = (0, import_react17.useState)(false);
2502
+ const completedRef = (0, import_react17.useRef)(false);
2503
+ const answeredRef = (0, import_react17.useRef)(false);
2504
+ const checkSnapshotRef = (0, import_react17.useRef)(null);
2505
+ const telemetryReplayedRef = (0, import_react17.useRef)(false);
2161
2506
  const reset = () => {
2162
2507
  completedRef.current = false;
2163
2508
  answeredRef.current = false;
2509
+ checkSnapshotRef.current = null;
2510
+ telemetryReplayedRef.current = false;
2164
2511
  setPassed(false);
2512
+ setSubmitted(false);
2165
2513
  setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
2166
2514
  setPool([...props.words]);
2167
2515
  setKeyboardWord(null);
2168
2516
  };
2169
- (0, import_react15.useEffect)(() => {
2517
+ (0, import_react17.useEffect)(() => {
2170
2518
  reset();
2171
2519
  }, [checkId, props.template, props.words.join("\0")]);
2172
2520
  const hasZones = answers.length > 0;
@@ -2177,14 +2525,39 @@ function DragTheWordsInner(props, ref) {
2177
2525
  });
2178
2526
  const maxScore = answers.length;
2179
2527
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2180
- const handle = (0, import_react15.useMemo)(
2181
- () => buildAssessmentHandle({
2528
+ const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2529
+ if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2530
+ telemetryReplayedRef.current = true;
2531
+ const nextPassedThreshold = meetsPassingThreshold(
2532
+ nextScore,
2533
+ nextMaxScore || 1,
2534
+ props.passingScore
2535
+ );
2536
+ assessment.answer({
2182
2537
  checkId,
2183
- getScore: () => score,
2184
- getMaxScore: () => maxScore || 1,
2185
- getAnswerGiven: () => allFilled,
2186
- resetTask: reset,
2187
- showSolutions: () => {
2538
+ interactionType: INTERACTION4,
2539
+ question: props.template,
2540
+ response: nextZones,
2541
+ correct: nextPassedThreshold
2542
+ });
2543
+ if (nextPassed || nextPassedThreshold) {
2544
+ assessment.complete({
2545
+ checkId,
2546
+ interactionType: INTERACTION4,
2547
+ score: nextScore,
2548
+ maxScore: nextMaxScore,
2549
+ passingScore: props.passingScore ?? nextMaxScore
2550
+ });
2551
+ }
2552
+ };
2553
+ const handle = (0, import_react17.useMemo)(
2554
+ () => buildAssessmentHandle({
2555
+ checkId,
2556
+ getScore: () => score,
2557
+ getMaxScore: () => maxScore || 1,
2558
+ getAnswerGiven: () => allFilled,
2559
+ resetTask: reset,
2560
+ showSolutions: () => {
2188
2561
  },
2189
2562
  getXAPIData: () => ({
2190
2563
  checkId,
@@ -2194,21 +2567,38 @@ function DragTheWordsInner(props, ref) {
2194
2567
  score,
2195
2568
  maxScore: maxScore || 1
2196
2569
  }),
2197
- getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
2570
+ getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
2198
2571
  resume: (state) => {
2199
2572
  const rawZones = state.zones;
2200
- if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
2573
+ let nextZones = zones;
2574
+ if (rawZones && typeof rawZones === "object") {
2575
+ nextZones = { ...rawZones };
2576
+ setZones(nextZones);
2577
+ }
2201
2578
  if (Array.isArray(state.pool)) setPool([...state.pool]);
2579
+ let nextPassed = passed;
2580
+ let nextSubmitted = submitted;
2202
2581
  readBooleanStateField(state, "passed", (value) => {
2582
+ nextPassed = value;
2203
2583
  setPassed(value);
2204
2584
  completedRef.current = value;
2205
2585
  answeredRef.current = value;
2206
2586
  });
2587
+ readBooleanStateField(state, "submitted", (value) => {
2588
+ nextSubmitted = value;
2589
+ setSubmitted(value);
2590
+ if (value) answeredRef.current = true;
2591
+ });
2207
2592
  const kw = state.keyboardWord;
2208
2593
  if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2594
+ let nextScore = 0;
2595
+ answers.forEach((ans, i) => {
2596
+ if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
2597
+ });
2598
+ replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
2209
2599
  }
2210
2600
  }),
2211
- [allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
2601
+ [allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
2212
2602
  );
2213
2603
  useAssessmentHandleRegistration(checkId, handle, ref);
2214
2604
  const placeInZone = (zoneId, word) => {
@@ -2238,16 +2628,19 @@ function DragTheWordsInner(props, ref) {
2238
2628
  return;
2239
2629
  }
2240
2630
  if (!allFilled) return;
2241
- if (!answeredRef.current) {
2242
- answeredRef.current = true;
2243
- assessment.answer({
2244
- checkId,
2245
- interactionType: INTERACTION4,
2246
- question: props.template,
2247
- response: zones,
2248
- correct: passedThreshold
2249
- });
2250
- }
2631
+ if (passed) return;
2632
+ const snapshot = JSON.stringify(zones);
2633
+ if (checkSnapshotRef.current === snapshot) return;
2634
+ checkSnapshotRef.current = snapshot;
2635
+ answeredRef.current = true;
2636
+ setSubmitted(true);
2637
+ assessment.answer({
2638
+ checkId,
2639
+ interactionType: INTERACTION4,
2640
+ question: props.template,
2641
+ response: zones,
2642
+ correct: passedThreshold
2643
+ });
2251
2644
  if (passedThreshold && !completedRef.current) {
2252
2645
  completedRef.current = true;
2253
2646
  setPassed(true);
@@ -2260,15 +2653,19 @@ function DragTheWordsInner(props, ref) {
2260
2653
  });
2261
2654
  }
2262
2655
  };
2263
- (0, import_react15.useEffect)(() => {
2264
- if (!allFilled) answeredRef.current = false;
2656
+ (0, import_react17.useEffect)(() => {
2657
+ if (!allFilled) {
2658
+ answeredRef.current = false;
2659
+ checkSnapshotRef.current = null;
2660
+ setSubmitted(false);
2661
+ }
2265
2662
  }, [allFilled]);
2266
- (0, import_react15.useEffect)(() => {
2267
- if (props.autoCheck && allFilled) check();
2268
- }, [allFilled, props.autoCheck, zones, passedThreshold]);
2269
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2270
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2271
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2663
+ (0, import_react17.useEffect)(() => {
2664
+ if (props.autoCheck && allFilled && !passed) check();
2665
+ }, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
2666
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2667
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2668
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2272
2669
  "button",
2273
2670
  {
2274
2671
  type: "button",
@@ -2282,9 +2679,9 @@ function DragTheWordsInner(props, ref) {
2282
2679
  },
2283
2680
  word
2284
2681
  )) }),
2285
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: parts.map((part, i) => {
2286
- if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react15.default.Fragment, { children: part }, i);
2287
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2682
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: parts.map((part, i) => {
2683
+ if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react17.default.Fragment, { children: part }, i);
2684
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2288
2685
  "span",
2289
2686
  {
2290
2687
  role: "button",
@@ -2308,53 +2705,56 @@ function DragTheWordsInner(props, ref) {
2308
2705
  part
2309
2706
  );
2310
2707
  }) }),
2311
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2312
- !hasZones ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2313
- allFilled ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2708
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2709
+ !hasZones ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2710
+ submitted ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2314
2711
  ] });
2315
2712
  }
2316
- var DragTheWordsInnerForwarded = (0, import_react15.forwardRef)(DragTheWordsInner);
2317
- var DragTheWords = (0, import_react15.forwardRef)(function DragTheWords2(props, ref) {
2318
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2713
+ var DragTheWordsInnerForwarded = (0, import_react17.forwardRef)(DragTheWordsInner);
2714
+ var DragTheWords = (0, import_react17.forwardRef)(function DragTheWords2(props, ref) {
2715
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2319
2716
  });
2320
2717
 
2321
2718
  // src/blocks/DragAndDrop.tsx
2322
- var import_react16 = require("react");
2323
- var import_jsx_runtime10 = require("react/jsx-runtime");
2719
+ var import_react18 = require("react");
2720
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2324
2721
  var INTERACTION5 = "dragAndDrop";
2325
2722
  function DragAndDropInner(props, ref) {
2326
- const checkId = (0, import_react16.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2723
+ const checkId = (0, import_react18.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2327
2724
  const assessment = useAssessmentState(props.enclosingLessonId);
2328
- const [assignments, setAssignments] = (0, import_react16.useState)(
2725
+ const [assignments, setAssignments] = (0, import_react18.useState)(
2329
2726
  () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2330
2727
  );
2331
- const [pool, setPool] = (0, import_react16.useState)(() => props.items.map((i) => i.id));
2332
- const [keyboardItem, setKeyboardItem] = (0, import_react16.useState)(null);
2333
- const [passed, setPassed] = (0, import_react16.useState)(false);
2334
- const completedRef = (0, import_react16.useRef)(false);
2728
+ const [pool, setPool] = (0, import_react18.useState)(() => props.items.map((i) => i.id));
2729
+ const [keyboardItem, setKeyboardItem] = (0, import_react18.useState)(null);
2730
+ const [passed, setPassed] = (0, import_react18.useState)(false);
2731
+ const [checked, setChecked] = (0, import_react18.useState)(false);
2732
+ const completedRef = (0, import_react18.useRef)(false);
2335
2733
  const reset = () => {
2336
2734
  completedRef.current = false;
2337
2735
  setPassed(false);
2736
+ setChecked(false);
2338
2737
  setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2339
2738
  setPool(props.items.map((i) => i.id));
2340
2739
  setKeyboardItem(null);
2341
2740
  };
2342
- (0, import_react16.useEffect)(() => {
2741
+ (0, import_react18.useEffect)(() => {
2343
2742
  reset();
2344
2743
  }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2345
- const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2346
- const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
2347
- const handle = (0, import_react16.useMemo)(() => {
2348
- const maxScore = props.targets.length || 1;
2349
- let score = 0;
2350
- props.targets.forEach((t) => {
2351
- if (assignments[t.id] === t.accepts) score += 1;
2352
- });
2744
+ const hasTargets = props.targets.length > 0;
2745
+ const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2746
+ let score = 0;
2747
+ props.targets.forEach((t) => {
2748
+ if (assignments[t.id] === t.accepts) score += 1;
2749
+ });
2750
+ const maxScore = props.targets.length || 1;
2751
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
2752
+ const handle = (0, import_react18.useMemo)(() => {
2353
2753
  return buildAssessmentHandle({
2354
2754
  checkId,
2355
2755
  getScore: () => score,
2356
2756
  getMaxScore: () => maxScore,
2357
- getAnswerGiven: () => allFilled,
2757
+ getAnswerGiven: () => hasTargets && allFilled,
2358
2758
  resetTask: reset,
2359
2759
  showSolutions: () => {
2360
2760
  },
@@ -2362,11 +2762,11 @@ function DragAndDropInner(props, ref) {
2362
2762
  checkId,
2363
2763
  interactionType: INTERACTION5,
2364
2764
  response: assignments,
2365
- correct: allCorrect,
2765
+ correct: passedThreshold,
2366
2766
  score,
2367
2767
  maxScore
2368
2768
  }),
2369
- getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
2769
+ getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
2370
2770
  resume: (state) => {
2371
2771
  const rawAssignments = state.assignments;
2372
2772
  if (rawAssignments && typeof rawAssignments === "object") {
@@ -2377,14 +2777,16 @@ function DragAndDropInner(props, ref) {
2377
2777
  setPassed(value);
2378
2778
  completedRef.current = value;
2379
2779
  });
2780
+ readBooleanStateField(state, "checked", setChecked);
2380
2781
  const item = state.keyboardItem;
2381
2782
  if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
2382
2783
  }
2383
2784
  });
2384
- }, [allCorrect, allFilled, assignments, checkId, keyboardItem, passed, pool, props.targets]);
2785
+ }, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
2385
2786
  useAssessmentHandleRegistration(checkId, handle, ref);
2386
2787
  const place = (targetId, itemId) => {
2387
2788
  if (passed && !props.enableRetry) return;
2789
+ setChecked(false);
2388
2790
  const prev = assignments[targetId];
2389
2791
  setAssignments((a) => ({ ...a, [targetId]: itemId }));
2390
2792
  setPool((p) => {
@@ -2396,29 +2798,31 @@ function DragAndDropInner(props, ref) {
2396
2798
  };
2397
2799
  const check = () => {
2398
2800
  if (!allFilled) return;
2801
+ setChecked(true);
2399
2802
  assessment.answer({
2400
2803
  checkId,
2401
2804
  interactionType: INTERACTION5,
2402
2805
  response: assignments,
2403
- correct: allCorrect
2806
+ correct: passedThreshold
2404
2807
  });
2405
- if (allCorrect && !completedRef.current) {
2808
+ if (passedThreshold && !completedRef.current) {
2406
2809
  completedRef.current = true;
2407
2810
  setPassed(true);
2408
2811
  assessment.complete({
2409
2812
  checkId,
2410
2813
  interactionType: INTERACTION5,
2411
- score: props.targets.length,
2412
- maxScore: props.targets.length,
2413
- passingScore: props.passingScore ?? props.targets.length
2814
+ score,
2815
+ maxScore,
2816
+ passingScore: props.passingScore ?? maxScore
2414
2817
  });
2415
2818
  }
2416
2819
  };
2417
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2418
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2419
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
2820
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2821
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2822
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
2420
2823
  const item = props.items.find((i) => i.id === id);
2421
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2824
+ if (!item) return [];
2825
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2422
2826
  "button",
2423
2827
  {
2424
2828
  type: "button",
@@ -2433,13 +2837,13 @@ function DragAndDropInner(props, ref) {
2433
2837
  id
2434
2838
  );
2435
2839
  }) }),
2436
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { children: props.targets.map((target) => {
2840
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { children: props.targets.map((target) => {
2437
2841
  const assigned = assignments[target.id];
2438
2842
  const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2439
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { children: [
2440
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: target.label }),
2843
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { children: [
2844
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: target.label }),
2441
2845
  " ",
2442
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2846
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2443
2847
  "span",
2444
2848
  {
2445
2849
  role: "button",
@@ -2466,30 +2870,31 @@ function DragAndDropInner(props, ref) {
2466
2870
  )
2467
2871
  ] }, target.id);
2468
2872
  }) }),
2469
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2470
- allFilled ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
2873
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
2874
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
2471
2875
  ] });
2472
2876
  }
2473
- var DragAndDropInnerForwarded = (0, import_react16.forwardRef)(DragAndDropInner);
2474
- var DragAndDrop = (0, import_react16.forwardRef)(function DragAndDrop2(props, ref) {
2475
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2877
+ var DragAndDropInnerForwarded = (0, import_react18.forwardRef)(DragAndDropInner);
2878
+ var DragAndDrop = (0, import_react18.forwardRef)(function DragAndDrop2(props, ref) {
2879
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2476
2880
  });
2477
2881
 
2478
2882
  // src/blocks/AssessmentSequence.tsx
2479
- var import_react22 = __toESM(require("react"), 1);
2883
+ var import_react24 = __toESM(require("react"), 1);
2884
+ var import_core17 = require("@lessonkit/core");
2480
2885
 
2481
2886
  // src/compound/useCompoundShell.ts
2482
- var import_react20 = require("react");
2483
- var import_core14 = require("@lessonkit/core");
2887
+ var import_react22 = require("react");
2888
+ var import_core15 = require("@lessonkit/core");
2484
2889
 
2485
2890
  // src/compound/useCompoundNavigation.ts
2486
- var import_react17 = require("react");
2891
+ var import_react19 = require("react");
2487
2892
  function useCompoundNavigation(pageCount, index, setIndex) {
2488
- const goNext = (0, import_react17.useCallback)(() => {
2893
+ const goNext = (0, import_react19.useCallback)(() => {
2489
2894
  if (pageCount < 1) return;
2490
2895
  setIndex((i) => Math.min(i + 1, pageCount - 1));
2491
2896
  }, [pageCount, setIndex]);
2492
- const goPrev = (0, import_react17.useCallback)(() => {
2897
+ const goPrev = (0, import_react19.useCallback)(() => {
2493
2898
  setIndex((i) => Math.max(i - 1, 0));
2494
2899
  }, [setIndex]);
2495
2900
  const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
@@ -2503,102 +2908,269 @@ function useCompoundNavigation(pageCount, index, setIndex) {
2503
2908
  }
2504
2909
 
2505
2910
  // src/compound/useCompoundPersistence.ts
2506
- var import_react19 = require("react");
2507
- var import_core13 = require("@lessonkit/core");
2911
+ var import_react21 = require("react");
2912
+ var import_core14 = require("@lessonkit/core");
2913
+
2914
+ // src/compound/resumeChildHandles.ts
2915
+ function filterRegisteredChildStates(handles, childStates) {
2916
+ const filtered = {};
2917
+ for (const [key, value] of Object.entries(childStates)) {
2918
+ if (handles.has(key)) {
2919
+ filtered[key] = value;
2920
+ }
2921
+ }
2922
+ return filtered;
2923
+ }
2924
+ function resumeChildHandles(handles, childStates, opts) {
2925
+ const pendingKeys = Object.keys(childStates);
2926
+ const alreadyResumed = opts?.alreadyResumed;
2927
+ if (opts?.waitForHandles && pendingKeys.length > 0) {
2928
+ if (handles.size === 0) return false;
2929
+ const registeredPending = pendingKeys.filter((k) => handles.has(k));
2930
+ if (registeredPending.length === 0) {
2931
+ return false;
2932
+ }
2933
+ if (registeredPending.length < pendingKeys.length) {
2934
+ for (const key of registeredPending) {
2935
+ if (alreadyResumed?.has(key)) continue;
2936
+ const handle = handles.get(key);
2937
+ const child = childStates[key];
2938
+ if (handle?.resume && child) {
2939
+ handle.resume(child);
2940
+ alreadyResumed?.add(key);
2941
+ }
2942
+ }
2943
+ return false;
2944
+ }
2945
+ }
2946
+ for (const [checkId, handle] of handles) {
2947
+ if (alreadyResumed?.has(checkId)) continue;
2948
+ const child = childStates[checkId];
2949
+ if (child && handle.resume) {
2950
+ handle.resume(child);
2951
+ alreadyResumed?.add(checkId);
2952
+ }
2953
+ }
2954
+ return true;
2955
+ }
2508
2956
 
2509
2957
  // src/compound/useCompoundResume.ts
2510
- var import_react18 = require("react");
2511
- var import_core11 = require("@lessonkit/core");
2958
+ var import_react20 = require("react");
2512
2959
  var import_core12 = require("@lessonkit/core");
2960
+ var import_core13 = require("@lessonkit/core");
2961
+ var warnedCompoundPersistFailure = false;
2962
+ function warnCompoundPersistFailure() {
2963
+ if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
2964
+ warnedCompoundPersistFailure = true;
2965
+ console.warn(
2966
+ "[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
2967
+ );
2968
+ }
2513
2969
  function useCompoundResume(opts) {
2514
- const storageRef = (0, import_react18.useRef)(opts.storage ?? (0, import_core12.createSessionStoragePort)());
2515
- const resumedRef = (0, import_react18.useRef)(false);
2516
- (0, import_react18.useEffect)(() => {
2970
+ const lessonkitCtx = (0, import_react20.useContext)(LessonkitContext);
2971
+ const storageRef = (0, import_react20.useRef)(opts.storage ?? lessonkitCtx?.storage ?? (0, import_core13.createSessionStoragePort)());
2972
+ const resumedRef = (0, import_react20.useRef)(false);
2973
+ const resumeKeyRef = (0, import_react20.useRef)("");
2974
+ const prevEnabledRef = (0, import_react20.useRef)(opts.enabled);
2975
+ (0, import_react20.useEffect)(() => {
2976
+ storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core13.createSessionStoragePort)();
2977
+ }, [opts.storage, lessonkitCtx?.storage]);
2978
+ (0, import_react20.useEffect)(() => {
2979
+ if (!prevEnabledRef.current && opts.enabled) {
2980
+ resumedRef.current = false;
2981
+ }
2982
+ prevEnabledRef.current = opts.enabled;
2983
+ const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
2984
+ if (resumeKeyRef.current !== key) {
2985
+ resumeKeyRef.current = key;
2986
+ resumedRef.current = false;
2987
+ }
2517
2988
  if (!opts.enabled || !opts.courseId || resumedRef.current) return;
2518
- const saved = (0, import_core11.loadCompoundState)(storageRef.current, opts.courseId, opts.compoundId);
2989
+ const saved = (0, import_core12.loadCompoundState)(storageRef.current, opts.courseId, opts.compoundId);
2519
2990
  if (saved) {
2520
2991
  resumedRef.current = true;
2521
2992
  opts.onResume?.(saved);
2522
2993
  }
2523
2994
  }, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
2524
- return (0, import_react18.useCallback)(
2995
+ return (0, import_react20.useCallback)(
2525
2996
  (state) => {
2526
2997
  if (!opts.enabled || !opts.courseId) return;
2527
- (0, import_core11.saveCompoundState)(storageRef.current, opts.courseId, opts.compoundId, state);
2998
+ const persisted = (0, import_core12.saveCompoundState)(storageRef.current, opts.courseId, opts.compoundId, state);
2999
+ if (!persisted) warnCompoundPersistFailure();
2528
3000
  },
2529
3001
  [opts.enabled, opts.courseId, opts.compoundId]
2530
3002
  );
2531
3003
  }
2532
3004
 
2533
3005
  // src/compound/useCompoundPersistence.ts
2534
- function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = (0, import_core13.createSessionStoragePort)()) {
3006
+ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = (0, import_core14.createSessionStoragePort)()) {
2535
3007
  if (!enabled || !courseId || pageCount < 1) return 0;
2536
- const saved = (0, import_core13.loadCompoundState)(storage, courseId, compoundId);
3008
+ const saved = (0, import_core14.loadCompoundState)(storage, courseId, compoundId);
2537
3009
  if (!saved) return 0;
2538
- return (0, import_core13.clampCompoundPageIndex)(saved.activePageIndex, pageCount);
3010
+ return (0, import_core14.clampCompoundPageIndex)(saved.activePageIndex, pageCount);
3011
+ }
3012
+ function stripOrphanChildStates(handles, childStates) {
3013
+ return filterRegisteredChildStates(handles, childStates);
2539
3014
  }
2540
3015
  function useCompoundPersistence(opts) {
2541
- const storage = opts.storage ?? (0, import_core13.createSessionStoragePort)();
3016
+ const lessonkitCtx = (0, import_react21.useContext)(LessonkitContext);
3017
+ const storage = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core14.createSessionStoragePort)();
2542
3018
  const ctx = useCompoundRegistry();
2543
3019
  const handlesVersion = useCompoundHandlesVersion();
2544
- const pendingChildResumeRef = (0, import_react19.useRef)(null);
2545
- const loadedChildStatesRef = (0, import_react19.useRef)({});
2546
- const skipSaveUntilHydratedRef = (0, import_react19.useRef)(false);
2547
- const buildState = (0, import_react19.useCallback)(() => {
3020
+ const bridgeRef = useCompoundHydrationBridgeRef();
3021
+ const pendingChildResumeRef = (0, import_react21.useRef)(null);
3022
+ const resumedChildKeysRef = (0, import_react21.useRef)(/* @__PURE__ */ new Set());
3023
+ const loadedChildStatesRef = (0, import_react21.useRef)({});
3024
+ const skipSaveUntilHydratedRef = (0, import_react21.useRef)(false);
3025
+ const hydrationKeyRef = (0, import_react21.useRef)("");
3026
+ const hydrationInitRef = (0, import_react21.useRef)(false);
3027
+ const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
3028
+ if (hydrationKeyRef.current !== hydrationKey) {
3029
+ hydrationKeyRef.current = hydrationKey;
3030
+ hydrationInitRef.current = false;
3031
+ loadedChildStatesRef.current = {};
3032
+ skipSaveUntilHydratedRef.current = false;
3033
+ pendingChildResumeRef.current = null;
3034
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
3035
+ }
3036
+ if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
3037
+ hydrationInitRef.current = true;
3038
+ const saved = (0, import_core14.loadCompoundState)(storage, opts.courseId, opts.compoundId);
3039
+ if (saved && Object.keys(saved.childStates).length > 0) {
3040
+ loadedChildStatesRef.current = { ...saved.childStates };
3041
+ skipSaveUntilHydratedRef.current = true;
3042
+ pendingChildResumeRef.current = saved;
3043
+ }
3044
+ }
3045
+ const buildState = (0, import_react21.useCallback)(() => {
2548
3046
  const childStates = {
2549
3047
  ...loadedChildStatesRef.current
2550
3048
  };
2551
3049
  if (ctx) {
2552
- for (const [checkId, handle] of ctx.getHandles()) {
3050
+ for (const [checkId, entry] of ctx.getRegisteredHandles()) {
3051
+ const handle = entry.handle;
2553
3052
  if (handle.getCurrentState) {
2554
3053
  childStates[checkId] = handle.getCurrentState();
2555
3054
  delete loadedChildStatesRef.current[checkId];
2556
3055
  }
2557
3056
  }
2558
3057
  }
2559
- return (0, import_core13.createCompoundResumeState)({
2560
- activePageIndex: (0, import_core13.clampCompoundPageIndex)(opts.index, opts.pageCount),
3058
+ return (0, import_core14.createCompoundResumeState)({
3059
+ activePageIndex: (0, import_core14.clampCompoundPageIndex)(opts.index, opts.pageCount),
2561
3060
  childStates
2562
3061
  });
2563
3062
  }, [ctx, opts.index, opts.pageCount]);
2564
- const applyPendingChildResume = (0, import_react19.useCallback)(() => {
3063
+ const buildStateRef = (0, import_react21.useRef)(buildState);
3064
+ buildStateRef.current = buildState;
3065
+ const persistNowRef = (0, import_react21.useRef)(() => {
3066
+ });
3067
+ const finalizeHydration = (0, import_react21.useCallback)(
3068
+ (childStates) => {
3069
+ loadedChildStatesRef.current = {
3070
+ ...loadedChildStatesRef.current,
3071
+ ...childStates
3072
+ };
3073
+ skipSaveUntilHydratedRef.current = false;
3074
+ pendingChildResumeRef.current = null;
3075
+ queueMicrotask(() => persistNowRef.current());
3076
+ },
3077
+ []
3078
+ );
3079
+ const applyPendingChildResume = (0, import_react21.useCallback)(() => {
2565
3080
  const pending = pendingChildResumeRef.current;
2566
3081
  if (!pending || !ctx) return;
2567
- const applied = resumeChildHandles(ctx.getHandles(), pending.childStates, { waitForHandles: true });
2568
- if (!applied) return;
2569
- pendingChildResumeRef.current = null;
2570
- skipSaveUntilHydratedRef.current = false;
2571
- }, [ctx]);
3082
+ const handles = ctx.getHandles();
3083
+ const applied = resumeChildHandles(handles, pending.childStates, {
3084
+ waitForHandles: true,
3085
+ alreadyResumed: resumedChildKeysRef.current
3086
+ });
3087
+ if (!applied) {
3088
+ if (handles.size === 0) {
3089
+ const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
3090
+ resumeChildHandles(handles, registeredOnly2, {
3091
+ alreadyResumed: resumedChildKeysRef.current
3092
+ });
3093
+ finalizeHydration(registeredOnly2);
3094
+ return;
3095
+ }
3096
+ const handlesAtWait = handles.size;
3097
+ queueMicrotask(() => {
3098
+ if (pendingChildResumeRef.current !== pending) return;
3099
+ const handlesNow = ctx.getHandles();
3100
+ if (handlesNow.size !== handlesAtWait) return;
3101
+ const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
3102
+ resumeChildHandles(handlesNow, registeredOnly2, {
3103
+ alreadyResumed: resumedChildKeysRef.current
3104
+ });
3105
+ finalizeHydration(registeredOnly2);
3106
+ });
3107
+ return;
3108
+ }
3109
+ const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
3110
+ finalizeHydration(registeredOnly);
3111
+ }, [ctx, finalizeHydration]);
2572
3112
  const saveResume = useCompoundResume({
2573
3113
  courseId: opts.courseId,
2574
3114
  compoundId: opts.compoundId,
2575
3115
  enabled: opts.enabled,
2576
3116
  storage,
2577
3117
  onResume: (state) => {
2578
- const clamped = (0, import_core13.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
3118
+ const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
2579
3119
  loadedChildStatesRef.current = { ...state.childStates };
2580
3120
  skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2581
3121
  opts.setIndex(clamped);
2582
- pendingChildResumeRef.current = { ...state, activePageIndex: clamped };
3122
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
3123
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
2583
3124
  queueMicrotask(() => applyPendingChildResume());
2584
3125
  }
2585
3126
  });
2586
- (0, import_react19.useEffect)(() => {
3127
+ const persistNow = (0, import_react21.useCallback)(() => {
2587
3128
  if (!opts.enabled || !opts.courseId) return;
2588
3129
  if (skipSaveUntilHydratedRef.current) return;
2589
- saveResume(buildState());
2590
- }, [
2591
- opts.enabled,
2592
- opts.courseId,
2593
- opts.index,
2594
- opts.pageCount,
2595
- handlesVersion,
2596
- saveResume,
2597
- buildState
2598
- ]);
2599
- (0, import_react19.useEffect)(() => {
3130
+ saveResume(buildStateRef.current());
3131
+ }, [opts.enabled, opts.courseId, saveResume]);
3132
+ (0, import_react21.useEffect)(() => {
3133
+ persistNowRef.current = persistNow;
3134
+ }, [persistNow]);
3135
+ const notifyImperativeResume = (0, import_react21.useCallback)(
3136
+ (state) => {
3137
+ const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
3138
+ loadedChildStatesRef.current = { ...state.childStates };
3139
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
3140
+ opts.setIndex(clamped);
3141
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
3142
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
3143
+ queueMicrotask(() => applyPendingChildResume());
3144
+ },
3145
+ [opts.pageCount, opts.setIndex, applyPendingChildResume]
3146
+ );
3147
+ (0, import_react21.useEffect)(() => {
3148
+ if (!bridgeRef) return;
3149
+ bridgeRef.current = { notifyImperativeResume };
3150
+ return () => {
3151
+ if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
3152
+ bridgeRef.current = null;
3153
+ }
3154
+ };
3155
+ }, [bridgeRef, notifyImperativeResume]);
3156
+ (0, import_react21.useEffect)(() => {
2600
3157
  applyPendingChildResume();
2601
3158
  }, [opts.index, handlesVersion, applyPendingChildResume]);
3159
+ (0, import_react21.useEffect)(() => {
3160
+ persistNow();
3161
+ }, [persistNow, opts.index, opts.pageCount, handlesVersion]);
3162
+ (0, import_react21.useEffect)(() => {
3163
+ if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
3164
+ const flushOnExit = () => {
3165
+ if (document.visibilityState === "hidden") persistNow();
3166
+ };
3167
+ document.addEventListener("visibilitychange", flushOnExit);
3168
+ window.addEventListener("pagehide", flushOnExit);
3169
+ return () => {
3170
+ document.removeEventListener("visibilitychange", flushOnExit);
3171
+ window.removeEventListener("pagehide", flushOnExit);
3172
+ };
3173
+ }, [opts.enabled, opts.courseId, persistNow]);
2602
3174
  }
2603
3175
 
2604
3176
  // src/compound/useCompoundShell.ts
@@ -2614,18 +3186,19 @@ function useCompoundShell(opts) {
2614
3186
  storage: opts.storage
2615
3187
  });
2616
3188
  const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
2617
- const visibleIndex = (0, import_core14.clampCompoundPageIndex)(opts.index, opts.pageCount);
3189
+ const visibleIndex = (0, import_core15.clampCompoundPageIndex)(opts.index, opts.pageCount);
2618
3190
  useCompoundHandleRef(opts.ref, {
2619
3191
  activePageIndex: visibleIndex,
2620
3192
  setActivePageIndex: opts.setIndex,
2621
3193
  getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
3194
+ getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
2622
3195
  pageCount: opts.pageCount,
2623
3196
  enableSolutionsButton: opts.enableSolutionsButton
2624
3197
  });
2625
3198
  return { visibleIndex, goNext, goPrev, progress, ctx };
2626
3199
  }
2627
3200
  function useCompoundInitialIndex(opts) {
2628
- return (0, import_react20.useMemo)(
3201
+ return (0, import_react22.useMemo)(
2629
3202
  () => readCompoundInitialIndex(
2630
3203
  opts.courseId,
2631
3204
  opts.compoundId,
@@ -2638,8 +3211,8 @@ function useCompoundInitialIndex(opts) {
2638
3211
  }
2639
3212
 
2640
3213
  // src/compound/validateChildren.ts
2641
- var import_react21 = __toESM(require("react"), 1);
2642
- var import_core15 = require("@lessonkit/core");
3214
+ var import_react23 = __toESM(require("react"), 1);
3215
+ var import_core16 = require("@lessonkit/core");
2643
3216
 
2644
3217
  // src/compound/blockType.ts
2645
3218
  var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
@@ -2663,6 +3236,8 @@ var warnedPairs = /* @__PURE__ */ new Set();
2663
3236
  var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
2664
3237
  "Page",
2665
3238
  "InteractiveBook",
3239
+ "Slide",
3240
+ "SlideDeck",
2666
3241
  "AssessmentSequence"
2667
3242
  ]);
2668
3243
  function warnOrThrow(msg, strict) {
@@ -2673,8 +3248,8 @@ function warnOrThrow(msg, strict) {
2673
3248
  }
2674
3249
  }
2675
3250
  function validateNode(parent, node, depth, strict) {
2676
- import_react21.default.Children.forEach(node, (child) => {
2677
- if (!import_react21.default.isValidElement(child)) return;
3251
+ import_react23.default.Children.forEach(node, (child) => {
3252
+ if (!import_react23.default.isValidElement(child)) return;
2678
3253
  const blockType = getLessonkitBlockType(child.type);
2679
3254
  if (!blockType) {
2680
3255
  if (child.props && typeof child.props === "object" && "children" in child.props) {
@@ -2682,7 +3257,7 @@ function validateNode(parent, node, depth, strict) {
2682
3257
  }
2683
3258
  return;
2684
3259
  }
2685
- if (!(0, import_core15.isChildTypeAllowed)(parent, blockType)) {
3260
+ if (!(0, import_core16.isChildTypeAllowed)(parent, blockType)) {
2686
3261
  const key = `${parent}:${blockType}`;
2687
3262
  if (!warnedPairs.has(key)) {
2688
3263
  warnedPairs.add(key);
@@ -2692,7 +3267,7 @@ function validateNode(parent, node, depth, strict) {
2692
3267
  }
2693
3268
  }
2694
3269
  if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
2695
- const maxDepth = import_core15.COMPOUND_MAX_NESTING_DEPTH[parent];
3270
+ const maxDepth = import_core16.COMPOUND_MAX_NESTING_DEPTH[parent];
2696
3271
  if (depth >= maxDepth) {
2697
3272
  warnOrThrow(
2698
3273
  `[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
@@ -2707,15 +3282,15 @@ function validateNode(parent, node, depth, strict) {
2707
3282
  } else if (child.props && typeof child.props === "object" && "children" in child.props) {
2708
3283
  validateSubtreeForForbidden(
2709
3284
  child.props.children,
2710
- import_core15.ACCORDION_FORBIDDEN_CHILD_TYPES,
3285
+ import_core16.ACCORDION_FORBIDDEN_CHILD_TYPES,
2711
3286
  strict
2712
3287
  );
2713
3288
  }
2714
3289
  });
2715
3290
  }
2716
3291
  function validateSubtreeForForbidden(node, forbidden, strict) {
2717
- import_react21.default.Children.forEach(node, (child) => {
2718
- if (!import_react21.default.isValidElement(child)) return;
3292
+ import_react23.default.Children.forEach(node, (child) => {
3293
+ if (!import_react23.default.isValidElement(child)) return;
2719
3294
  const blockType = getLessonkitBlockType(child.type);
2720
3295
  if (blockType && forbidden.includes(blockType)) {
2721
3296
  warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
@@ -2737,7 +3312,7 @@ function validateSubtreeForForbidden(node, forbidden, strict) {
2737
3312
  function validateAccordionSections(sections, strict) {
2738
3313
  if (!isDevEnvironment4() && !strict) return;
2739
3314
  for (const section of sections) {
2740
- validateSubtreeForForbidden(section.content, import_core15.ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
3315
+ validateSubtreeForForbidden(section.content, import_core16.ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
2741
3316
  }
2742
3317
  }
2743
3318
  function validateCompoundChildren(parent, children, strict) {
@@ -2755,8 +3330,8 @@ function warnSharedCompoundStorageKey(opts) {
2755
3330
  }
2756
3331
 
2757
3332
  // src/blocks/AssessmentSequence.tsx
2758
- var import_jsx_runtime11 = require("react/jsx-runtime");
2759
- var AssessmentSequenceInner = (0, import_react22.forwardRef)(
3333
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3334
+ var AssessmentSequenceInner = (0, import_react24.forwardRef)(
2760
3335
  function AssessmentSequenceInner2(props, ref) {
2761
3336
  const { compoundId, childArray, index, setIndex, persistEnabled } = props;
2762
3337
  const sequential = props.sequential !== false;
@@ -2773,18 +3348,18 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
2773
3348
  });
2774
3349
  validateCompoundChildren("AssessmentSequence", props.children);
2775
3350
  if (!sequential) {
2776
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
3351
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
2777
3352
  }
2778
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
2779
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { children: [
3353
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
3354
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("p", { children: [
2780
3355
  "Question ",
2781
3356
  progress.current,
2782
3357
  " of ",
2783
3358
  progress.total
2784
3359
  ] }),
2785
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { hidden: i !== visibleIndex, children: child }, child.key ?? i)) }),
2786
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
2787
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3360
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
3361
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
3362
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2788
3363
  "button",
2789
3364
  {
2790
3365
  type: "button",
@@ -2794,7 +3369,7 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
2794
3369
  children: "Previous"
2795
3370
  }
2796
3371
  ),
2797
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3372
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2798
3373
  "button",
2799
3374
  {
2800
3375
  type: "button",
@@ -2808,18 +3383,23 @@ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
2808
3383
  ] });
2809
3384
  }
2810
3385
  );
2811
- var AssessmentSequence = (0, import_react22.forwardRef)(
3386
+ var AssessmentSequence = (0, import_react24.forwardRef)(
2812
3387
  function AssessmentSequence2(props, ref) {
2813
- const compoundId = (0, import_react22.useMemo)(
2814
- () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
3388
+ const reactInstanceId = (0, import_react24.useId)();
3389
+ const autoCompoundIdRef = (0, import_react24.useRef)(null);
3390
+ if (!props.blockId && !autoCompoundIdRef.current) {
3391
+ autoCompoundIdRef.current = (0, import_core17.deriveId)(`assessment-sequence-${reactInstanceId}`);
3392
+ }
3393
+ const compoundId = (0, import_react24.useMemo)(
3394
+ () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
2815
3395
  [props.blockId]
2816
3396
  );
2817
- const childArray = import_react22.default.Children.toArray(props.children).filter(
2818
- import_react22.default.isValidElement
3397
+ const childArray = import_react24.default.Children.toArray(props.children).filter(
3398
+ import_react24.default.isValidElement
2819
3399
  );
2820
- const { config } = useLessonkit();
3400
+ const { config, storage } = useLessonkit();
2821
3401
  const persistEnabled = config.session?.persistCompoundState !== false;
2822
- (0, import_react22.useEffect)(() => {
3402
+ (0, import_react24.useEffect)(() => {
2823
3403
  warnSharedCompoundStorageKey({
2824
3404
  persistEnabled,
2825
3405
  hasExplicitBlockId: Boolean(props.blockId),
@@ -2830,11 +3410,15 @@ var AssessmentSequence = (0, import_react22.forwardRef)(
2830
3410
  courseId: config.courseId,
2831
3411
  compoundId,
2832
3412
  pageCount: childArray.length,
2833
- persistEnabled
3413
+ persistEnabled,
3414
+ storage
2834
3415
  });
2835
- const [index, setIndex] = (0, import_react22.useState)(initialIndex);
2836
- const setIndexStable = (0, import_react22.useCallback)((i) => setIndex(i), []);
2837
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3416
+ const [index, setIndex] = (0, import_react24.useState)(initialIndex);
3417
+ const setIndexStable = (0, import_react24.useCallback)((i) => setIndex(i), []);
3418
+ (0, import_react24.useEffect)(() => {
3419
+ setIndex(initialIndex);
3420
+ }, [config.courseId, compoundId, initialIndex]);
3421
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2838
3422
  AssessmentSequenceInner,
2839
3423
  {
2840
3424
  ...props,
@@ -2851,25 +3435,25 @@ var AssessmentSequence = (0, import_react22.forwardRef)(
2851
3435
  setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
2852
3436
 
2853
3437
  // src/blocks/Text.tsx
2854
- var import_react23 = require("react");
2855
- var import_jsx_runtime12 = require("react/jsx-runtime");
3438
+ var import_react25 = require("react");
3439
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2856
3440
  function Text(props) {
2857
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
3441
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
2858
3442
  }
2859
3443
  setLessonkitBlockType(Text, "Text");
2860
3444
 
2861
3445
  // src/blocks/Heading.tsx
2862
- var import_jsx_runtime13 = require("react/jsx-runtime");
3446
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2863
3447
  function Heading(props) {
2864
3448
  const Tag = `h${props.level}`;
2865
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
3449
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
2866
3450
  }
2867
3451
  setLessonkitBlockType(Heading, "Heading");
2868
3452
 
2869
3453
  // src/blocks/Image.tsx
2870
- var import_jsx_runtime14 = require("react/jsx-runtime");
3454
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2871
3455
  function Image(props) {
2872
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3456
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2873
3457
  "img",
2874
3458
  {
2875
3459
  src: props.src,
@@ -2883,14 +3467,14 @@ function Image(props) {
2883
3467
  setLessonkitBlockType(Image, "Image");
2884
3468
 
2885
3469
  // src/blocks/Page.tsx
2886
- var import_react24 = require("react");
2887
- var import_jsx_runtime15 = require("react/jsx-runtime");
3470
+ var import_react26 = require("react");
3471
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2888
3472
  function Page(props) {
2889
3473
  validateCompoundChildren("Page", props.children);
2890
3474
  const { track } = useLessonkit();
2891
3475
  const lessonId = useEnclosingLessonId();
2892
- (0, import_react24.useEffect)(() => {
2893
- if (props.hidden || !lessonId) return;
3476
+ (0, import_react26.useEffect)(() => {
3477
+ if (props.hidden || !lessonId || props.parentType) return;
2894
3478
  track(
2895
3479
  "compound_page_viewed",
2896
3480
  {
@@ -2901,7 +3485,7 @@ function Page(props) {
2901
3485
  { lessonId }
2902
3486
  );
2903
3487
  }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
2904
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3488
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
2905
3489
  "section",
2906
3490
  {
2907
3491
  "aria-label": props.title ?? "Page",
@@ -2909,8 +3493,8 @@ function Page(props) {
2909
3493
  "data-testid": `page-${props.blockId}`,
2910
3494
  hidden: props.hidden ? true : void 0,
2911
3495
  children: [
2912
- props.title ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { children: props.title }) : null,
2913
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: props.children })
3496
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { children: props.title }) : null,
3497
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: props.children }) })
2914
3498
  ]
2915
3499
  }
2916
3500
  );
@@ -2918,9 +3502,9 @@ function Page(props) {
2918
3502
  setLessonkitBlockType(Page, "Page");
2919
3503
 
2920
3504
  // src/blocks/InteractiveBook.tsx
2921
- var import_react25 = __toESM(require("react"), 1);
2922
- var import_jsx_runtime16 = require("react/jsx-runtime");
2923
- var InteractiveBookInner = (0, import_react25.forwardRef)(
3505
+ var import_react27 = __toESM(require("react"), 1);
3506
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3507
+ var InteractiveBookInner = (0, import_react27.forwardRef)(
2924
3508
  function InteractiveBookInner2(props, ref) {
2925
3509
  const { blockId, pages, index, setIndex, persistEnabled } = props;
2926
3510
  validateCompoundChildren("InteractiveBook", pages);
@@ -2935,11 +3519,11 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
2935
3519
  persistEnabled,
2936
3520
  ref
2937
3521
  });
2938
- const pageTitles = (0, import_react25.useMemo)(
3522
+ const pageTitles = (0, import_react27.useMemo)(
2939
3523
  () => pages.map((page) => page.props.title),
2940
3524
  [pages]
2941
3525
  );
2942
- (0, import_react25.useEffect)(() => {
3526
+ (0, import_react27.useEffect)(() => {
2943
3527
  if (!lessonId || pages.length === 0) return;
2944
3528
  track(
2945
3529
  "book_page_viewed",
@@ -2951,31 +3535,31 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
2951
3535
  { lessonId }
2952
3536
  );
2953
3537
  }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
2954
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
2955
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { children: props.title }),
2956
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { children: [
3538
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
3539
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }),
3540
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { children: [
2957
3541
  "Page ",
2958
3542
  progress.current,
2959
3543
  " of ",
2960
3544
  progress.total
2961
3545
  ] }),
2962
- props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { "data-testid": "book-score", children: [
3546
+ props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { "data-testid": "book-score", children: [
2963
3547
  "Score: ",
2964
3548
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
2965
3549
  " /",
2966
3550
  " ",
2967
3551
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
2968
3552
  ] }) : null,
2969
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
2970
- (page, i) => import_react25.default.cloneElement(page, {
3553
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
3554
+ (page, i) => import_react27.default.cloneElement(page, {
2971
3555
  key: page.key ?? page.props.blockId,
2972
3556
  hidden: i !== visibleIndex,
2973
3557
  pageIndex: i,
2974
3558
  parentType: "InteractiveBook"
2975
3559
  })
2976
3560
  ) }),
2977
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("nav", { "aria-label": "Book navigation", children: [
2978
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3561
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("nav", { "aria-label": "Book navigation", children: [
3562
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2979
3563
  "button",
2980
3564
  {
2981
3565
  type: "button",
@@ -2985,7 +3569,7 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
2985
3569
  children: "Previous"
2986
3570
  }
2987
3571
  ),
2988
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3572
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2989
3573
  "button",
2990
3574
  {
2991
3575
  type: "button",
@@ -2999,25 +3583,29 @@ var InteractiveBookInner = (0, import_react25.forwardRef)(
2999
3583
  ] });
3000
3584
  }
3001
3585
  );
3002
- var InteractiveBook = (0, import_react25.forwardRef)(function InteractiveBook2(props, ref) {
3003
- const blockId = (0, import_react25.useMemo)(
3586
+ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(props, ref) {
3587
+ const blockId = (0, import_react27.useMemo)(
3004
3588
  () => normalizeComponentId(props.blockId, "blockId"),
3005
3589
  [props.blockId]
3006
3590
  );
3007
- const pages = import_react25.default.Children.toArray(props.children).filter(
3008
- import_react25.default.isValidElement
3591
+ const pages = import_react27.default.Children.toArray(props.children).filter(
3592
+ import_react27.default.isValidElement
3009
3593
  );
3010
- const { config } = useLessonkit();
3594
+ const { config, storage } = useLessonkit();
3011
3595
  const persistEnabled = config.session?.persistCompoundState !== false;
3012
3596
  const initialIndex = useCompoundInitialIndex({
3013
3597
  courseId: config.courseId,
3014
3598
  compoundId: blockId,
3015
3599
  pageCount: pages.length,
3016
- persistEnabled
3600
+ persistEnabled,
3601
+ storage
3017
3602
  });
3018
- const [index, setIndex] = (0, import_react25.useState)(initialIndex);
3019
- const setIndexStable = (0, import_react25.useCallback)((i) => setIndex(i), []);
3020
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3603
+ const [index, setIndex] = (0, import_react27.useState)(initialIndex);
3604
+ const setIndexStable = (0, import_react27.useCallback)((i) => setIndex(i), []);
3605
+ (0, import_react27.useEffect)(() => {
3606
+ setIndex(initialIndex);
3607
+ }, [config.courseId, blockId, initialIndex]);
3608
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3021
3609
  InteractiveBookInner,
3022
3610
  {
3023
3611
  ...props,
@@ -3032,17 +3620,249 @@ var InteractiveBook = (0, import_react25.forwardRef)(function InteractiveBook2(p
3032
3620
  });
3033
3621
  setLessonkitBlockType(InteractiveBook, "InteractiveBook");
3034
3622
 
3623
+ // src/blocks/Slide.tsx
3624
+ var import_react28 = require("react");
3625
+ var import_jsx_runtime19 = require("react/jsx-runtime");
3626
+ function Slide(props) {
3627
+ validateCompoundChildren("Slide", props.children);
3628
+ const { track } = useLessonkit();
3629
+ const lessonId = useEnclosingLessonId();
3630
+ (0, import_react28.useEffect)(() => {
3631
+ if (props.hidden || !lessonId || props.parentType) return;
3632
+ track(
3633
+ "compound_page_viewed",
3634
+ {
3635
+ blockId: props.blockId,
3636
+ pageIndex: props.slideIndex ?? 0,
3637
+ parentType: props.parentType
3638
+ },
3639
+ { lessonId }
3640
+ );
3641
+ }, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
3642
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
3643
+ "section",
3644
+ {
3645
+ "aria-label": props.title ?? "Slide",
3646
+ "data-lk-block-id": props.blockId,
3647
+ "data-testid": `slide-${props.blockId}`,
3648
+ hidden: props.hidden ? true : void 0,
3649
+ children: [
3650
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }) : null,
3651
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { children: props.children }) })
3652
+ ]
3653
+ }
3654
+ );
3655
+ }
3656
+ setLessonkitBlockType(Slide, "Slide");
3657
+
3658
+ // src/blocks/SlideDeck.tsx
3659
+ var import_react30 = __toESM(require("react"), 1);
3660
+
3661
+ // src/compound/useCompoundKeyboardNav.ts
3662
+ var import_react29 = require("react");
3663
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
3664
+ function isEditableTarget(target) {
3665
+ if (!(target instanceof HTMLElement)) return false;
3666
+ if (INTERACTIVE_TAGS.has(target.tagName)) return true;
3667
+ if (target.isContentEditable) return true;
3668
+ if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
3669
+ return true;
3670
+ }
3671
+ return false;
3672
+ }
3673
+ function useCompoundKeyboardNav(opts) {
3674
+ const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
3675
+ (0, import_react29.useEffect)(() => {
3676
+ const el = containerRef.current;
3677
+ if (!el || pageCount === 0) return;
3678
+ const onKeyDown = (event) => {
3679
+ if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
3680
+ return;
3681
+ }
3682
+ if (isEditableTarget(event.target)) return;
3683
+ switch (event.key) {
3684
+ case "ArrowRight":
3685
+ case "ArrowDown":
3686
+ if (visibleIndex < pageCount - 1) {
3687
+ event.preventDefault();
3688
+ goNext();
3689
+ }
3690
+ break;
3691
+ case "ArrowLeft":
3692
+ case "ArrowUp":
3693
+ if (visibleIndex > 0) {
3694
+ event.preventDefault();
3695
+ goPrev();
3696
+ }
3697
+ break;
3698
+ case "Home":
3699
+ if (visibleIndex !== 0) {
3700
+ event.preventDefault();
3701
+ setIndex(0);
3702
+ }
3703
+ break;
3704
+ case "End":
3705
+ if (visibleIndex !== pageCount - 1) {
3706
+ event.preventDefault();
3707
+ setIndex(pageCount - 1);
3708
+ }
3709
+ break;
3710
+ default:
3711
+ break;
3712
+ }
3713
+ };
3714
+ el.addEventListener("keydown", onKeyDown);
3715
+ return () => el.removeEventListener("keydown", onKeyDown);
3716
+ }, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
3717
+ }
3718
+
3719
+ // src/blocks/SlideDeck.tsx
3720
+ var import_jsx_runtime20 = require("react/jsx-runtime");
3721
+ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(props, ref) {
3722
+ const { blockId, slides, index, setIndex, persistEnabled } = props;
3723
+ validateCompoundChildren("SlideDeck", slides);
3724
+ const { config, track } = useLessonkit();
3725
+ const lessonId = useEnclosingLessonId();
3726
+ const containerRef = (0, import_react30.useRef)(null);
3727
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3728
+ courseId: config.courseId,
3729
+ compoundId: blockId,
3730
+ pageCount: slides.length,
3731
+ index,
3732
+ setIndex,
3733
+ persistEnabled,
3734
+ ref
3735
+ });
3736
+ const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), [setIndex]);
3737
+ useCompoundKeyboardNav({
3738
+ containerRef,
3739
+ visibleIndex,
3740
+ pageCount: slides.length,
3741
+ goNext,
3742
+ goPrev,
3743
+ setIndex: setIndexStable
3744
+ });
3745
+ const slideTitles = (0, import_react30.useMemo)(
3746
+ () => slides.map((slide) => slide.props.title),
3747
+ [slides]
3748
+ );
3749
+ (0, import_react30.useEffect)(() => {
3750
+ if (!lessonId || slides.length === 0) return;
3751
+ track(
3752
+ "slide_viewed",
3753
+ {
3754
+ blockId,
3755
+ slideIndex: visibleIndex,
3756
+ slideTitle: slideTitles[visibleIndex]
3757
+ },
3758
+ { lessonId }
3759
+ );
3760
+ }, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
3761
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
3762
+ "section",
3763
+ {
3764
+ ref: containerRef,
3765
+ tabIndex: -1,
3766
+ "aria-label": props.title,
3767
+ "data-testid": "slide-deck",
3768
+ "data-lk-block-id": blockId,
3769
+ children: [
3770
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }),
3771
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { children: [
3772
+ "Slide ",
3773
+ progress.current,
3774
+ " of ",
3775
+ progress.total
3776
+ ] }),
3777
+ props.showDeckScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { "data-testid": "deck-score", children: [
3778
+ "Score: ",
3779
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3780
+ " /",
3781
+ " ",
3782
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3783
+ ] }) : null,
3784
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
3785
+ (slide, i) => import_react30.default.cloneElement(slide, {
3786
+ key: slide.key ?? slide.props.blockId,
3787
+ hidden: i !== visibleIndex,
3788
+ slideIndex: i,
3789
+ parentType: "SlideDeck"
3790
+ })
3791
+ ) }),
3792
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3793
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3794
+ "button",
3795
+ {
3796
+ type: "button",
3797
+ "data-testid": "slide-prev",
3798
+ disabled: visibleIndex === 0 || slides.length === 0,
3799
+ onClick: goPrev,
3800
+ children: "Previous slide"
3801
+ }
3802
+ ),
3803
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3804
+ "button",
3805
+ {
3806
+ type: "button",
3807
+ "data-testid": "slide-next",
3808
+ disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
3809
+ onClick: goNext,
3810
+ children: "Next slide"
3811
+ }
3812
+ )
3813
+ ] })
3814
+ ]
3815
+ }
3816
+ );
3817
+ });
3818
+ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3819
+ const blockId = (0, import_react30.useMemo)(
3820
+ () => normalizeComponentId(props.blockId, "blockId"),
3821
+ [props.blockId]
3822
+ );
3823
+ const slides = import_react30.default.Children.toArray(props.children).filter(
3824
+ import_react30.default.isValidElement
3825
+ );
3826
+ const { config, storage } = useLessonkit();
3827
+ const persistEnabled = config.session?.persistCompoundState !== false;
3828
+ const initialIndex = useCompoundInitialIndex({
3829
+ courseId: config.courseId,
3830
+ compoundId: blockId,
3831
+ pageCount: slides.length,
3832
+ persistEnabled,
3833
+ storage
3834
+ });
3835
+ const [index, setIndex] = (0, import_react30.useState)(initialIndex);
3836
+ const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), []);
3837
+ (0, import_react30.useEffect)(() => {
3838
+ setIndex(initialIndex);
3839
+ }, [config.courseId, blockId, initialIndex]);
3840
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3841
+ SlideDeckInner,
3842
+ {
3843
+ ...props,
3844
+ ref,
3845
+ blockId,
3846
+ slides,
3847
+ index,
3848
+ setIndex,
3849
+ persistEnabled
3850
+ }
3851
+ ) });
3852
+ });
3853
+ setLessonkitBlockType(SlideDeck, "SlideDeck");
3854
+
3035
3855
  // src/blocks/Accordion.tsx
3036
- var import_react26 = require("react");
3037
- var import_jsx_runtime17 = require("react/jsx-runtime");
3856
+ var import_react31 = require("react");
3857
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3038
3858
  function Accordion(props) {
3039
3859
  if (isDevEnvironment4()) {
3040
3860
  validateAccordionSections(props.sections);
3041
3861
  }
3042
- const [open, setOpen] = (0, import_react26.useState)(/* @__PURE__ */ new Set());
3862
+ const [open, setOpen] = (0, import_react31.useState)(/* @__PURE__ */ new Set());
3043
3863
  const { track } = useLessonkit();
3044
3864
  const lessonId = useEnclosingLessonId();
3045
- const baseId = (0, import_react26.useId)();
3865
+ const baseId = (0, import_react31.useId)();
3046
3866
  const toggle = (sectionId) => {
3047
3867
  setOpen((prev) => {
3048
3868
  const next = new Set(prev);
@@ -3057,12 +3877,12 @@ function Accordion(props) {
3057
3877
  return next;
3058
3878
  });
3059
3879
  };
3060
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3880
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3061
3881
  const expanded = open.has(section.id);
3062
3882
  const panelId = `${baseId}-${section.id}`;
3063
3883
  const triggerId = `${baseId}-trigger-${section.id}`;
3064
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
3065
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3884
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
3885
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3066
3886
  "button",
3067
3887
  {
3068
3888
  id: triggerId,
@@ -3074,28 +3894,28 @@ function Accordion(props) {
3074
3894
  children: section.title
3075
3895
  }
3076
3896
  ) }),
3077
- expanded ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3897
+ expanded ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3078
3898
  ] }, section.id);
3079
3899
  }) });
3080
3900
  }
3081
3901
  setLessonkitBlockType(Accordion, "Accordion");
3082
3902
 
3083
3903
  // src/blocks/DialogCards.tsx
3084
- var import_react27 = require("react");
3085
- var import_jsx_runtime18 = require("react/jsx-runtime");
3904
+ var import_react32 = require("react");
3905
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3086
3906
  function DialogCards(props) {
3087
- const [index, setIndex] = (0, import_react27.useState)(0);
3088
- const [flipped, setFlipped] = (0, import_react27.useState)(false);
3907
+ const [index, setIndex] = (0, import_react32.useState)(0);
3908
+ const [flipped, setFlipped] = (0, import_react32.useState)(false);
3089
3909
  const card = props.cards[index];
3090
3910
  if (!card) return null;
3091
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3092
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { children: [
3911
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3912
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { children: [
3093
3913
  "Card ",
3094
3914
  index + 1,
3095
3915
  " of ",
3096
3916
  props.cards.length
3097
3917
  ] }),
3098
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3918
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3099
3919
  "button",
3100
3920
  {
3101
3921
  type: "button",
@@ -3106,8 +3926,8 @@ function DialogCards(props) {
3106
3926
  children: flipped ? card.back : card.front
3107
3927
  }
3108
3928
  ),
3109
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("nav", { "aria-label": "Card navigation", children: [
3110
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3929
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("nav", { "aria-label": "Card navigation", children: [
3930
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3111
3931
  "button",
3112
3932
  {
3113
3933
  type: "button",
@@ -3120,7 +3940,7 @@ function DialogCards(props) {
3120
3940
  children: "Previous"
3121
3941
  }
3122
3942
  ),
3123
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3943
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3124
3944
  "button",
3125
3945
  {
3126
3946
  type: "button",
@@ -3139,11 +3959,11 @@ function DialogCards(props) {
3139
3959
  setLessonkitBlockType(DialogCards, "DialogCards");
3140
3960
 
3141
3961
  // src/blocks/Flashcards.tsx
3142
- var import_react28 = require("react");
3143
- var import_jsx_runtime19 = require("react/jsx-runtime");
3962
+ var import_react33 = require("react");
3963
+ var import_jsx_runtime23 = require("react/jsx-runtime");
3144
3964
  function Flashcards(props) {
3145
- const [index, setIndex] = (0, import_react28.useState)(0);
3146
- const [face, setFace] = (0, import_react28.useState)("front");
3965
+ const [index, setIndex] = (0, import_react33.useState)(0);
3966
+ const [face, setFace] = (0, import_react33.useState)("front");
3147
3967
  const { track } = useLessonkit();
3148
3968
  const lessonId = useEnclosingLessonId();
3149
3969
  const card = props.cards[index];
@@ -3157,10 +3977,10 @@ function Flashcards(props) {
3157
3977
  lessonId ? { lessonId } : void 0
3158
3978
  );
3159
3979
  };
3160
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3161
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3162
- props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3163
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3980
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3981
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3982
+ props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3983
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3164
3984
  "button",
3165
3985
  {
3166
3986
  type: "button",
@@ -3178,10 +3998,10 @@ function Flashcards(props) {
3178
3998
  setLessonkitBlockType(Flashcards, "Flashcards");
3179
3999
 
3180
4000
  // src/blocks/ImageHotspots.tsx
3181
- var import_react29 = require("react");
3182
- var import_jsx_runtime20 = require("react/jsx-runtime");
4001
+ var import_react34 = require("react");
4002
+ var import_jsx_runtime24 = require("react/jsx-runtime");
3183
4003
  function ImageHotspots(props) {
3184
- const [active, setActive] = (0, import_react29.useState)(null);
4004
+ const [active, setActive] = (0, import_react34.useState)(null);
3185
4005
  const { track } = useLessonkit();
3186
4006
  const lessonId = useEnclosingLessonId();
3187
4007
  const open = (hotspotId) => {
@@ -3192,10 +4012,10 @@ function ImageHotspots(props) {
3192
4012
  lessonId ? { lessonId } : void 0
3193
4013
  );
3194
4014
  };
3195
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3196
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3197
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3198
- props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4015
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
4016
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4017
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4018
+ props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3199
4019
  "button",
3200
4020
  {
3201
4021
  type: "button",
@@ -3214,19 +4034,19 @@ function ImageHotspots(props) {
3214
4034
  h.id
3215
4035
  ))
3216
4036
  ] }),
3217
- active ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
4037
+ active ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3218
4038
  props.hotspots.find((h) => h.id === active)?.content,
3219
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
4039
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3220
4040
  ] }) : null
3221
4041
  ] });
3222
4042
  }
3223
4043
  setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3224
4044
 
3225
4045
  // src/blocks/ImageSlider.tsx
3226
- var import_react30 = require("react");
3227
- var import_jsx_runtime21 = require("react/jsx-runtime");
4046
+ var import_react35 = require("react");
4047
+ var import_jsx_runtime25 = require("react/jsx-runtime");
3228
4048
  function ImageSlider(props) {
3229
- const [index, setIndex] = (0, import_react30.useState)(0);
4049
+ const [index, setIndex] = (0, import_react35.useState)(0);
3230
4050
  const { track } = useLessonkit();
3231
4051
  const lessonId = useEnclosingLessonId();
3232
4052
  const slide = props.slides[index];
@@ -3239,11 +4059,11 @@ function ImageSlider(props) {
3239
4059
  lessonId ? { lessonId } : void 0
3240
4060
  );
3241
4061
  };
3242
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
3243
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
3244
- slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: slide.caption }) : null,
3245
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3246
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4062
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
4063
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
4064
+ slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: slide.caption }) : null,
4065
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("nav", { "aria-label": "Slide navigation", children: [
4066
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3247
4067
  "button",
3248
4068
  {
3249
4069
  type: "button",
@@ -3253,12 +4073,12 @@ function ImageSlider(props) {
3253
4073
  children: "Previous"
3254
4074
  }
3255
4075
  ),
3256
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { children: [
4076
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { children: [
3257
4077
  index + 1,
3258
4078
  " / ",
3259
4079
  props.slides.length
3260
4080
  ] }),
3261
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4081
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3262
4082
  "button",
3263
4083
  {
3264
4084
  type: "button",
@@ -3274,16 +4094,42 @@ function ImageSlider(props) {
3274
4094
  setLessonkitBlockType(ImageSlider, "ImageSlider");
3275
4095
 
3276
4096
  // src/blocks/FindHotspot.tsx
3277
- var import_react31 = require("react");
3278
- var import_jsx_runtime22 = require("react/jsx-runtime");
4097
+ var import_react36 = require("react");
4098
+ var import_jsx_runtime26 = require("react/jsx-runtime");
3279
4099
  var INTERACTION6 = "findHotspot";
3280
4100
  function FindHotspotInner(props, ref) {
3281
- const checkId = (0, import_react31.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3282
- const [selected, setSelected] = (0, import_react31.useState)(null);
3283
- const [checked, setChecked] = (0, import_react31.useState)(false);
4101
+ const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4102
+ const [selected, setSelected] = (0, import_react36.useState)(null);
4103
+ const [checked, setChecked] = (0, import_react36.useState)(false);
4104
+ const telemetryReplayedRef = (0, import_react36.useRef)(false);
3284
4105
  const assessment = useAssessmentState(props.enclosingLessonId);
4106
+ const targetIdsKey = props.targets.map((t) => t.id).join("\0");
4107
+ (0, import_react36.useEffect)(() => {
4108
+ setSelected(null);
4109
+ setChecked(false);
4110
+ telemetryReplayedRef.current = false;
4111
+ }, [checkId, props.correctTargetId, targetIdsKey]);
3285
4112
  const correct = selected === props.correctTargetId;
3286
- const handle = (0, import_react31.useMemo)(
4113
+ const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
4114
+ if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
4115
+ telemetryReplayedRef.current = true;
4116
+ assessment.answer({
4117
+ checkId,
4118
+ interactionType: INTERACTION6,
4119
+ response: nextSelected,
4120
+ correct: nextCorrect
4121
+ });
4122
+ if (nextCorrect) {
4123
+ assessment.complete({
4124
+ checkId,
4125
+ interactionType: INTERACTION6,
4126
+ score: 1,
4127
+ maxScore: 1,
4128
+ passingScore: props.passingScore ?? 1
4129
+ });
4130
+ }
4131
+ };
4132
+ const handle = (0, import_react36.useMemo)(
3287
4133
  () => buildAssessmentHandle({
3288
4134
  checkId,
3289
4135
  getScore: () => checked && correct ? 1 : 0,
@@ -3292,6 +4138,7 @@ function FindHotspotInner(props, ref) {
3292
4138
  resetTask: () => {
3293
4139
  setSelected(null);
3294
4140
  setChecked(false);
4141
+ telemetryReplayedRef.current = false;
3295
4142
  },
3296
4143
  showSolutions: () => setSelected(props.correctTargetId),
3297
4144
  getXAPIData: () => ({
@@ -3304,16 +4151,31 @@ function FindHotspotInner(props, ref) {
3304
4151
  }),
3305
4152
  getCurrentState: () => ({ selected, checked }),
3306
4153
  resume: (state) => {
3307
- const nextSelected = readStringField(state, "selected");
3308
- if (typeof nextSelected === "string") setSelected(nextSelected);
3309
- readBooleanStateField(state, "checked", setChecked);
4154
+ let nextSelected = selected;
4155
+ const rawSelected = readStringField(state, "selected");
4156
+ if (typeof rawSelected === "string" || rawSelected === null) {
4157
+ const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
4158
+ nextSelected = valid ? rawSelected : null;
4159
+ setSelected(nextSelected);
4160
+ }
4161
+ let nextChecked = checked;
4162
+ readBooleanStateField(state, "checked", (value) => {
4163
+ nextChecked = value;
4164
+ setChecked(value);
4165
+ });
4166
+ const nextCorrect = nextSelected === props.correctTargetId;
4167
+ replayTelemetry(nextSelected, nextChecked, nextCorrect);
3310
4168
  }
3311
4169
  }),
3312
- [checkId, selected, checked, correct, props.correctTargetId]
4170
+ [assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
3313
4171
  );
3314
4172
  useAssessmentHandleRegistration(checkId, handle, ref);
4173
+ const selectTarget = (id) => {
4174
+ setSelected(id);
4175
+ setChecked(false);
4176
+ };
3315
4177
  const submit = () => {
3316
- if (!selected) return;
4178
+ if (!selected || checked) return;
3317
4179
  setChecked(true);
3318
4180
  assessment.answer({
3319
4181
  checkId,
@@ -3327,14 +4189,14 @@ function FindHotspotInner(props, ref) {
3327
4189
  interactionType: INTERACTION6,
3328
4190
  score: 1,
3329
4191
  maxScore: 1,
3330
- passingScore: props.passingScore
4192
+ passingScore: props.passingScore ?? 1
3331
4193
  });
3332
4194
  }
3333
4195
  };
3334
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
3335
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3336
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3337
- props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4196
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
4197
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4198
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4199
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3338
4200
  "button",
3339
4201
  {
3340
4202
  type: "button",
@@ -3347,30 +4209,30 @@ function FindHotspotInner(props, ref) {
3347
4209
  top: `${t.y}%`,
3348
4210
  transform: "translate(-50%, -50%)"
3349
4211
  },
3350
- onClick: () => setSelected(t.id),
4212
+ onClick: () => selectTarget(t.id),
3351
4213
  children: t.label
3352
4214
  },
3353
4215
  t.id
3354
4216
  ))
3355
4217
  ] }),
3356
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
3357
- checked ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4218
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
4219
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
3358
4220
  ] });
3359
4221
  }
3360
- var FindHotspotInnerForwarded = (0, import_react31.forwardRef)(FindHotspotInner);
3361
- var FindHotspot = (0, import_react31.forwardRef)(function FindHotspot2(props, ref) {
3362
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
4222
+ var FindHotspotInnerForwarded = (0, import_react36.forwardRef)(FindHotspotInner);
4223
+ var FindHotspot = (0, import_react36.forwardRef)(function FindHotspot2(props, ref) {
4224
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
3363
4225
  });
3364
4226
  setLessonkitBlockType(FindHotspot, "FindHotspot");
3365
4227
 
3366
4228
  // src/blocks/FindMultipleHotspots.tsx
3367
- var import_react32 = require("react");
3368
- var import_jsx_runtime23 = require("react/jsx-runtime");
4229
+ var import_react37 = require("react");
4230
+ var import_jsx_runtime27 = require("react/jsx-runtime");
3369
4231
  var INTERACTION7 = "findMultipleHotspots";
3370
4232
  function FindMultipleHotspotsInner(props, ref) {
3371
- const checkId = (0, import_react32.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3372
- const [selected, setSelected] = (0, import_react32.useState)(/* @__PURE__ */ new Set());
3373
- const [checked, setChecked] = (0, import_react32.useState)(false);
4233
+ const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4234
+ const [selected, setSelected] = (0, import_react37.useState)(/* @__PURE__ */ new Set());
4235
+ const [checked, setChecked] = (0, import_react37.useState)(false);
3374
4236
  const assessment = useAssessmentState(props.enclosingLessonId);
3375
4237
  const toggle = (id) => {
3376
4238
  setSelected((prev) => {
@@ -3379,9 +4241,10 @@ function FindMultipleHotspotsInner(props, ref) {
3379
4241
  else next.add(id);
3380
4242
  return next;
3381
4243
  });
4244
+ setChecked(false);
3382
4245
  };
3383
4246
  const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
3384
- const handle = (0, import_react32.useMemo)(
4247
+ const handle = (0, import_react37.useMemo)(
3385
4248
  () => buildAssessmentHandle({
3386
4249
  checkId,
3387
4250
  getScore: () => checked && correct ? 1 : 0,
@@ -3411,7 +4274,7 @@ function FindMultipleHotspotsInner(props, ref) {
3411
4274
  );
3412
4275
  useAssessmentHandleRegistration(checkId, handle, ref);
3413
4276
  const submit = () => {
3414
- if (selected.size === 0) return;
4277
+ if (selected.size === 0 || checked) return;
3415
4278
  setChecked(true);
3416
4279
  assessment.answer({
3417
4280
  checkId,
@@ -3425,14 +4288,14 @@ function FindMultipleHotspotsInner(props, ref) {
3425
4288
  interactionType: INTERACTION7,
3426
4289
  score: 1,
3427
4290
  maxScore: 1,
3428
- passingScore: props.passingScore
4291
+ passingScore: props.passingScore ?? 1
3429
4292
  });
3430
4293
  }
3431
4294
  };
3432
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
3433
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3434
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3435
- props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4295
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
4296
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4297
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4298
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3436
4299
  "button",
3437
4300
  {
3438
4301
  type: "button",
@@ -3451,23 +4314,23 @@ function FindMultipleHotspotsInner(props, ref) {
3451
4314
  t.id
3452
4315
  ))
3453
4316
  ] }),
3454
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
3455
- checked ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4317
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
4318
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
3456
4319
  ] });
3457
4320
  }
3458
- var FindMultipleHotspotsInnerForwarded = (0, import_react32.forwardRef)(FindMultipleHotspotsInner);
3459
- var FindMultipleHotspots = (0, import_react32.forwardRef)(
4321
+ var FindMultipleHotspotsInnerForwarded = (0, import_react37.forwardRef)(FindMultipleHotspotsInner);
4322
+ var FindMultipleHotspots = (0, import_react37.forwardRef)(
3460
4323
  function FindMultipleHotspots2(props, ref) {
3461
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
4324
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
3462
4325
  }
3463
4326
  );
3464
4327
  setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
3465
4328
 
3466
4329
  // src/index.tsx
3467
- var import_core17 = require("@lessonkit/core");
4330
+ var import_core19 = require("@lessonkit/core");
3468
4331
 
3469
4332
  // src/theme/ThemeProvider.tsx
3470
- var import_react33 = __toESM(require("react"), 1);
4333
+ var import_react38 = __toESM(require("react"), 1);
3471
4334
  var import_themes = require("@lessonkit/themes");
3472
4335
 
3473
4336
  // src/theme/applyCssVariables.ts
@@ -3486,11 +4349,11 @@ function applyCssVariables(target, vars, previousKeys) {
3486
4349
  }
3487
4350
 
3488
4351
  // src/theme/ThemeProvider.tsx
3489
- var import_jsx_runtime24 = require("react/jsx-runtime");
3490
- var ThemeContext = (0, import_react33.createContext)(null);
4352
+ var import_jsx_runtime28 = require("react/jsx-runtime");
4353
+ var ThemeContext = (0, import_react38.createContext)(null);
3491
4354
  var useIsoLayoutEffect2 = (
3492
4355
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
3493
- typeof window !== "undefined" ? import_react33.useLayoutEffect : import_react33.default.useEffect
4356
+ typeof window !== "undefined" ? import_react38.useLayoutEffect : import_react38.default.useEffect
3494
4357
  );
3495
4358
  function getSystemMode() {
3496
4359
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -3509,7 +4372,7 @@ function ThemeProvider(props) {
3509
4372
  const preset = props.preset ?? "default";
3510
4373
  const mode = props.mode ?? "light";
3511
4374
  const targetKind = props.target ?? "document";
3512
- const [resolvedMode, setResolvedMode] = (0, import_react33.useState)(
4375
+ const [resolvedMode, setResolvedMode] = (0, import_react38.useState)(
3513
4376
  () => mode === "system" ? getSystemMode() : mode
3514
4377
  );
3515
4378
  useIsoLayoutEffect2(() => {
@@ -3525,20 +4388,20 @@ function ThemeProvider(props) {
3525
4388
  return () => mq.removeEventListener("change", onChange);
3526
4389
  }, [mode]);
3527
4390
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
3528
- const effectiveTheme = (0, import_react33.useMemo)(() => {
4391
+ const effectiveTheme = (0, import_react38.useMemo)(() => {
3529
4392
  const modeBase = resolveModeBase(mode, dataTheme);
3530
4393
  const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
3531
4394
  return (0, import_themes.mergeThemes)(base, props.theme ?? {});
3532
4395
  }, [preset, mode, dataTheme, props.theme]);
3533
- const hostRef = (0, import_react33.useRef)(null);
3534
- const appliedKeysRef = (0, import_react33.useRef)(/* @__PURE__ */ new Set());
4396
+ const hostRef = (0, import_react38.useRef)(null);
4397
+ const appliedKeysRef = (0, import_react38.useRef)(/* @__PURE__ */ new Set());
3535
4398
  useIsoLayoutEffect2(() => {
3536
4399
  if (targetKind === "document" && typeof document !== "undefined") {
3537
4400
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
3538
4401
  return () => document.documentElement.removeAttribute("data-lk-theme");
3539
4402
  }
3540
4403
  }, [targetKind, dataTheme]);
3541
- const inject = (0, import_react33.useCallback)(() => {
4404
+ const inject = (0, import_react38.useCallback)(() => {
3542
4405
  const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
3543
4406
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
3544
4407
  if (!el) return;
@@ -3555,7 +4418,7 @@ function ThemeProvider(props) {
3555
4418
  appliedKeysRef.current = /* @__PURE__ */ new Set();
3556
4419
  };
3557
4420
  }, [inject, targetKind]);
3558
- const value = (0, import_react33.useMemo)(
4421
+ const value = (0, import_react38.useMemo)(
3559
4422
  () => ({
3560
4423
  theme: effectiveTheme,
3561
4424
  preset,
@@ -3565,12 +4428,12 @@ function ThemeProvider(props) {
3565
4428
  [effectiveTheme, preset, mode, dataTheme]
3566
4429
  );
3567
4430
  if (targetKind === "document") {
3568
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
4431
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
3569
4432
  }
3570
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
4433
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
3571
4434
  }
3572
4435
  function useTheme() {
3573
- const ctx = (0, import_react33.useContext)(ThemeContext);
4436
+ const ctx = (0, import_react38.useContext)(ThemeContext);
3574
4437
  if (!ctx) {
3575
4438
  throw new Error("useTheme must be used within a ThemeProvider");
3576
4439
  }
@@ -3578,8 +4441,15 @@ function useTheme() {
3578
4441
  }
3579
4442
 
3580
4443
  // src/catalogV3Entries.ts
3581
- var import_core16 = require("@lessonkit/core");
3582
- var COMPOUND_PARENTS = ["Lesson", "Page", "InteractiveBook", "AssessmentSequence"];
4444
+ var import_core18 = require("@lessonkit/core");
4445
+ var COMPOUND_PARENTS = [
4446
+ "Lesson",
4447
+ "Page",
4448
+ "InteractiveBook",
4449
+ "Slide",
4450
+ "SlideDeck",
4451
+ "AssessmentSequence"
4452
+ ];
3583
4453
  function extendParents(entry) {
3584
4454
  if (!entry.parentConstraints?.length) return entry;
3585
4455
  const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
@@ -3643,8 +4513,8 @@ var v3CompoundAndContentEntries = [
3643
4513
  h5pMachineName: "H5P.Column",
3644
4514
  h5pAlias: "Column",
3645
4515
  description: "Column layout container (H5P Column / Page).",
3646
- allowedChildTypes: [...import_core16.PAGE_ALLOWED_CHILD_TYPES],
3647
- maxNestingDepth: import_core16.COMPOUND_MAX_NESTING_DEPTH.Page,
4516
+ allowedChildTypes: [...import_core18.PAGE_ALLOWED_CHILD_TYPES],
4517
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Page,
3648
4518
  props: [
3649
4519
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3650
4520
  { name: "title", type: "string", required: false, description: "Page title." },
@@ -3664,8 +4534,8 @@ var v3CompoundAndContentEntries = [
3664
4534
  h5pMachineName: "H5P.InteractiveBook",
3665
4535
  h5pAlias: "Interactive Book",
3666
4536
  description: "Multi-page book with chapter navigation.",
3667
- allowedChildTypes: [...import_core16.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
3668
- maxNestingDepth: import_core16.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
4537
+ allowedChildTypes: [...import_core18.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
4538
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
3669
4539
  props: [
3670
4540
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3671
4541
  { name: "title", type: "string", required: true, description: "Book title." },
@@ -3683,6 +4553,53 @@ var v3CompoundAndContentEntries = [
3683
4553
  theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
3684
4554
  telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
3685
4555
  },
4556
+ {
4557
+ type: "Slide",
4558
+ category: "container",
4559
+ compoundContract: true,
4560
+ h5pMachineName: "H5P.CoursePresentation",
4561
+ h5pAlias: "Course Presentation slide",
4562
+ description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
4563
+ allowedChildTypes: [...import_core18.SLIDE_ALLOWED_CHILD_TYPES],
4564
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Slide,
4565
+ props: [
4566
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4567
+ { name: "title", type: "string", required: false, description: "Slide title." },
4568
+ { name: "children", type: "ReactNode", required: true, description: "Slide content." }
4569
+ ],
4570
+ requiredIds: [],
4571
+ optionalIds: ["blockId"],
4572
+ parentConstraints: ["SlideDeck"],
4573
+ a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
4574
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
4575
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
4576
+ },
4577
+ {
4578
+ type: "SlideDeck",
4579
+ category: "container",
4580
+ compoundContract: true,
4581
+ h5pMachineName: "H5P.CoursePresentation",
4582
+ h5pAlias: "Course Presentation",
4583
+ description: "Multi-slide presentation with keyboard navigation.",
4584
+ allowedChildTypes: [...import_core18.SLIDE_DECK_ALLOWED_CHILD_TYPES],
4585
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
4586
+ props: [
4587
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4588
+ { name: "title", type: "string", required: true, description: "Deck title." },
4589
+ { name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
4590
+ { name: "children", type: "Slide[]", required: true, description: "Slides." }
4591
+ ],
4592
+ requiredIds: ["blockId"],
4593
+ parentConstraints: ["Lesson"],
4594
+ a11y: {
4595
+ element: "section",
4596
+ ariaLabel: "Slide deck",
4597
+ keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
4598
+ notes: "H5P Course Presentation equivalent."
4599
+ },
4600
+ theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
4601
+ telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
4602
+ },
3686
4603
  {
3687
4604
  type: "Accordion",
3688
4605
  category: "content",
@@ -3816,8 +4733,8 @@ function buildV3CatalogFromV2(v2) {
3816
4733
  return {
3817
4734
  ...base,
3818
4735
  compoundContract: true,
3819
- allowedChildTypes: [...import_core16.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
3820
- maxNestingDepth: import_core16.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
4736
+ allowedChildTypes: [...import_core18.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
4737
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
3821
4738
  };
3822
4739
  }
3823
4740
  return base;
@@ -4229,6 +5146,8 @@ function getBlockCatalogEntry(type, opts) {
4229
5146
  Quiz,
4230
5147
  Reflection,
4231
5148
  Scenario,
5149
+ Slide,
5150
+ SlideDeck,
4232
5151
  Text,
4233
5152
  ThemeProvider,
4234
5153
  TrueFalse,