@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components.tsx
2
- import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
2
+ import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
3
3
  import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
4
4
 
5
5
  // src/context.tsx
@@ -28,6 +28,44 @@ function createXapiQueueFromObservability(observability) {
28
28
  }
29
29
  return createInMemoryXAPIQueue(opts);
30
30
  }
31
+ function wrapBatchSink(batchSink, observability) {
32
+ if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
33
+ const onError = observability.onTelemetrySinkError;
34
+ return async (events) => {
35
+ try {
36
+ await batchSink(events);
37
+ } catch (err) {
38
+ onError(err, { sinkId: "tracking-batch" });
39
+ throw err;
40
+ }
41
+ };
42
+ }
43
+ function warnMissingProductionObservability(observability, opts) {
44
+ let isProduction = false;
45
+ try {
46
+ isProduction = import.meta.env?.PROD === true;
47
+ } catch {
48
+ }
49
+ if (!isProduction) {
50
+ const g = globalThis;
51
+ isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
52
+ }
53
+ if (!isProduction) return;
54
+ if (!opts.trackingEnabled && !opts.xapiEnabled) return;
55
+ const hooks = [
56
+ observability?.onTelemetrySinkError,
57
+ observability?.onTelemetryBufferDrop,
58
+ observability?.onXapiQueueDepth,
59
+ observability?.onXapiQueueCap,
60
+ observability?.onLxpackBridgeMiss
61
+ ];
62
+ if (hooks.some(Boolean)) return;
63
+ if (typeof console !== "undefined") {
64
+ console.warn(
65
+ "[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"
66
+ );
67
+ }
68
+ }
31
69
  function wrapTrackingSink(sink, observability) {
32
70
  if (!sink || !observability?.onTelemetrySinkError) return sink;
33
71
  const onError = observability.onTelemetrySinkError;
@@ -152,6 +190,9 @@ import {
152
190
  resetStoragePortForTests
153
191
  } from "@lessonkit/core";
154
192
 
193
+ // src/provider/useLessonkitProviderRuntime.ts
194
+ import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
195
+
155
196
  // src/runtime/progress.ts
156
197
  import { createProgressController } from "@lessonkit/core";
157
198
 
@@ -166,6 +207,7 @@ function createXapiClientFromConfig(config, queue) {
166
207
  return createXAPIClient({
167
208
  courseId: config.courseId,
168
209
  transport: config.xapi?.transport,
210
+ exitTransport: config.xapi?.exitTransport,
169
211
  queue
170
212
  });
171
213
  }
@@ -225,6 +267,7 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
225
267
  const statement = telemetryEventToXAPIStatement2(opts.event);
226
268
  if (statement) {
227
269
  opts.xapi.send(statement);
270
+ await opts.xapi.flush();
228
271
  xapiStatementSent = true;
229
272
  }
230
273
  }
@@ -260,7 +303,7 @@ function emitTelemetryWithPlugins(opts) {
260
303
  }
261
304
 
262
305
  // src/provider/courseStarted/emit.ts
263
- var courseStartedTrackingFlightKey = null;
306
+ var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
264
307
  function isTrackingActive(tracking) {
265
308
  return tracking?.enabled !== false;
266
309
  }
@@ -288,24 +331,40 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
288
331
  if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
289
332
  return true;
290
333
  }
291
- if (courseStartedTrackingFlightKey === flightKey) {
292
- return false;
334
+ const existing = courseStartedTrackingFlights.get(flightKey);
335
+ if (existing) {
336
+ const settled = await existing;
337
+ if (settled) return true;
293
338
  }
294
- courseStartedTrackingFlightKey = flightKey;
295
- try {
296
- if (shouldCommit && !shouldCommit()) return false;
297
- tracking.track(event);
298
- await tracking.flush?.();
299
- if (shouldCommit && !shouldCommit()) return false;
300
- markCourseStartedEmittedToTracking(storage, sessionId, courseId);
301
- return true;
302
- } catch {
303
- return false;
304
- } finally {
305
- if (courseStartedTrackingFlightKey === flightKey) {
306
- courseStartedTrackingFlightKey = null;
339
+ let resolveFlight;
340
+ const flight = new Promise((resolve) => {
341
+ resolveFlight = resolve;
342
+ });
343
+ courseStartedTrackingFlights.set(flightKey, flight);
344
+ void (async () => {
345
+ try {
346
+ if (shouldCommit && !shouldCommit()) {
347
+ resolveFlight(false);
348
+ return;
349
+ }
350
+ tracking.track(event);
351
+ const delivered = await tracking.flush?.();
352
+ if (delivered === false) {
353
+ resolveFlight(false);
354
+ return;
355
+ }
356
+ if (markCourseStartedEmittedToTracking(storage, sessionId, courseId) === false) {
357
+ resolveFlight(false);
358
+ return;
359
+ }
360
+ resolveFlight(true);
361
+ } catch {
362
+ resolveFlight(false);
363
+ } finally {
364
+ courseStartedTrackingFlights.delete(flightKey);
307
365
  }
308
- }
366
+ })();
367
+ return flight;
309
368
  }
310
369
  async function emitCourseStartedPipelineOnly(opts) {
311
370
  try {
@@ -319,8 +378,10 @@ async function emitCourseStartedPipelineOnly(opts) {
319
378
  skipXapi: opts.skipXapi
320
379
  });
321
380
  if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
322
- markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
323
- markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
381
+ if (markCourseStarted(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
382
+ if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
383
+ return "failed";
384
+ }
324
385
  if (xapiStatementSent) {
325
386
  opts.onXapiStatementSent?.();
326
387
  }
@@ -371,7 +432,9 @@ async function emitCourseStartedToTrackingOnly(opts) {
371
432
  extraSinks: opts.extraSinks,
372
433
  skipXapi: true
373
434
  });
374
- markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
435
+ if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
436
+ return "failed";
437
+ }
375
438
  return "emitted";
376
439
  } catch {
377
440
  return "failed";
@@ -424,13 +487,15 @@ function assertTrackingSinkConfig(tracking) {
424
487
 
425
488
  // src/runtime/telemetry.ts
426
489
  import { createTrackingClient } from "@lessonkit/core";
427
- function createTrackingClientFromConfig(config) {
490
+ function createTrackingClientFromConfig(config, observability) {
428
491
  if (config.tracking?.enabled === false) return createTrackingClient();
429
492
  if (config.tracking?.createClient) return config.tracking.createClient();
430
493
  return createTrackingClient({
431
494
  sink: config.tracking?.sink,
432
495
  batchSink: config.tracking?.batchSink,
433
- batch: config.tracking?.batch
496
+ batch: config.tracking?.batch,
497
+ exitBatchSink: config.tracking?.exitBatchSink,
498
+ onBufferDrop: observability?.onTelemetryBufferDrop
434
499
  });
435
500
  }
436
501
  async function disposeTrackingClient(client) {
@@ -505,6 +570,7 @@ function useLessonkitProviderRuntime(config) {
505
570
  const pendingCourseIdResetRef = useRef(false);
506
571
  const prevUseV2RuntimeRef = useRef(useV2Runtime);
507
572
  const xapiCourseStartedSentOnClientRef = useRef(false);
573
+ const xapiBootstrapSendRef = useRef(false);
508
574
  if (prevUseV2RuntimeRef.current !== useV2Runtime) {
509
575
  prevUseV2RuntimeRef.current = useV2Runtime;
510
576
  if (useV2Runtime) {
@@ -512,7 +578,8 @@ function useLessonkitProviderRuntime(config) {
512
578
  courseId: normalizedCourseId,
513
579
  runtimeVersion: "v2",
514
580
  session: normalizedConfig.session,
515
- plugins: pluginHostRef.current ?? normalizedConfig.plugins
581
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
582
+ deferPluginSetup: true
516
583
  });
517
584
  progressRef.current = headlessRef.current.progress;
518
585
  } else {
@@ -527,7 +594,8 @@ function useLessonkitProviderRuntime(config) {
527
594
  courseId: normalizedCourseId,
528
595
  runtimeVersion: "v2",
529
596
  session: normalizedConfig.session,
530
- plugins: pluginHostRef.current ?? normalizedConfig.plugins
597
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
598
+ deferPluginSetup: true
531
599
  });
532
600
  }
533
601
  if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
@@ -575,19 +643,22 @@ function useLessonkitProviderRuntime(config) {
575
643
  xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
576
644
  prevXapiCourseIdRef.current = courseId;
577
645
  xapiCourseStartedSentOnClientRef.current = false;
646
+ xapiBootstrapSendRef.current = false;
578
647
  }
579
648
  const prev = xapiRef.current;
580
649
  const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
581
650
  xapiRef.current = next;
582
651
  setXapi(next);
652
+ let bootstrapSent = false;
653
+ let bootstrapAlreadyStarted = false;
583
654
  if (next) {
584
655
  const sessionId = sessionIdRef.current;
585
656
  const cid = courseIdRef.current;
586
657
  const trackingActive = isTrackingActive(normalizedConfig.tracking);
587
- const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
658
+ bootstrapAlreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
588
659
  const clientChanged = !prev || prev !== next;
589
- const skipBootstrap = trackingActive && !alreadyStarted;
590
- const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
660
+ const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
661
+ const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
591
662
  if (needsBootstrap) {
592
663
  try {
593
664
  const event = buildCourseStartedEvent({
@@ -598,15 +669,12 @@ function useLessonkitProviderRuntime(config) {
598
669
  user: userRef.current,
599
670
  lxpackBridge: lxpackBridgeModeRef.current
600
671
  });
601
- if (event === null) {
602
- } else {
672
+ if (event !== null) {
603
673
  const statement = telemetryEventToXAPIStatement3(event);
604
674
  if (statement) {
605
675
  next.send(statement);
606
- if (!alreadyStarted) {
607
- markCourseStarted(defaultStorage, sessionId, cid);
608
- }
609
- xapiCourseStartedSentOnClientRef.current = true;
676
+ xapiBootstrapSendRef.current = true;
677
+ bootstrapSent = true;
610
678
  }
611
679
  }
612
680
  } catch {
@@ -624,6 +692,12 @@ function useLessonkitProviderRuntime(config) {
624
692
  if (cancelled) return;
625
693
  try {
626
694
  await next?.flush();
695
+ if (bootstrapSent && !cancelled) {
696
+ if (!bootstrapAlreadyStarted) {
697
+ markCourseStarted(defaultStorage, sessionIdRef.current, courseIdRef.current);
698
+ }
699
+ xapiCourseStartedSentOnClientRef.current = true;
700
+ }
627
701
  } catch {
628
702
  }
629
703
  })();
@@ -652,7 +726,10 @@ function useLessonkitProviderRuntime(config) {
652
726
  useIsoLayoutEffect(() => {
653
727
  const prev = trackingRef.current;
654
728
  const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
655
- const userBatchSink = normalizedConfig.tracking?.batchSink;
729
+ const userBatchSink = wrapBatchSink(
730
+ normalizedConfig.tracking?.batchSink,
731
+ observabilityRef.current
732
+ );
656
733
  assertTrackingSinkConfig(normalizedConfig.tracking);
657
734
  const sink = pluginHostRef.current && baseSink ? (
658
735
  /* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
@@ -672,9 +749,12 @@ function useLessonkitProviderRuntime(config) {
672
749
  }
673
750
  return userBatchSink(perEventForBatch);
674
751
  } : userBatchSink;
675
- const next = createTrackingClientFromConfig({
676
- tracking: { ...normalizedConfig.tracking, sink, batchSink }
677
- });
752
+ const next = createTrackingClientFromConfig(
753
+ {
754
+ tracking: { ...normalizedConfig.tracking, sink, batchSink }
755
+ },
756
+ observabilityRef.current
757
+ );
678
758
  trackingRef.current = next;
679
759
  trackingClientForUnmountRef.current = next;
680
760
  setTracking(next);
@@ -803,7 +883,11 @@ function useLessonkitProviderRuntime(config) {
803
883
  user: userRef.current,
804
884
  lxpackBridge: lxpackBridgeModeRef.current,
805
885
  onLxpackBridgeMiss,
806
- extraSinks: extraSinksRef.current
886
+ extraSinks: extraSinksRef.current,
887
+ skipXapi: xapiCourseStartedSentOnClientRef.current,
888
+ onXapiStatementSent: () => {
889
+ xapiCourseStartedSentOnClientRef.current = true;
890
+ }
807
891
  });
808
892
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
809
893
  }
@@ -859,20 +943,39 @@ function useLessonkitProviderRuntime(config) {
859
943
  }, []);
860
944
  useEffect(() => {
861
945
  if (typeof document === "undefined") return;
862
- const flushOnExit = () => {
863
- void xapiRef.current?.flush();
864
- void trackingRef.current?.flush?.();
946
+ const flushOnPageExit = () => {
947
+ try {
948
+ xapiRef.current?.flushOnExit?.();
949
+ trackingRef.current?.flushOnExit?.();
950
+ } finally {
951
+ void xapiRef.current?.flush();
952
+ void trackingRef.current?.flush?.();
953
+ }
865
954
  };
866
955
  const onVisibilityChange = () => {
867
- if (document.visibilityState === "hidden") flushOnExit();
956
+ if (document.visibilityState === "hidden") flushOnPageExit();
868
957
  };
869
958
  document.addEventListener("visibilitychange", onVisibilityChange);
870
- window.addEventListener("pagehide", flushOnExit);
959
+ window.addEventListener("pagehide", flushOnPageExit);
871
960
  return () => {
872
961
  document.removeEventListener("visibilitychange", onVisibilityChange);
873
- window.removeEventListener("pagehide", flushOnExit);
962
+ window.removeEventListener("pagehide", flushOnPageExit);
874
963
  };
875
964
  }, []);
965
+ useEffect(() => {
966
+ warnMissingProductionObservability(observabilityRef.current, {
967
+ trackingEnabled: isTrackingActive(normalizedConfig.tracking),
968
+ xapiEnabled: normalizedConfig.xapi?.enabled !== false && Boolean(
969
+ normalizedConfig.xapi?.client || normalizedConfig.xapi?.transport || normalizedConfig.xapi?.enabled === true
970
+ )
971
+ });
972
+ }, [
973
+ normalizedConfig.tracking,
974
+ normalizedConfig.xapi?.enabled,
975
+ normalizedConfig.xapi?.client,
976
+ normalizedConfig.xapi?.transport,
977
+ normalizedConfig.observability
978
+ ]);
876
979
  const setActiveLesson = useCallback(
877
980
  (lessonId) => {
878
981
  if (useV2Runtime && headlessRef.current) {
@@ -992,6 +1095,7 @@ function useLessonkitProviderRuntime(config) {
992
1095
  config: normalizedConfig,
993
1096
  tracking,
994
1097
  xapi,
1098
+ storage: defaultStorage,
995
1099
  session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
996
1100
  progress,
997
1101
  setActiveLesson,
@@ -1127,7 +1231,7 @@ function getLessonMountCount(lessonId) {
1127
1231
  }
1128
1232
 
1129
1233
  // src/components/Quiz.tsx
1130
- import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef3, useState as useState3 } from "react";
1234
+ import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
1131
1235
  import { visuallyHiddenStyle } from "@lessonkit/accessibility";
1132
1236
 
1133
1237
  // src/assessment/AssessmentLessonGuard.tsx
@@ -1186,6 +1290,12 @@ function readStringField(state, key) {
1186
1290
  if (typeof value === "string" || value === null) return value;
1187
1291
  return void 0;
1188
1292
  }
1293
+ function readNumberField(state, key) {
1294
+ const value = state[key];
1295
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1296
+ if (value === null) return null;
1297
+ return void 0;
1298
+ }
1189
1299
  function readBooleanStateField(state, key, apply) {
1190
1300
  const value = state[key];
1191
1301
  if (typeof value === "boolean") apply(value);
@@ -1195,53 +1305,81 @@ function readBooleanStateField(state, key, apply) {
1195
1305
  import { useImperativeHandle as useImperativeHandle2 } from "react";
1196
1306
 
1197
1307
  // src/compound/CompoundProvider.tsx
1198
- import React3, { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useImperativeHandle, useMemo as useMemo4, useRef as useRef2, useState as useState2 } from "react";
1308
+ import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
1199
1309
  import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
1200
1310
 
1201
1311
  // src/compound/aggregateScores.ts
1202
- function aggregateAssessmentScores(handles) {
1312
+ function aggregateAssessmentScores(handles, opts) {
1203
1313
  let score = 0;
1204
1314
  let maxScore = 0;
1205
1315
  let allAnswered = true;
1206
- for (const handle of handles) {
1316
+ for (const entry of handles) {
1317
+ const handle = "handle" in entry ? entry.handle : entry;
1318
+ const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
1207
1319
  score += handle.getScore();
1208
1320
  maxScore += handle.getMaxScore();
1209
- if (!handle.getAnswerGiven()) allAnswered = false;
1321
+ const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
1322
+ if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
1210
1323
  }
1211
1324
  return { score, maxScore, allAnswered };
1212
1325
  }
1213
1326
 
1214
- // src/compound/resumeChildHandles.ts
1215
- function resumeChildHandles(handles, childStates, opts) {
1216
- if (opts?.waitForHandles && handles.size === 0 && Object.keys(childStates).length > 0) {
1217
- return false;
1218
- }
1219
- for (const [checkId, handle] of handles) {
1220
- const child = childStates[checkId];
1221
- if (child && handle.resume) handle.resume(child);
1222
- }
1223
- return true;
1327
+ // src/compound/CompoundHydrationBridge.tsx
1328
+ import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
1329
+ import { jsx as jsx3 } from "react/jsx-runtime";
1330
+ var CompoundHydrationBridgeContext = createContext3(
1331
+ null
1332
+ );
1333
+ function CompoundHydrationBridgeProvider({ children }) {
1334
+ const bridgeRef = useRef2(null);
1335
+ return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
1336
+ }
1337
+ function useCompoundHydrationBridgeRef() {
1338
+ return useContext3(CompoundHydrationBridgeContext);
1339
+ }
1340
+
1341
+ // src/compound/CompoundPageIndexContext.tsx
1342
+ import { createContext as createContext4, useContext as useContext4 } from "react";
1343
+ import { jsx as jsx4 } from "react/jsx-runtime";
1344
+ var CompoundPageIndexContext = createContext4(void 0);
1345
+ function CompoundPageIndexProvider({
1346
+ pageIndex,
1347
+ children
1348
+ }) {
1349
+ return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
1350
+ }
1351
+ function useCompoundPageIndex() {
1352
+ return useContext4(CompoundPageIndexContext);
1224
1353
  }
1225
1354
 
1226
1355
  // src/compound/CompoundProvider.tsx
1227
- import { jsx as jsx3 } from "react/jsx-runtime";
1228
- var CompoundRegistryContext = createContext3(null);
1229
- var CompoundHandlesVersionContext = createContext3(0);
1356
+ import { jsx as jsx5 } from "react/jsx-runtime";
1357
+ var CompoundRegistryContext = createContext5(null);
1358
+ var CompoundHandlesVersionContext = createContext5(0);
1230
1359
  function CompoundProvider({
1231
1360
  children,
1232
1361
  activePageIndex: _activePageIndex,
1233
1362
  onActivePageIndexChange: _onActivePageIndexChange
1234
1363
  }) {
1235
- const registryRef = useRef2(/* @__PURE__ */ new Map());
1364
+ const registryRef = useRef3(/* @__PURE__ */ new Map());
1236
1365
  const [handlesVersion, setHandlesVersion] = useState2(0);
1237
- const register = useCallback2((checkId, handle) => {
1366
+ const register = useCallback2((checkId, handle, pageIndex) => {
1238
1367
  const prev = registryRef.current.get(checkId);
1239
- registryRef.current.set(checkId, handle);
1240
- if (prev !== handle) {
1368
+ if (prev && prev.handle !== handle) {
1369
+ const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
1370
+ if (isDevEnvironment4()) {
1371
+ console.error(message);
1372
+ } else {
1373
+ console.warn(message);
1374
+ }
1375
+ }
1376
+ registryRef.current.set(checkId, { handle, pageIndex });
1377
+ if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
1241
1378
  setHandlesVersion((v) => v + 1);
1242
1379
  }
1243
1380
  return () => {
1244
- if (registryRef.current.get(checkId) === handle) {
1381
+ const current = registryRef.current.get(checkId);
1382
+ if (current?.handle === handle) {
1245
1383
  registryRef.current.delete(checkId);
1246
1384
  setHandlesVersion((v) => v + 1);
1247
1385
  }
@@ -1250,30 +1388,39 @@ function CompoundProvider({
1250
1388
  const registryValue = useMemo4(
1251
1389
  () => ({
1252
1390
  register,
1253
- getHandles: () => registryRef.current
1391
+ getHandles: () => {
1392
+ const handles = /* @__PURE__ */ new Map();
1393
+ for (const [checkId, entry] of registryRef.current) {
1394
+ handles.set(checkId, entry.handle);
1395
+ }
1396
+ return handles;
1397
+ },
1398
+ getRegisteredHandles: () => registryRef.current
1254
1399
  }),
1255
1400
  [register]
1256
1401
  );
1257
- return /* @__PURE__ */ jsx3(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx3(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) });
1402
+ return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
1258
1403
  }
1259
1404
  function useCompoundRegistry() {
1260
- const registry = useContext3(CompoundRegistryContext);
1261
- const handlesVersion = useContext3(CompoundHandlesVersionContext);
1405
+ const registry = useContext5(CompoundRegistryContext);
1406
+ const handlesVersion = useContext5(CompoundHandlesVersionContext);
1262
1407
  if (!registry) return null;
1263
1408
  return { ...registry, handlesVersion };
1264
1409
  }
1265
1410
  function useCompoundHandlesVersion() {
1266
- return useContext3(CompoundHandlesVersionContext);
1411
+ return useContext5(CompoundHandlesVersionContext);
1267
1412
  }
1268
1413
  function useRegisterAssessmentHandle(checkId, handle) {
1269
- const registry = useContext3(CompoundRegistryContext);
1270
- React3.useEffect(() => {
1414
+ const registry = useContext5(CompoundRegistryContext);
1415
+ const pageIndex = useCompoundPageIndex();
1416
+ React5.useLayoutEffect(() => {
1271
1417
  if (!registry || !handle) return;
1272
- return registry.register(checkId, handle);
1273
- }, [registry, checkId, handle]);
1418
+ return registry.register(checkId, handle, pageIndex);
1419
+ }, [registry, checkId, handle, pageIndex]);
1274
1420
  }
1275
1421
  function useCompoundHandleRef(ref, opts) {
1276
- const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
1422
+ const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
1423
+ const bridgeRef = useCompoundHydrationBridgeRef();
1277
1424
  const setIndexClamped = useCallback2(
1278
1425
  (index) => {
1279
1426
  const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
@@ -1284,31 +1431,32 @@ function useCompoundHandleRef(ref, opts) {
1284
1431
  useImperativeHandle(
1285
1432
  ref,
1286
1433
  () => ({
1287
- getScore: () => aggregateAssessmentScores(getHandles().values()).score,
1288
- getMaxScore: () => aggregateAssessmentScores(getHandles().values()).maxScore,
1289
- getAnswerGiven: () => aggregateAssessmentScores(getHandles().values()).allAnswered,
1434
+ getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
1435
+ getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
1436
+ getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
1437
+ answerPageIndex: activePageIndex
1438
+ }).allAnswered,
1290
1439
  resetTask: () => {
1291
- for (const handle of getHandles().values()) handle.resetTask();
1440
+ for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
1292
1441
  },
1293
1442
  showSolutions: () => {
1294
1443
  if (!opts.enableSolutionsButton) return;
1295
- for (const handle of getHandles().values()) handle.showSolutions();
1444
+ for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
1296
1445
  },
1297
1446
  getCurrentState: () => {
1298
1447
  const childStates = {};
1299
- for (const [checkId, handle] of getHandles()) {
1300
- if (handle.getCurrentState) {
1301
- childStates[checkId] = handle.getCurrentState();
1448
+ for (const [checkId, entry] of getRegisteredHandles()) {
1449
+ if (entry.handle.getCurrentState) {
1450
+ childStates[checkId] = entry.handle.getCurrentState();
1302
1451
  }
1303
1452
  }
1304
1453
  return createCompoundResumeState({ activePageIndex, childStates });
1305
1454
  },
1306
1455
  resume: (state) => {
1307
- setIndexClamped(state.activePageIndex);
1308
- resumeChildHandles(getHandles(), state.childStates);
1456
+ bridgeRef?.current?.notifyImperativeResume(state);
1309
1457
  }
1310
1458
  }),
1311
- [activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
1459
+ [activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
1312
1460
  );
1313
1461
  }
1314
1462
 
@@ -1378,7 +1526,7 @@ function usePluginScoring(checkId, lessonId) {
1378
1526
  }
1379
1527
 
1380
1528
  // src/components/Quiz.tsx
1381
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1529
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
1382
1530
  function QuizInner(props, ref) {
1383
1531
  const { enclosingLessonId } = props;
1384
1532
  const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
@@ -1387,44 +1535,85 @@ function QuizInner(props, ref) {
1387
1535
  const [selected, setSelected] = useState3(null);
1388
1536
  const [selectionCorrect, setSelectionCorrect] = useState3(null);
1389
1537
  const [quizPassed, setQuizPassed] = useState3(false);
1390
- const completedRef = useRef3(false);
1538
+ const [completedScore, setCompletedScore] = useState3(null);
1539
+ const [completedMaxScore, setCompletedMaxScore] = useState3(null);
1540
+ const completedRef = useRef4(false);
1541
+ const telemetryReplayedRef = useRef4(false);
1391
1542
  const questionId = useId();
1392
1543
  const choicesKey = props.choices.join("\0");
1393
1544
  useEffect3(() => {
1394
1545
  completedRef.current = false;
1546
+ telemetryReplayedRef.current = false;
1395
1547
  setQuizPassed(false);
1396
1548
  setSelected(null);
1397
1549
  setSelectionCorrect(null);
1550
+ setCompletedScore(null);
1551
+ setCompletedMaxScore(null);
1398
1552
  }, [checkId, props.answer, props.question, choicesKey]);
1399
1553
  const passed = quizPassed;
1554
+ const resolveScores = () => {
1555
+ const maxScore = completedMaxScore ?? 1;
1556
+ if (quizPassed) {
1557
+ return { score: completedScore ?? maxScore, maxScore };
1558
+ }
1559
+ if (selected !== null && selectionCorrect) {
1560
+ return { score: completedMaxScore ?? maxScore, maxScore };
1561
+ }
1562
+ return { score: 0, maxScore };
1563
+ };
1564
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1565
+ if (!nextPassed || telemetryReplayedRef.current) return;
1566
+ telemetryReplayedRef.current = true;
1567
+ if (nextSelected !== null) {
1568
+ quiz.answer({
1569
+ checkId,
1570
+ question: props.question,
1571
+ choice: nextSelected,
1572
+ correct: nextCorrect ?? false
1573
+ });
1574
+ }
1575
+ quiz.complete({
1576
+ checkId,
1577
+ score: nextScore,
1578
+ maxScore: nextMaxScore,
1579
+ passingScore: props.passingScore ?? nextMaxScore
1580
+ });
1581
+ };
1400
1582
  const handle = useMemo5(
1401
1583
  () => buildAssessmentHandle({
1402
1584
  checkId,
1403
- getScore: () => {
1404
- const maxScore = 1;
1405
- if (quizPassed && selected !== null) return maxScore;
1406
- if (selected === null) return 0;
1407
- return selectionCorrect ? maxScore : 0;
1408
- },
1409
- getMaxScore: () => 1,
1585
+ getScore: () => resolveScores().score,
1586
+ getMaxScore: () => resolveScores().maxScore,
1410
1587
  getAnswerGiven: () => selected !== null,
1411
1588
  resetTask: () => {
1412
1589
  completedRef.current = false;
1590
+ telemetryReplayedRef.current = false;
1413
1591
  setQuizPassed(false);
1414
1592
  setSelected(null);
1415
1593
  setSelectionCorrect(null);
1594
+ setCompletedScore(null);
1595
+ setCompletedMaxScore(null);
1416
1596
  },
1417
1597
  showSolutions: () => {
1418
1598
  },
1419
- getXAPIData: () => ({
1420
- checkId,
1421
- interactionType: "mcq",
1422
- response: selected ?? void 0,
1423
- correct: selectionCorrect ?? void 0,
1424
- score: quizPassed && selected !== null ? 1 : selected === null ? 0 : selectionCorrect ? 1 : 0,
1425
- maxScore: 1
1599
+ getXAPIData: () => {
1600
+ const { score, maxScore } = resolveScores();
1601
+ return {
1602
+ checkId,
1603
+ interactionType: "mcq",
1604
+ response: selected ?? void 0,
1605
+ correct: selectionCorrect ?? void 0,
1606
+ score,
1607
+ maxScore
1608
+ };
1609
+ },
1610
+ getCurrentState: () => ({
1611
+ selected,
1612
+ selectionCorrect,
1613
+ quizPassed,
1614
+ completedScore,
1615
+ completedMaxScore
1426
1616
  }),
1427
- getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
1428
1617
  resume: (state) => {
1429
1618
  const nextSelected = readStringField(state, "selected");
1430
1619
  if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
@@ -1432,21 +1621,47 @@ function QuizInner(props, ref) {
1432
1621
  if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1433
1622
  setSelectionCorrect(nextCorrect);
1434
1623
  }
1435
- readBooleanStateField(state, "quizPassed", (value) => {
1436
- setQuizPassed(value);
1437
- completedRef.current = value;
1438
- });
1624
+ const nextCompletedScore = readNumberField(state, "completedScore");
1625
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1626
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1627
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1628
+ const nextPassed = readBooleanField(state, "quizPassed");
1629
+ if (nextPassed === true || nextPassed === false) {
1630
+ setQuizPassed(nextPassed);
1631
+ completedRef.current = nextPassed;
1632
+ if (nextPassed) {
1633
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1634
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
1635
+ replayTelemetry(
1636
+ nextSelected ?? null,
1637
+ nextCorrect ?? null,
1638
+ nextPassed,
1639
+ score,
1640
+ maxScore
1641
+ );
1642
+ }
1643
+ }
1439
1644
  }
1440
1645
  }),
1441
- [checkId, quizPassed, selected, selectionCorrect]
1646
+ [
1647
+ checkId,
1648
+ completedMaxScore,
1649
+ completedScore,
1650
+ props.passingScore,
1651
+ props.question,
1652
+ quiz,
1653
+ quizPassed,
1654
+ selected,
1655
+ selectionCorrect
1656
+ ]
1442
1657
  );
1443
1658
  useAssessmentHandleRegistration(checkId, handle, ref);
1444
1659
  return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1445
- /* @__PURE__ */ jsx4("p", { id: questionId, children: props.question }),
1660
+ /* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
1446
1661
  /* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
1447
- /* @__PURE__ */ jsx4("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
1662
+ /* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
1448
1663
  props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
1449
- /* @__PURE__ */ jsx4(
1664
+ /* @__PURE__ */ jsx6(
1450
1665
  "input",
1451
1666
  {
1452
1667
  type: "radio",
@@ -1471,9 +1686,12 @@ function QuizInner(props, ref) {
1471
1686
  completedRef.current = true;
1472
1687
  setQuizPassed(true);
1473
1688
  const maxScore = custom?.maxScore ?? 1;
1689
+ const score = custom?.score ?? maxScore;
1690
+ setCompletedScore(score);
1691
+ setCompletedMaxScore(maxScore);
1474
1692
  quiz.complete({
1475
1693
  checkId,
1476
- score: custom?.score ?? maxScore,
1694
+ score,
1477
1695
  maxScore,
1478
1696
  passingScore: props.passingScore ?? maxScore
1479
1697
  });
@@ -1484,15 +1702,15 @@ function QuizInner(props, ref) {
1484
1702
  c
1485
1703
  ] }, `${questionId}-${i}`))
1486
1704
  ] }),
1487
- selected && selectionCorrect !== null ? /* @__PURE__ */ jsx4("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1705
+ selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1488
1706
  ] });
1489
1707
  }
1490
1708
  var QuizInnerForwarded = forwardRef(QuizInner);
1491
1709
  var Quiz = forwardRef(function Quiz2(props, ref) {
1492
- return /* @__PURE__ */ jsx4(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx4(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1710
+ return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1493
1711
  });
1494
1712
  function KnowledgeCheck(props) {
1495
- return /* @__PURE__ */ jsx4(
1713
+ return /* @__PURE__ */ jsx6(
1496
1714
  Quiz,
1497
1715
  {
1498
1716
  checkId: props.checkId,
@@ -1508,16 +1726,16 @@ function resetQuizWarningsForTests() {
1508
1726
  }
1509
1727
 
1510
1728
  // src/components.tsx
1511
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1729
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
1512
1730
  function Course(props) {
1513
1731
  const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1514
1732
  const providerConfig = useMemo6(
1515
1733
  () => ({ ...props.config, courseId }),
1516
1734
  [props.config, courseId]
1517
1735
  );
1518
- return /* @__PURE__ */ jsx5(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
1519
- /* @__PURE__ */ jsx5("h1", { children: props.title }),
1520
- /* @__PURE__ */ jsx5("div", { children: props.children })
1736
+ return /* @__PURE__ */ jsx7(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
1737
+ /* @__PURE__ */ jsx7("h1", { children: props.title }),
1738
+ /* @__PURE__ */ jsx7("div", { children: props.children })
1521
1739
  ] }) });
1522
1740
  }
1523
1741
  function Lesson(props) {
@@ -1525,8 +1743,8 @@ function Lesson(props) {
1525
1743
  const autoComplete = props.autoCompleteOnUnmount !== false;
1526
1744
  const { setActiveLesson, config } = useLessonkit();
1527
1745
  const { completeLesson } = useCompletion();
1528
- const lessonMountGenerationRef = useRef4(0);
1529
- const liveCourseIdRef = useRef4(config.courseId);
1746
+ const lessonMountGenerationRef = useRef5(0);
1747
+ const liveCourseIdRef = useRef5(config.courseId);
1530
1748
  liveCourseIdRef.current = config.courseId;
1531
1749
  useEffect4(() => {
1532
1750
  const unregister = registerLessonMount(lessonId);
@@ -1553,9 +1771,9 @@ function Lesson(props) {
1553
1771
  });
1554
1772
  };
1555
1773
  }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1556
- return /* @__PURE__ */ jsx5(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
1557
- /* @__PURE__ */ jsx5("h2", { children: props.title }),
1558
- /* @__PURE__ */ jsx5("div", { children: props.children })
1774
+ return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
1775
+ /* @__PURE__ */ jsx7("h2", { children: props.title }),
1776
+ /* @__PURE__ */ jsx7("div", { children: props.children })
1559
1777
  ] }) });
1560
1778
  }
1561
1779
  function Scenario(props) {
@@ -1563,7 +1781,7 @@ function Scenario(props) {
1563
1781
  () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1564
1782
  [props.blockId]
1565
1783
  );
1566
- return /* @__PURE__ */ jsx5("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1784
+ return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1567
1785
  }
1568
1786
  function Reflection(props) {
1569
1787
  const blockId = useMemo6(
@@ -1580,10 +1798,10 @@ function Reflection(props) {
1580
1798
  props.onChange?.(event.target.value);
1581
1799
  };
1582
1800
  return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1583
- props.prompt ? /* @__PURE__ */ jsx5("p", { id: promptId, children: props.prompt }) : null,
1584
- props.hint ? /* @__PURE__ */ jsx5("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
1801
+ props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
1802
+ props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
1585
1803
  props.children,
1586
- /* @__PURE__ */ jsx5(
1804
+ /* @__PURE__ */ jsx7(
1587
1805
  "textarea",
1588
1806
  {
1589
1807
  value,
@@ -1601,7 +1819,7 @@ function ProgressTracker(props) {
1601
1819
  if (props.totalLessons != null) {
1602
1820
  const total = props.totalLessons;
1603
1821
  const displayed = Math.min(completed, total);
1604
- return /* @__PURE__ */ jsx5("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx5(
1822
+ return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
1605
1823
  "div",
1606
1824
  {
1607
1825
  role: "progressbar",
@@ -1618,15 +1836,15 @@ function ProgressTracker(props) {
1618
1836
  }
1619
1837
  ) });
1620
1838
  }
1621
- return /* @__PURE__ */ jsx5("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
1839
+ return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
1622
1840
  "Lessons completed: ",
1623
1841
  completed
1624
1842
  ] }) });
1625
1843
  }
1626
1844
 
1627
1845
  // src/blocks/TrueFalse.tsx
1628
- import React7, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef5, useState as useState5 } from "react";
1629
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1846
+ import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
1847
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1630
1848
  var INTERACTION = "trueFalse";
1631
1849
  function TrueFalseInner(props, ref) {
1632
1850
  const { enclosingLessonId } = props;
@@ -1638,38 +1856,81 @@ function TrueFalseInner(props, ref) {
1638
1856
  const [selectionCorrect, setSelectionCorrect] = useState5(null);
1639
1857
  const [showSolutions, setShowSolutions] = useState5(false);
1640
1858
  const [passed, setPassed] = useState5(false);
1641
- const completedRef = useRef5(false);
1642
- const questionId = React7.useId();
1859
+ const [completedScore, setCompletedScore] = useState5(null);
1860
+ const [completedMaxScore, setCompletedMaxScore] = useState5(null);
1861
+ const completedRef = useRef6(false);
1862
+ const telemetryReplayedRef = useRef6(false);
1863
+ const questionId = React9.useId();
1643
1864
  const reset = () => {
1644
1865
  completedRef.current = false;
1866
+ telemetryReplayedRef.current = false;
1645
1867
  setPassed(false);
1646
1868
  setSelected(null);
1647
1869
  setSelectionCorrect(null);
1648
1870
  setShowSolutions(false);
1871
+ setCompletedScore(null);
1872
+ setCompletedMaxScore(null);
1649
1873
  };
1650
1874
  useEffect5(() => {
1651
1875
  reset();
1652
1876
  }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1877
+ const resolveScores = () => {
1878
+ const maxScore = completedMaxScore ?? 1;
1879
+ if (passed) {
1880
+ return { score: completedScore ?? maxScore, maxScore };
1881
+ }
1882
+ if (selectionCorrect) {
1883
+ return { score: completedMaxScore ?? maxScore, maxScore };
1884
+ }
1885
+ return { score: 0, maxScore };
1886
+ };
1887
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1888
+ if (!nextPassed || telemetryReplayedRef.current) return;
1889
+ telemetryReplayedRef.current = true;
1890
+ if (nextSelected !== null) {
1891
+ assessment.answer({
1892
+ checkId,
1893
+ interactionType: INTERACTION,
1894
+ question: props.question,
1895
+ response: nextSelected,
1896
+ correct: nextCorrect ?? false
1897
+ });
1898
+ }
1899
+ assessment.complete({
1900
+ checkId,
1901
+ interactionType: INTERACTION,
1902
+ score: nextScore,
1903
+ maxScore: nextMaxScore,
1904
+ passingScore: props.passingScore ?? nextMaxScore
1905
+ });
1906
+ };
1653
1907
  const handle = useMemo7(
1654
1908
  () => buildAssessmentHandle({
1655
1909
  checkId,
1656
- getScore: () => {
1657
- const maxScore = 1;
1658
- return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
1659
- },
1660
- getMaxScore: () => 1,
1910
+ getScore: () => resolveScores().score,
1911
+ getMaxScore: () => resolveScores().maxScore,
1661
1912
  getAnswerGiven: () => selected !== null,
1662
1913
  resetTask: reset,
1663
1914
  showSolutions: () => setShowSolutions(true),
1664
- getXAPIData: () => ({
1665
- checkId,
1666
- interactionType: INTERACTION,
1667
- response: selected ?? void 0,
1668
- correct: selected === props.answer,
1669
- score: passed ? 1 : selected === null ? 0 : selected === props.answer ? 1 : 0,
1670
- maxScore: 1
1915
+ getXAPIData: () => {
1916
+ const { score, maxScore } = resolveScores();
1917
+ return {
1918
+ checkId,
1919
+ interactionType: INTERACTION,
1920
+ response: selected ?? void 0,
1921
+ correct: selectionCorrect ?? void 0,
1922
+ score,
1923
+ maxScore
1924
+ };
1925
+ },
1926
+ getCurrentState: () => ({
1927
+ selected,
1928
+ selectionCorrect,
1929
+ passed,
1930
+ showSolutions,
1931
+ completedScore,
1932
+ completedMaxScore
1671
1933
  }),
1672
- getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
1673
1934
  resume: (state) => {
1674
1935
  const nextSelected = readBooleanField(state, "selected");
1675
1936
  if (nextSelected === true || nextSelected === false || nextSelected === null) {
@@ -1679,14 +1940,35 @@ function TrueFalseInner(props, ref) {
1679
1940
  if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1680
1941
  setSelectionCorrect(nextCorrect);
1681
1942
  }
1682
- readBooleanStateField(state, "passed", (value) => {
1683
- setPassed(value);
1684
- completedRef.current = value;
1685
- });
1943
+ const nextCompletedScore = readNumberField(state, "completedScore");
1944
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1945
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1946
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1947
+ const nextPassed = readBooleanField(state, "passed");
1948
+ if (nextPassed === true || nextPassed === false) {
1949
+ setPassed(nextPassed);
1950
+ completedRef.current = nextPassed;
1951
+ if (nextPassed) {
1952
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1953
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
1954
+ replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
1955
+ }
1956
+ }
1686
1957
  readBooleanStateField(state, "showSolutions", setShowSolutions);
1687
1958
  }
1688
1959
  }),
1689
- [checkId, passed, props.answer, selected, selectionCorrect, showSolutions]
1960
+ [
1961
+ assessment,
1962
+ checkId,
1963
+ completedMaxScore,
1964
+ completedScore,
1965
+ passed,
1966
+ props.passingScore,
1967
+ props.question,
1968
+ selected,
1969
+ selectionCorrect,
1970
+ showSolutions
1971
+ ]
1690
1972
  );
1691
1973
  useAssessmentHandleRegistration(checkId, handle, ref);
1692
1974
  const submit = (value) => {
@@ -1705,6 +1987,8 @@ function TrueFalseInner(props, ref) {
1705
1987
  if (scored.passed && !completedRef.current) {
1706
1988
  completedRef.current = true;
1707
1989
  setPassed(true);
1990
+ setCompletedScore(scored.score);
1991
+ setCompletedMaxScore(scored.maxScore);
1708
1992
  assessment.complete({
1709
1993
  checkId,
1710
1994
  interactionType: INTERACTION,
@@ -1716,11 +2000,11 @@ function TrueFalseInner(props, ref) {
1716
2000
  };
1717
2001
  const reveal = showSolutions || passed && props.enableSolutionsButton;
1718
2002
  return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1719
- /* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
2003
+ /* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
1720
2004
  /* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
1721
- /* @__PURE__ */ jsx6("legend", { className: "lk-visually-hidden", children: "True or False" }),
2005
+ /* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
1722
2006
  /* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
1723
- /* @__PURE__ */ jsx6(
2007
+ /* @__PURE__ */ jsx8(
1724
2008
  "input",
1725
2009
  {
1726
2010
  type: "radio",
@@ -1733,7 +2017,7 @@ function TrueFalseInner(props, ref) {
1733
2017
  "True"
1734
2018
  ] }),
1735
2019
  /* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
1736
- /* @__PURE__ */ jsx6(
2020
+ /* @__PURE__ */ jsx8(
1737
2021
  "input",
1738
2022
  {
1739
2023
  type: "radio",
@@ -1748,21 +2032,21 @@ function TrueFalseInner(props, ref) {
1748
2032
  ] }),
1749
2033
  reveal ? /* @__PURE__ */ jsxs4("p", { children: [
1750
2034
  "Correct answer: ",
1751
- /* @__PURE__ */ jsx6("strong", { children: props.answer ? "True" : "False" })
2035
+ /* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
1752
2036
  ] }) : null,
1753
- selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1754
- props.enableRetry && passed ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1755
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2037
+ selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
2038
+ props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2039
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1756
2040
  ] });
1757
2041
  }
1758
2042
  var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
1759
2043
  var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
1760
- return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2044
+ return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1761
2045
  });
1762
2046
 
1763
2047
  // src/blocks/MarkTheWords.tsx
1764
- import React8, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef6, useState as useState6 } from "react";
1765
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
2048
+ import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
2049
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1766
2050
  var INTERACTION2 = "markTheWords";
1767
2051
  function tokenize(text) {
1768
2052
  return text.split(/(\s+)/).filter((t) => t.length > 0);
@@ -1778,7 +2062,7 @@ function MarkTheWordsInner(props, ref) {
1778
2062
  const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
1779
2063
  const [passed, setPassed] = useState6(false);
1780
2064
  const [showSolutions, setShowSolutions] = useState6(false);
1781
- const completedRef = useRef6(false);
2065
+ const completedRef = useRef7(false);
1782
2066
  const reset = () => {
1783
2067
  completedRef.current = false;
1784
2068
  setPassed(false);
@@ -1857,7 +2141,7 @@ function MarkTheWordsInner(props, ref) {
1857
2141
  interactionType: INTERACTION2,
1858
2142
  question: props.text,
1859
2143
  response: [...marked].map((i) => tokens[i]),
1860
- correct: true
2144
+ correct: passedThreshold
1861
2145
  });
1862
2146
  assessment.complete({
1863
2147
  checkId,
@@ -1882,17 +2166,17 @@ function MarkTheWordsInner(props, ref) {
1882
2166
  return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1883
2167
  !hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
1884
2168
  "No words in this sentence match ",
1885
- /* @__PURE__ */ jsx7("code", { children: "correctWords" }),
2169
+ /* @__PURE__ */ jsx9("code", { children: "correctWords" }),
1886
2170
  ". Check spelling and capitalization in the source text."
1887
2171
  ] }) : null,
1888
- /* @__PURE__ */ jsx7("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
1889
- /* @__PURE__ */ jsx7("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
2172
+ /* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
2173
+ /* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1890
2174
  const isWord = !/^\s+$/.test(token);
1891
2175
  const isTarget = isWord && correctSet.has(token.toLowerCase());
1892
- if (!isTarget) return /* @__PURE__ */ jsx7(React8.Fragment, { children: token }, i);
2176
+ if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
1893
2177
  const selected = marked.has(i);
1894
2178
  const solution = showSolutions || passed && props.enableSolutionsButton;
1895
- return /* @__PURE__ */ jsx7(
2179
+ return /* @__PURE__ */ jsx9(
1896
2180
  "button",
1897
2181
  {
1898
2182
  type: "button",
@@ -1910,18 +2194,18 @@ function MarkTheWordsInner(props, ref) {
1910
2194
  i
1911
2195
  );
1912
2196
  }) }),
1913
- allMarked ? /* @__PURE__ */ jsx7("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
1914
- props.enableRetry && passed ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1915
- props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2197
+ allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
2198
+ props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2199
+ props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1916
2200
  ] });
1917
2201
  }
1918
2202
  var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
1919
2203
  var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
1920
- return /* @__PURE__ */ jsx7(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx7(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2204
+ return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1921
2205
  });
1922
2206
 
1923
2207
  // src/blocks/FillInTheBlanks.tsx
1924
- import React9, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef7, useState as useState7 } from "react";
2208
+ import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
1925
2209
 
1926
2210
  // src/assessment/internal/parseStarDelimitedTemplate.ts
1927
2211
  function parseStarDelimitedTemplate(template, idPrefix) {
@@ -1942,7 +2226,7 @@ function parseStarDelimitedTemplate(template, idPrefix) {
1942
2226
  }
1943
2227
 
1944
2228
  // src/blocks/FillInTheBlanks.tsx
1945
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2229
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1946
2230
  var INTERACTION3 = "fillInBlanks";
1947
2231
  function parseTemplate(template) {
1948
2232
  const { parts, values } = parseStarDelimitedTemplate(template, "blank");
@@ -1961,14 +2245,20 @@ function FillInTheBlanksInner(props, ref) {
1961
2245
  );
1962
2246
  const [passed, setPassed] = useState7(false);
1963
2247
  const [showSolutions, setShowSolutions] = useState7(false);
1964
- const completedRef = useRef7(false);
1965
- const answeredRef = useRef7(false);
2248
+ const [submitted, setSubmitted] = useState7(false);
2249
+ const completedRef = useRef8(false);
2250
+ const answeredRef = useRef8(false);
2251
+ const checkSnapshotRef = useRef8(null);
2252
+ const telemetryReplayedRef = useRef8(false);
1966
2253
  const reset = () => {
1967
2254
  completedRef.current = false;
1968
2255
  answeredRef.current = false;
2256
+ checkSnapshotRef.current = null;
2257
+ telemetryReplayedRef.current = false;
1969
2258
  setPassed(false);
1970
2259
  setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
1971
2260
  setShowSolutions(false);
2261
+ setSubmitted(false);
1972
2262
  };
1973
2263
  useEffect7(() => {
1974
2264
  reset();
@@ -1981,6 +2271,31 @@ function FillInTheBlanksInner(props, ref) {
1981
2271
  });
1982
2272
  const maxScore = blanks.length;
1983
2273
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2274
+ const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2275
+ if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2276
+ telemetryReplayedRef.current = true;
2277
+ const nextPassedThreshold = meetsPassingThreshold(
2278
+ nextScore,
2279
+ nextMaxScore || 1,
2280
+ props.passingScore
2281
+ );
2282
+ assessment.answer({
2283
+ checkId,
2284
+ interactionType: INTERACTION3,
2285
+ question: props.template,
2286
+ response: nextValues,
2287
+ correct: nextPassedThreshold
2288
+ });
2289
+ if (nextPassed || nextPassedThreshold) {
2290
+ assessment.complete({
2291
+ checkId,
2292
+ interactionType: INTERACTION3,
2293
+ score: nextScore,
2294
+ maxScore: nextMaxScore,
2295
+ passingScore: props.passingScore ?? nextMaxScore
2296
+ });
2297
+ }
2298
+ };
1984
2299
  const handle = useMemo9(
1985
2300
  () => buildAssessmentHandle({
1986
2301
  checkId,
@@ -1997,19 +2312,36 @@ function FillInTheBlanksInner(props, ref) {
1997
2312
  score,
1998
2313
  maxScore: maxScore || 1
1999
2314
  }),
2000
- getCurrentState: () => ({ values, passed, showSolutions }),
2315
+ getCurrentState: () => ({ values, passed, showSolutions, submitted }),
2001
2316
  resume: (state) => {
2002
2317
  const raw = state.values;
2003
- if (raw && typeof raw === "object") setValues({ ...raw });
2318
+ let nextValues = values;
2319
+ if (raw && typeof raw === "object") {
2320
+ nextValues = { ...raw };
2321
+ setValues(nextValues);
2322
+ }
2323
+ let nextPassed = passed;
2324
+ let nextSubmitted = submitted;
2004
2325
  readBooleanStateField(state, "passed", (value) => {
2326
+ nextPassed = value;
2005
2327
  setPassed(value);
2006
2328
  completedRef.current = value;
2007
2329
  answeredRef.current = value;
2008
2330
  });
2009
2331
  readBooleanStateField(state, "showSolutions", setShowSolutions);
2332
+ readBooleanStateField(state, "submitted", (value) => {
2333
+ nextSubmitted = value;
2334
+ setSubmitted(value);
2335
+ if (value) answeredRef.current = true;
2336
+ });
2337
+ let nextScore = 0;
2338
+ blanks.forEach((b) => {
2339
+ if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
2340
+ });
2341
+ replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
2010
2342
  }
2011
2343
  }),
2012
- [allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
2344
+ [allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
2013
2345
  );
2014
2346
  useAssessmentHandleRegistration(checkId, handle, ref);
2015
2347
  const check = () => {
@@ -2020,16 +2352,19 @@ function FillInTheBlanksInner(props, ref) {
2020
2352
  return;
2021
2353
  }
2022
2354
  if (!allFilled) return;
2023
- if (!answeredRef.current) {
2024
- answeredRef.current = true;
2025
- assessment.answer({
2026
- checkId,
2027
- interactionType: INTERACTION3,
2028
- question: props.template,
2029
- response: values,
2030
- correct: passedThreshold
2031
- });
2032
- }
2355
+ if (passed) return;
2356
+ const snapshot = JSON.stringify(values);
2357
+ if (checkSnapshotRef.current === snapshot) return;
2358
+ checkSnapshotRef.current = snapshot;
2359
+ answeredRef.current = true;
2360
+ setSubmitted(true);
2361
+ assessment.answer({
2362
+ checkId,
2363
+ interactionType: INTERACTION3,
2364
+ question: props.template,
2365
+ response: values,
2366
+ correct: passedThreshold
2367
+ });
2033
2368
  if (passedThreshold && !completedRef.current) {
2034
2369
  completedRef.current = true;
2035
2370
  setPassed(true);
@@ -2043,19 +2378,23 @@ function FillInTheBlanksInner(props, ref) {
2043
2378
  }
2044
2379
  };
2045
2380
  useEffect7(() => {
2046
- if (!allFilled) answeredRef.current = false;
2381
+ if (!allFilled) {
2382
+ answeredRef.current = false;
2383
+ checkSnapshotRef.current = null;
2384
+ setSubmitted(false);
2385
+ }
2047
2386
  }, [allFilled]);
2048
2387
  useEffect7(() => {
2049
- if (props.autoCheck && allFilled) check();
2050
- }, [allFilled, props.autoCheck, values, passedThreshold]);
2388
+ if (props.autoCheck && allFilled && !passed) check();
2389
+ }, [allFilled, props.autoCheck, values, passedThreshold, passed]);
2051
2390
  const reveal = showSolutions || passed && props.enableSolutionsButton;
2052
2391
  return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2053
- /* @__PURE__ */ jsx8("p", { children: parsed.parts.map((part, i) => {
2392
+ /* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
2054
2393
  const blank = blanks.find((b) => b.id === part);
2055
- if (!blank) return /* @__PURE__ */ jsx8(React9.Fragment, { children: part }, i);
2394
+ if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
2056
2395
  return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
2057
- /* @__PURE__ */ jsx8("span", { className: "lk-visually-hidden", children: blank.answer }),
2058
- /* @__PURE__ */ jsx8(
2396
+ /* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
2397
+ /* @__PURE__ */ jsx10(
2059
2398
  "input",
2060
2399
  {
2061
2400
  type: "text",
@@ -2071,23 +2410,23 @@ function FillInTheBlanksInner(props, ref) {
2071
2410
  )
2072
2411
  ] }, blank.id);
2073
2412
  }) }),
2074
- !props.autoCheck ? /* @__PURE__ */ jsx8("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2075
- !hasBlanks ? /* @__PURE__ */ jsx8("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2076
- allFilled ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2077
- props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2078
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2413
+ !props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2414
+ !hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2415
+ submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2416
+ props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2417
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2079
2418
  ] });
2080
2419
  }
2081
2420
  var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
2082
2421
  var FillInTheBlanks = forwardRef4(
2083
2422
  function FillInTheBlanks2(props, ref) {
2084
- return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2423
+ return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2085
2424
  }
2086
2425
  );
2087
2426
 
2088
2427
  // src/blocks/DragTheWords.tsx
2089
- import React10, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef8, useState as useState8 } from "react";
2090
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2428
+ import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
2429
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2091
2430
  var INTERACTION4 = "dragTheWords";
2092
2431
  function parseZones(template) {
2093
2432
  const { parts, values } = parseStarDelimitedTemplate(template, "zone");
@@ -2103,12 +2442,18 @@ function DragTheWordsInner(props, ref) {
2103
2442
  const [pool, setPool] = useState8(() => [...props.words]);
2104
2443
  const [keyboardWord, setKeyboardWord] = useState8(null);
2105
2444
  const [passed, setPassed] = useState8(false);
2106
- const completedRef = useRef8(false);
2107
- const answeredRef = useRef8(false);
2445
+ const [submitted, setSubmitted] = useState8(false);
2446
+ const completedRef = useRef9(false);
2447
+ const answeredRef = useRef9(false);
2448
+ const checkSnapshotRef = useRef9(null);
2449
+ const telemetryReplayedRef = useRef9(false);
2108
2450
  const reset = () => {
2109
2451
  completedRef.current = false;
2110
2452
  answeredRef.current = false;
2453
+ checkSnapshotRef.current = null;
2454
+ telemetryReplayedRef.current = false;
2111
2455
  setPassed(false);
2456
+ setSubmitted(false);
2112
2457
  setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
2113
2458
  setPool([...props.words]);
2114
2459
  setKeyboardWord(null);
@@ -2124,6 +2469,31 @@ function DragTheWordsInner(props, ref) {
2124
2469
  });
2125
2470
  const maxScore = answers.length;
2126
2471
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2472
+ const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2473
+ if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2474
+ telemetryReplayedRef.current = true;
2475
+ const nextPassedThreshold = meetsPassingThreshold(
2476
+ nextScore,
2477
+ nextMaxScore || 1,
2478
+ props.passingScore
2479
+ );
2480
+ assessment.answer({
2481
+ checkId,
2482
+ interactionType: INTERACTION4,
2483
+ question: props.template,
2484
+ response: nextZones,
2485
+ correct: nextPassedThreshold
2486
+ });
2487
+ if (nextPassed || nextPassedThreshold) {
2488
+ assessment.complete({
2489
+ checkId,
2490
+ interactionType: INTERACTION4,
2491
+ score: nextScore,
2492
+ maxScore: nextMaxScore,
2493
+ passingScore: props.passingScore ?? nextMaxScore
2494
+ });
2495
+ }
2496
+ };
2127
2497
  const handle = useMemo10(
2128
2498
  () => buildAssessmentHandle({
2129
2499
  checkId,
@@ -2141,21 +2511,38 @@ function DragTheWordsInner(props, ref) {
2141
2511
  score,
2142
2512
  maxScore: maxScore || 1
2143
2513
  }),
2144
- getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
2514
+ getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
2145
2515
  resume: (state) => {
2146
2516
  const rawZones = state.zones;
2147
- if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
2517
+ let nextZones = zones;
2518
+ if (rawZones && typeof rawZones === "object") {
2519
+ nextZones = { ...rawZones };
2520
+ setZones(nextZones);
2521
+ }
2148
2522
  if (Array.isArray(state.pool)) setPool([...state.pool]);
2523
+ let nextPassed = passed;
2524
+ let nextSubmitted = submitted;
2149
2525
  readBooleanStateField(state, "passed", (value) => {
2526
+ nextPassed = value;
2150
2527
  setPassed(value);
2151
2528
  completedRef.current = value;
2152
2529
  answeredRef.current = value;
2153
2530
  });
2531
+ readBooleanStateField(state, "submitted", (value) => {
2532
+ nextSubmitted = value;
2533
+ setSubmitted(value);
2534
+ if (value) answeredRef.current = true;
2535
+ });
2154
2536
  const kw = state.keyboardWord;
2155
2537
  if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2538
+ let nextScore = 0;
2539
+ answers.forEach((ans, i) => {
2540
+ if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
2541
+ });
2542
+ replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
2156
2543
  }
2157
2544
  }),
2158
- [allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
2545
+ [allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
2159
2546
  );
2160
2547
  useAssessmentHandleRegistration(checkId, handle, ref);
2161
2548
  const placeInZone = (zoneId, word) => {
@@ -2185,16 +2572,19 @@ function DragTheWordsInner(props, ref) {
2185
2572
  return;
2186
2573
  }
2187
2574
  if (!allFilled) return;
2188
- if (!answeredRef.current) {
2189
- answeredRef.current = true;
2190
- assessment.answer({
2191
- checkId,
2192
- interactionType: INTERACTION4,
2193
- question: props.template,
2194
- response: zones,
2195
- correct: passedThreshold
2196
- });
2197
- }
2575
+ if (passed) return;
2576
+ const snapshot = JSON.stringify(zones);
2577
+ if (checkSnapshotRef.current === snapshot) return;
2578
+ checkSnapshotRef.current = snapshot;
2579
+ answeredRef.current = true;
2580
+ setSubmitted(true);
2581
+ assessment.answer({
2582
+ checkId,
2583
+ interactionType: INTERACTION4,
2584
+ question: props.template,
2585
+ response: zones,
2586
+ correct: passedThreshold
2587
+ });
2198
2588
  if (passedThreshold && !completedRef.current) {
2199
2589
  completedRef.current = true;
2200
2590
  setPassed(true);
@@ -2208,14 +2598,18 @@ function DragTheWordsInner(props, ref) {
2208
2598
  }
2209
2599
  };
2210
2600
  useEffect8(() => {
2211
- if (!allFilled) answeredRef.current = false;
2601
+ if (!allFilled) {
2602
+ answeredRef.current = false;
2603
+ checkSnapshotRef.current = null;
2604
+ setSubmitted(false);
2605
+ }
2212
2606
  }, [allFilled]);
2213
2607
  useEffect8(() => {
2214
- if (props.autoCheck && allFilled) check();
2215
- }, [allFilled, props.autoCheck, zones, passedThreshold]);
2608
+ if (props.autoCheck && allFilled && !passed) check();
2609
+ }, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
2216
2610
  return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2217
- /* @__PURE__ */ jsx9("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2218
- /* @__PURE__ */ jsx9("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx9(
2611
+ /* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2612
+ /* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
2219
2613
  "button",
2220
2614
  {
2221
2615
  type: "button",
@@ -2229,9 +2623,9 @@ function DragTheWordsInner(props, ref) {
2229
2623
  },
2230
2624
  word
2231
2625
  )) }),
2232
- /* @__PURE__ */ jsx9("p", { children: parts.map((part, i) => {
2233
- if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx9(React10.Fragment, { children: part }, i);
2234
- return /* @__PURE__ */ jsx9(
2626
+ /* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
2627
+ if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
2628
+ return /* @__PURE__ */ jsx11(
2235
2629
  "span",
2236
2630
  {
2237
2631
  role: "button",
@@ -2255,19 +2649,19 @@ function DragTheWordsInner(props, ref) {
2255
2649
  part
2256
2650
  );
2257
2651
  }) }),
2258
- /* @__PURE__ */ jsx9("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2259
- !hasZones ? /* @__PURE__ */ jsx9("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2260
- allFilled ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2652
+ /* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2653
+ !hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2654
+ submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2261
2655
  ] });
2262
2656
  }
2263
2657
  var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
2264
2658
  var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
2265
- return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2659
+ return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2266
2660
  });
2267
2661
 
2268
2662
  // src/blocks/DragAndDrop.tsx
2269
- import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef9, useState as useState9 } from "react";
2270
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2663
+ import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
2664
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2271
2665
  var INTERACTION5 = "dragAndDrop";
2272
2666
  function DragAndDropInner(props, ref) {
2273
2667
  const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
@@ -2278,10 +2672,12 @@ function DragAndDropInner(props, ref) {
2278
2672
  const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
2279
2673
  const [keyboardItem, setKeyboardItem] = useState9(null);
2280
2674
  const [passed, setPassed] = useState9(false);
2281
- const completedRef = useRef9(false);
2675
+ const [checked, setChecked] = useState9(false);
2676
+ const completedRef = useRef10(false);
2282
2677
  const reset = () => {
2283
2678
  completedRef.current = false;
2284
2679
  setPassed(false);
2680
+ setChecked(false);
2285
2681
  setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2286
2682
  setPool(props.items.map((i) => i.id));
2287
2683
  setKeyboardItem(null);
@@ -2289,19 +2685,20 @@ function DragAndDropInner(props, ref) {
2289
2685
  useEffect9(() => {
2290
2686
  reset();
2291
2687
  }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2292
- const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2293
- const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
2688
+ const hasTargets = props.targets.length > 0;
2689
+ const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2690
+ let score = 0;
2691
+ props.targets.forEach((t) => {
2692
+ if (assignments[t.id] === t.accepts) score += 1;
2693
+ });
2694
+ const maxScore = props.targets.length || 1;
2695
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
2294
2696
  const handle = useMemo11(() => {
2295
- const maxScore = props.targets.length || 1;
2296
- let score = 0;
2297
- props.targets.forEach((t) => {
2298
- if (assignments[t.id] === t.accepts) score += 1;
2299
- });
2300
2697
  return buildAssessmentHandle({
2301
2698
  checkId,
2302
2699
  getScore: () => score,
2303
2700
  getMaxScore: () => maxScore,
2304
- getAnswerGiven: () => allFilled,
2701
+ getAnswerGiven: () => hasTargets && allFilled,
2305
2702
  resetTask: reset,
2306
2703
  showSolutions: () => {
2307
2704
  },
@@ -2309,11 +2706,11 @@ function DragAndDropInner(props, ref) {
2309
2706
  checkId,
2310
2707
  interactionType: INTERACTION5,
2311
2708
  response: assignments,
2312
- correct: allCorrect,
2709
+ correct: passedThreshold,
2313
2710
  score,
2314
2711
  maxScore
2315
2712
  }),
2316
- getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
2713
+ getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
2317
2714
  resume: (state) => {
2318
2715
  const rawAssignments = state.assignments;
2319
2716
  if (rawAssignments && typeof rawAssignments === "object") {
@@ -2324,14 +2721,16 @@ function DragAndDropInner(props, ref) {
2324
2721
  setPassed(value);
2325
2722
  completedRef.current = value;
2326
2723
  });
2724
+ readBooleanStateField(state, "checked", setChecked);
2327
2725
  const item = state.keyboardItem;
2328
2726
  if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
2329
2727
  }
2330
2728
  });
2331
- }, [allCorrect, allFilled, assignments, checkId, keyboardItem, passed, pool, props.targets]);
2729
+ }, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
2332
2730
  useAssessmentHandleRegistration(checkId, handle, ref);
2333
2731
  const place = (targetId, itemId) => {
2334
2732
  if (passed && !props.enableRetry) return;
2733
+ setChecked(false);
2335
2734
  const prev = assignments[targetId];
2336
2735
  setAssignments((a) => ({ ...a, [targetId]: itemId }));
2337
2736
  setPool((p) => {
@@ -2343,29 +2742,31 @@ function DragAndDropInner(props, ref) {
2343
2742
  };
2344
2743
  const check = () => {
2345
2744
  if (!allFilled) return;
2745
+ setChecked(true);
2346
2746
  assessment.answer({
2347
2747
  checkId,
2348
2748
  interactionType: INTERACTION5,
2349
2749
  response: assignments,
2350
- correct: allCorrect
2750
+ correct: passedThreshold
2351
2751
  });
2352
- if (allCorrect && !completedRef.current) {
2752
+ if (passedThreshold && !completedRef.current) {
2353
2753
  completedRef.current = true;
2354
2754
  setPassed(true);
2355
2755
  assessment.complete({
2356
2756
  checkId,
2357
2757
  interactionType: INTERACTION5,
2358
- score: props.targets.length,
2359
- maxScore: props.targets.length,
2360
- passingScore: props.passingScore ?? props.targets.length
2758
+ score,
2759
+ maxScore,
2760
+ passingScore: props.passingScore ?? maxScore
2361
2761
  });
2362
2762
  }
2363
2763
  };
2364
2764
  return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2365
- /* @__PURE__ */ jsx10("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2366
- /* @__PURE__ */ jsx10("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
2765
+ /* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2766
+ /* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
2367
2767
  const item = props.items.find((i) => i.id === id);
2368
- return /* @__PURE__ */ jsx10(
2768
+ if (!item) return [];
2769
+ return /* @__PURE__ */ jsx12(
2369
2770
  "button",
2370
2771
  {
2371
2772
  type: "button",
@@ -2380,13 +2781,13 @@ function DragAndDropInner(props, ref) {
2380
2781
  id
2381
2782
  );
2382
2783
  }) }),
2383
- /* @__PURE__ */ jsx10("ul", { children: props.targets.map((target) => {
2784
+ /* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
2384
2785
  const assigned = assignments[target.id];
2385
2786
  const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2386
2787
  return /* @__PURE__ */ jsxs8("li", { children: [
2387
- /* @__PURE__ */ jsx10("strong", { children: target.label }),
2788
+ /* @__PURE__ */ jsx12("strong", { children: target.label }),
2388
2789
  " ",
2389
- /* @__PURE__ */ jsx10(
2790
+ /* @__PURE__ */ jsx12(
2390
2791
  "span",
2391
2792
  {
2392
2793
  role: "button",
@@ -2413,17 +2814,18 @@ function DragAndDropInner(props, ref) {
2413
2814
  )
2414
2815
  ] }, target.id);
2415
2816
  }) }),
2416
- /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2417
- allFilled ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
2817
+ /* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
2818
+ checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
2418
2819
  ] });
2419
2820
  }
2420
2821
  var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
2421
2822
  var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
2422
- return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2823
+ return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2423
2824
  });
2424
2825
 
2425
2826
  // src/blocks/AssessmentSequence.tsx
2426
- import React14, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo13, useState as useState10 } from "react";
2827
+ import React16, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useId as useId3, useMemo as useMemo13, useRef as useRef13, useState as useState10 } from "react";
2828
+ import { deriveId } from "@lessonkit/core";
2427
2829
 
2428
2830
  // src/compound/useCompoundShell.ts
2429
2831
  import { useMemo as useMemo12 } from "react";
@@ -2450,7 +2852,7 @@ function useCompoundNavigation(pageCount, index, setIndex) {
2450
2852
  }
2451
2853
 
2452
2854
  // src/compound/useCompoundPersistence.ts
2453
- import { useCallback as useCallback6, useEffect as useEffect11, useRef as useRef11 } from "react";
2855
+ import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
2454
2856
  import {
2455
2857
  clampCompoundPageIndex as clampCompoundPageIndex2,
2456
2858
  createCompoundResumeState as createCompoundResumeState2,
@@ -2458,14 +2860,80 @@ import {
2458
2860
  loadCompoundState as loadCompoundState2
2459
2861
  } from "@lessonkit/core";
2460
2862
 
2863
+ // src/compound/resumeChildHandles.ts
2864
+ function filterRegisteredChildStates(handles, childStates) {
2865
+ const filtered = {};
2866
+ for (const [key, value] of Object.entries(childStates)) {
2867
+ if (handles.has(key)) {
2868
+ filtered[key] = value;
2869
+ }
2870
+ }
2871
+ return filtered;
2872
+ }
2873
+ function resumeChildHandles(handles, childStates, opts) {
2874
+ const pendingKeys = Object.keys(childStates);
2875
+ const alreadyResumed = opts?.alreadyResumed;
2876
+ if (opts?.waitForHandles && pendingKeys.length > 0) {
2877
+ if (handles.size === 0) return false;
2878
+ const registeredPending = pendingKeys.filter((k) => handles.has(k));
2879
+ if (registeredPending.length === 0) {
2880
+ return false;
2881
+ }
2882
+ if (registeredPending.length < pendingKeys.length) {
2883
+ for (const key of registeredPending) {
2884
+ if (alreadyResumed?.has(key)) continue;
2885
+ const handle = handles.get(key);
2886
+ const child = childStates[key];
2887
+ if (handle?.resume && child) {
2888
+ handle.resume(child);
2889
+ alreadyResumed?.add(key);
2890
+ }
2891
+ }
2892
+ return false;
2893
+ }
2894
+ }
2895
+ for (const [checkId, handle] of handles) {
2896
+ if (alreadyResumed?.has(checkId)) continue;
2897
+ const child = childStates[checkId];
2898
+ if (child && handle.resume) {
2899
+ handle.resume(child);
2900
+ alreadyResumed?.add(checkId);
2901
+ }
2902
+ }
2903
+ return true;
2904
+ }
2905
+
2461
2906
  // src/compound/useCompoundResume.ts
2462
- import { useCallback as useCallback5, useEffect as useEffect10, useRef as useRef10 } from "react";
2907
+ import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
2463
2908
  import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
2464
2909
  import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
2910
+ var warnedCompoundPersistFailure = false;
2911
+ function warnCompoundPersistFailure() {
2912
+ if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
2913
+ warnedCompoundPersistFailure = true;
2914
+ console.warn(
2915
+ "[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
2916
+ );
2917
+ }
2465
2918
  function useCompoundResume(opts) {
2466
- const storageRef = useRef10(opts.storage ?? createSessionStoragePort2());
2467
- const resumedRef = useRef10(false);
2919
+ const lessonkitCtx = useContext6(LessonkitContext);
2920
+ const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
2921
+ const resumedRef = useRef11(false);
2922
+ const resumeKeyRef = useRef11("");
2923
+ const prevEnabledRef = useRef11(opts.enabled);
2924
+ useEffect10(() => {
2925
+ storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
2926
+ }, [opts.storage, lessonkitCtx?.storage]);
2468
2927
  useEffect10(() => {
2928
+ if (!prevEnabledRef.current && opts.enabled) {
2929
+ resumedRef.current = false;
2930
+ }
2931
+ prevEnabledRef.current = opts.enabled;
2932
+ const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
2933
+ if (resumeKeyRef.current !== key) {
2934
+ resumeKeyRef.current = key;
2935
+ resumedRef.current = false;
2936
+ }
2469
2937
  if (!opts.enabled || !opts.courseId || resumedRef.current) return;
2470
2938
  const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
2471
2939
  if (saved) {
@@ -2476,7 +2944,8 @@ function useCompoundResume(opts) {
2476
2944
  return useCallback5(
2477
2945
  (state) => {
2478
2946
  if (!opts.enabled || !opts.courseId) return;
2479
- saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
2947
+ const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
2948
+ if (!persisted) warnCompoundPersistFailure();
2480
2949
  },
2481
2950
  [opts.enabled, opts.courseId, opts.compoundId]
2482
2951
  );
@@ -2489,19 +2958,46 @@ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, stor
2489
2958
  if (!saved) return 0;
2490
2959
  return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
2491
2960
  }
2961
+ function stripOrphanChildStates(handles, childStates) {
2962
+ return filterRegisteredChildStates(handles, childStates);
2963
+ }
2492
2964
  function useCompoundPersistence(opts) {
2493
- const storage = opts.storage ?? createSessionStoragePort3();
2965
+ const lessonkitCtx = useContext7(LessonkitContext);
2966
+ const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
2494
2967
  const ctx = useCompoundRegistry();
2495
2968
  const handlesVersion = useCompoundHandlesVersion();
2496
- const pendingChildResumeRef = useRef11(null);
2497
- const loadedChildStatesRef = useRef11({});
2498
- const skipSaveUntilHydratedRef = useRef11(false);
2969
+ const bridgeRef = useCompoundHydrationBridgeRef();
2970
+ const pendingChildResumeRef = useRef12(null);
2971
+ const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
2972
+ const loadedChildStatesRef = useRef12({});
2973
+ const skipSaveUntilHydratedRef = useRef12(false);
2974
+ const hydrationKeyRef = useRef12("");
2975
+ const hydrationInitRef = useRef12(false);
2976
+ const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
2977
+ if (hydrationKeyRef.current !== hydrationKey) {
2978
+ hydrationKeyRef.current = hydrationKey;
2979
+ hydrationInitRef.current = false;
2980
+ loadedChildStatesRef.current = {};
2981
+ skipSaveUntilHydratedRef.current = false;
2982
+ pendingChildResumeRef.current = null;
2983
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2984
+ }
2985
+ if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
2986
+ hydrationInitRef.current = true;
2987
+ const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
2988
+ if (saved && Object.keys(saved.childStates).length > 0) {
2989
+ loadedChildStatesRef.current = { ...saved.childStates };
2990
+ skipSaveUntilHydratedRef.current = true;
2991
+ pendingChildResumeRef.current = saved;
2992
+ }
2993
+ }
2499
2994
  const buildState = useCallback6(() => {
2500
2995
  const childStates = {
2501
2996
  ...loadedChildStatesRef.current
2502
2997
  };
2503
2998
  if (ctx) {
2504
- for (const [checkId, handle] of ctx.getHandles()) {
2999
+ for (const [checkId, entry] of ctx.getRegisteredHandles()) {
3000
+ const handle = entry.handle;
2505
3001
  if (handle.getCurrentState) {
2506
3002
  childStates[checkId] = handle.getCurrentState();
2507
3003
  delete loadedChildStatesRef.current[checkId];
@@ -2513,14 +3009,55 @@ function useCompoundPersistence(opts) {
2513
3009
  childStates
2514
3010
  });
2515
3011
  }, [ctx, opts.index, opts.pageCount]);
3012
+ const buildStateRef = useRef12(buildState);
3013
+ buildStateRef.current = buildState;
3014
+ const persistNowRef = useRef12(() => {
3015
+ });
3016
+ const finalizeHydration = useCallback6(
3017
+ (childStates) => {
3018
+ loadedChildStatesRef.current = {
3019
+ ...loadedChildStatesRef.current,
3020
+ ...childStates
3021
+ };
3022
+ skipSaveUntilHydratedRef.current = false;
3023
+ pendingChildResumeRef.current = null;
3024
+ queueMicrotask(() => persistNowRef.current());
3025
+ },
3026
+ []
3027
+ );
2516
3028
  const applyPendingChildResume = useCallback6(() => {
2517
3029
  const pending = pendingChildResumeRef.current;
2518
3030
  if (!pending || !ctx) return;
2519
- const applied = resumeChildHandles(ctx.getHandles(), pending.childStates, { waitForHandles: true });
2520
- if (!applied) return;
2521
- pendingChildResumeRef.current = null;
2522
- skipSaveUntilHydratedRef.current = false;
2523
- }, [ctx]);
3031
+ const handles = ctx.getHandles();
3032
+ const applied = resumeChildHandles(handles, pending.childStates, {
3033
+ waitForHandles: true,
3034
+ alreadyResumed: resumedChildKeysRef.current
3035
+ });
3036
+ if (!applied) {
3037
+ if (handles.size === 0) {
3038
+ const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
3039
+ resumeChildHandles(handles, registeredOnly2, {
3040
+ alreadyResumed: resumedChildKeysRef.current
3041
+ });
3042
+ finalizeHydration(registeredOnly2);
3043
+ return;
3044
+ }
3045
+ const handlesAtWait = handles.size;
3046
+ queueMicrotask(() => {
3047
+ if (pendingChildResumeRef.current !== pending) return;
3048
+ const handlesNow = ctx.getHandles();
3049
+ if (handlesNow.size !== handlesAtWait) return;
3050
+ const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
3051
+ resumeChildHandles(handlesNow, registeredOnly2, {
3052
+ alreadyResumed: resumedChildKeysRef.current
3053
+ });
3054
+ finalizeHydration(registeredOnly2);
3055
+ });
3056
+ return;
3057
+ }
3058
+ const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
3059
+ finalizeHydration(registeredOnly);
3060
+ }, [ctx, finalizeHydration]);
2524
3061
  const saveResume = useCompoundResume({
2525
3062
  courseId: opts.courseId,
2526
3063
  compoundId: opts.compoundId,
@@ -2531,26 +3068,58 @@ function useCompoundPersistence(opts) {
2531
3068
  loadedChildStatesRef.current = { ...state.childStates };
2532
3069
  skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2533
3070
  opts.setIndex(clamped);
2534
- pendingChildResumeRef.current = { ...state, activePageIndex: clamped };
3071
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
3072
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
2535
3073
  queueMicrotask(() => applyPendingChildResume());
2536
3074
  }
2537
3075
  });
2538
- useEffect11(() => {
3076
+ const persistNow = useCallback6(() => {
2539
3077
  if (!opts.enabled || !opts.courseId) return;
2540
3078
  if (skipSaveUntilHydratedRef.current) return;
2541
- saveResume(buildState());
2542
- }, [
2543
- opts.enabled,
2544
- opts.courseId,
2545
- opts.index,
2546
- opts.pageCount,
2547
- handlesVersion,
2548
- saveResume,
2549
- buildState
2550
- ]);
3079
+ saveResume(buildStateRef.current());
3080
+ }, [opts.enabled, opts.courseId, saveResume]);
3081
+ useEffect11(() => {
3082
+ persistNowRef.current = persistNow;
3083
+ }, [persistNow]);
3084
+ const notifyImperativeResume = useCallback6(
3085
+ (state) => {
3086
+ const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
3087
+ loadedChildStatesRef.current = { ...state.childStates };
3088
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
3089
+ opts.setIndex(clamped);
3090
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
3091
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
3092
+ queueMicrotask(() => applyPendingChildResume());
3093
+ },
3094
+ [opts.pageCount, opts.setIndex, applyPendingChildResume]
3095
+ );
3096
+ useEffect11(() => {
3097
+ if (!bridgeRef) return;
3098
+ bridgeRef.current = { notifyImperativeResume };
3099
+ return () => {
3100
+ if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
3101
+ bridgeRef.current = null;
3102
+ }
3103
+ };
3104
+ }, [bridgeRef, notifyImperativeResume]);
2551
3105
  useEffect11(() => {
2552
3106
  applyPendingChildResume();
2553
3107
  }, [opts.index, handlesVersion, applyPendingChildResume]);
3108
+ useEffect11(() => {
3109
+ persistNow();
3110
+ }, [persistNow, opts.index, opts.pageCount, handlesVersion]);
3111
+ useEffect11(() => {
3112
+ if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
3113
+ const flushOnExit = () => {
3114
+ if (document.visibilityState === "hidden") persistNow();
3115
+ };
3116
+ document.addEventListener("visibilitychange", flushOnExit);
3117
+ window.addEventListener("pagehide", flushOnExit);
3118
+ return () => {
3119
+ document.removeEventListener("visibilitychange", flushOnExit);
3120
+ window.removeEventListener("pagehide", flushOnExit);
3121
+ };
3122
+ }, [opts.enabled, opts.courseId, persistNow]);
2554
3123
  }
2555
3124
 
2556
3125
  // src/compound/useCompoundShell.ts
@@ -2571,6 +3140,7 @@ function useCompoundShell(opts) {
2571
3140
  activePageIndex: visibleIndex,
2572
3141
  setActivePageIndex: opts.setIndex,
2573
3142
  getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
3143
+ getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
2574
3144
  pageCount: opts.pageCount,
2575
3145
  enableSolutionsButton: opts.enableSolutionsButton
2576
3146
  });
@@ -2590,7 +3160,7 @@ function useCompoundInitialIndex(opts) {
2590
3160
  }
2591
3161
 
2592
3162
  // src/compound/validateChildren.ts
2593
- import React13 from "react";
3163
+ import React15 from "react";
2594
3164
  import {
2595
3165
  ACCORDION_FORBIDDEN_CHILD_TYPES,
2596
3166
  COMPOUND_MAX_NESTING_DEPTH,
@@ -2619,6 +3189,8 @@ var warnedPairs = /* @__PURE__ */ new Set();
2619
3189
  var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
2620
3190
  "Page",
2621
3191
  "InteractiveBook",
3192
+ "Slide",
3193
+ "SlideDeck",
2622
3194
  "AssessmentSequence"
2623
3195
  ]);
2624
3196
  function warnOrThrow(msg, strict) {
@@ -2629,8 +3201,8 @@ function warnOrThrow(msg, strict) {
2629
3201
  }
2630
3202
  }
2631
3203
  function validateNode(parent, node, depth, strict) {
2632
- React13.Children.forEach(node, (child) => {
2633
- if (!React13.isValidElement(child)) return;
3204
+ React15.Children.forEach(node, (child) => {
3205
+ if (!React15.isValidElement(child)) return;
2634
3206
  const blockType = getLessonkitBlockType(child.type);
2635
3207
  if (!blockType) {
2636
3208
  if (child.props && typeof child.props === "object" && "children" in child.props) {
@@ -2670,8 +3242,8 @@ function validateNode(parent, node, depth, strict) {
2670
3242
  });
2671
3243
  }
2672
3244
  function validateSubtreeForForbidden(node, forbidden, strict) {
2673
- React13.Children.forEach(node, (child) => {
2674
- if (!React13.isValidElement(child)) return;
3245
+ React15.Children.forEach(node, (child) => {
3246
+ if (!React15.isValidElement(child)) return;
2675
3247
  const blockType = getLessonkitBlockType(child.type);
2676
3248
  if (blockType && forbidden.includes(blockType)) {
2677
3249
  warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
@@ -2711,7 +3283,7 @@ function warnSharedCompoundStorageKey(opts) {
2711
3283
  }
2712
3284
 
2713
3285
  // src/blocks/AssessmentSequence.tsx
2714
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
3286
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2715
3287
  var AssessmentSequenceInner = forwardRef7(
2716
3288
  function AssessmentSequenceInner2(props, ref) {
2717
3289
  const { compoundId, childArray, index, setIndex, persistEnabled } = props;
@@ -2729,7 +3301,7 @@ var AssessmentSequenceInner = forwardRef7(
2729
3301
  });
2730
3302
  validateCompoundChildren("AssessmentSequence", props.children);
2731
3303
  if (!sequential) {
2732
- return /* @__PURE__ */ jsx11("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
3304
+ return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
2733
3305
  }
2734
3306
  return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
2735
3307
  /* @__PURE__ */ jsxs9("p", { children: [
@@ -2738,9 +3310,9 @@ var AssessmentSequenceInner = forwardRef7(
2738
3310
  " of ",
2739
3311
  progress.total
2740
3312
  ] }),
2741
- /* @__PURE__ */ jsx11("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx11("div", { hidden: i !== visibleIndex, children: child }, child.key ?? i)) }),
3313
+ /* @__PURE__ */ jsx13("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx13("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ jsx13(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
2742
3314
  /* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
2743
- /* @__PURE__ */ jsx11(
3315
+ /* @__PURE__ */ jsx13(
2744
3316
  "button",
2745
3317
  {
2746
3318
  type: "button",
@@ -2750,7 +3322,7 @@ var AssessmentSequenceInner = forwardRef7(
2750
3322
  children: "Previous"
2751
3323
  }
2752
3324
  ),
2753
- /* @__PURE__ */ jsx11(
3325
+ /* @__PURE__ */ jsx13(
2754
3326
  "button",
2755
3327
  {
2756
3328
  type: "button",
@@ -2766,14 +3338,19 @@ var AssessmentSequenceInner = forwardRef7(
2766
3338
  );
2767
3339
  var AssessmentSequence = forwardRef7(
2768
3340
  function AssessmentSequence2(props, ref) {
3341
+ const reactInstanceId = useId3();
3342
+ const autoCompoundIdRef = useRef13(null);
3343
+ if (!props.blockId && !autoCompoundIdRef.current) {
3344
+ autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
3345
+ }
2769
3346
  const compoundId = useMemo13(
2770
- () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
3347
+ () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
2771
3348
  [props.blockId]
2772
3349
  );
2773
- const childArray = React14.Children.toArray(props.children).filter(
2774
- React14.isValidElement
3350
+ const childArray = React16.Children.toArray(props.children).filter(
3351
+ React16.isValidElement
2775
3352
  );
2776
- const { config } = useLessonkit();
3353
+ const { config, storage } = useLessonkit();
2777
3354
  const persistEnabled = config.session?.persistCompoundState !== false;
2778
3355
  useEffect12(() => {
2779
3356
  warnSharedCompoundStorageKey({
@@ -2786,11 +3363,15 @@ var AssessmentSequence = forwardRef7(
2786
3363
  courseId: config.courseId,
2787
3364
  compoundId,
2788
3365
  pageCount: childArray.length,
2789
- persistEnabled
3366
+ persistEnabled,
3367
+ storage
2790
3368
  });
2791
3369
  const [index, setIndex] = useState10(initialIndex);
2792
3370
  const setIndexStable = useCallback7((i) => setIndex(i), []);
2793
- return /* @__PURE__ */ jsx11(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx11(
3371
+ useEffect12(() => {
3372
+ setIndex(initialIndex);
3373
+ }, [config.courseId, compoundId, initialIndex]);
3374
+ return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
2794
3375
  AssessmentSequenceInner,
2795
3376
  {
2796
3377
  ...props,
@@ -2808,24 +3389,24 @@ setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
2808
3389
 
2809
3390
  // src/blocks/Text.tsx
2810
3391
  import "react";
2811
- import { jsx as jsx12 } from "react/jsx-runtime";
3392
+ import { jsx as jsx14 } from "react/jsx-runtime";
2812
3393
  function Text(props) {
2813
- return /* @__PURE__ */ jsx12("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
3394
+ return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
2814
3395
  }
2815
3396
  setLessonkitBlockType(Text, "Text");
2816
3397
 
2817
3398
  // src/blocks/Heading.tsx
2818
- import { jsx as jsx13 } from "react/jsx-runtime";
3399
+ import { jsx as jsx15 } from "react/jsx-runtime";
2819
3400
  function Heading(props) {
2820
3401
  const Tag = `h${props.level}`;
2821
- return /* @__PURE__ */ jsx13(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
3402
+ return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
2822
3403
  }
2823
3404
  setLessonkitBlockType(Heading, "Heading");
2824
3405
 
2825
3406
  // src/blocks/Image.tsx
2826
- import { jsx as jsx14 } from "react/jsx-runtime";
3407
+ import { jsx as jsx16 } from "react/jsx-runtime";
2827
3408
  function Image(props) {
2828
- return /* @__PURE__ */ jsx14(
3409
+ return /* @__PURE__ */ jsx16(
2829
3410
  "img",
2830
3411
  {
2831
3412
  src: props.src,
@@ -2840,13 +3421,13 @@ setLessonkitBlockType(Image, "Image");
2840
3421
 
2841
3422
  // src/blocks/Page.tsx
2842
3423
  import { useEffect as useEffect13 } from "react";
2843
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
3424
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
2844
3425
  function Page(props) {
2845
3426
  validateCompoundChildren("Page", props.children);
2846
3427
  const { track } = useLessonkit();
2847
3428
  const lessonId = useEnclosingLessonId();
2848
3429
  useEffect13(() => {
2849
- if (props.hidden || !lessonId) return;
3430
+ if (props.hidden || !lessonId || props.parentType) return;
2850
3431
  track(
2851
3432
  "compound_page_viewed",
2852
3433
  {
@@ -2865,8 +3446,8 @@ function Page(props) {
2865
3446
  "data-testid": `page-${props.blockId}`,
2866
3447
  hidden: props.hidden ? true : void 0,
2867
3448
  children: [
2868
- props.title ? /* @__PURE__ */ jsx15("h3", { children: props.title }) : null,
2869
- /* @__PURE__ */ jsx15("div", { children: props.children })
3449
+ props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
3450
+ /* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
2870
3451
  ]
2871
3452
  }
2872
3453
  );
@@ -2874,8 +3455,8 @@ function Page(props) {
2874
3455
  setLessonkitBlockType(Page, "Page");
2875
3456
 
2876
3457
  // src/blocks/InteractiveBook.tsx
2877
- import React17, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
2878
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3458
+ import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
3459
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2879
3460
  var InteractiveBookInner = forwardRef8(
2880
3461
  function InteractiveBookInner2(props, ref) {
2881
3462
  const { blockId, pages, index, setIndex, persistEnabled } = props;
@@ -2908,7 +3489,7 @@ var InteractiveBookInner = forwardRef8(
2908
3489
  );
2909
3490
  }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
2910
3491
  return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
2911
- /* @__PURE__ */ jsx16("h3", { children: props.title }),
3492
+ /* @__PURE__ */ jsx18("h3", { children: props.title }),
2912
3493
  /* @__PURE__ */ jsxs11("p", { children: [
2913
3494
  "Page ",
2914
3495
  progress.current,
@@ -2922,8 +3503,8 @@ var InteractiveBookInner = forwardRef8(
2922
3503
  " ",
2923
3504
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
2924
3505
  ] }) : null,
2925
- /* @__PURE__ */ jsx16("div", { "data-testid": "interactive-book-page", children: pages.map(
2926
- (page, i) => React17.cloneElement(page, {
3506
+ /* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
3507
+ (page, i) => React19.cloneElement(page, {
2927
3508
  key: page.key ?? page.props.blockId,
2928
3509
  hidden: i !== visibleIndex,
2929
3510
  pageIndex: i,
@@ -2931,7 +3512,7 @@ var InteractiveBookInner = forwardRef8(
2931
3512
  })
2932
3513
  ) }),
2933
3514
  /* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
2934
- /* @__PURE__ */ jsx16(
3515
+ /* @__PURE__ */ jsx18(
2935
3516
  "button",
2936
3517
  {
2937
3518
  type: "button",
@@ -2941,7 +3522,7 @@ var InteractiveBookInner = forwardRef8(
2941
3522
  children: "Previous"
2942
3523
  }
2943
3524
  ),
2944
- /* @__PURE__ */ jsx16(
3525
+ /* @__PURE__ */ jsx18(
2945
3526
  "button",
2946
3527
  {
2947
3528
  type: "button",
@@ -2960,20 +3541,24 @@ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
2960
3541
  () => normalizeComponentId(props.blockId, "blockId"),
2961
3542
  [props.blockId]
2962
3543
  );
2963
- const pages = React17.Children.toArray(props.children).filter(
2964
- React17.isValidElement
3544
+ const pages = React19.Children.toArray(props.children).filter(
3545
+ React19.isValidElement
2965
3546
  );
2966
- const { config } = useLessonkit();
3547
+ const { config, storage } = useLessonkit();
2967
3548
  const persistEnabled = config.session?.persistCompoundState !== false;
2968
3549
  const initialIndex = useCompoundInitialIndex({
2969
3550
  courseId: config.courseId,
2970
3551
  compoundId: blockId,
2971
3552
  pageCount: pages.length,
2972
- persistEnabled
3553
+ persistEnabled,
3554
+ storage
2973
3555
  });
2974
3556
  const [index, setIndex] = useState11(initialIndex);
2975
3557
  const setIndexStable = useCallback8((i) => setIndex(i), []);
2976
- return /* @__PURE__ */ jsx16(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx16(
3558
+ useEffect14(() => {
3559
+ setIndex(initialIndex);
3560
+ }, [config.courseId, blockId, initialIndex]);
3561
+ return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
2977
3562
  InteractiveBookInner,
2978
3563
  {
2979
3564
  ...props,
@@ -2988,17 +3573,249 @@ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
2988
3573
  });
2989
3574
  setLessonkitBlockType(InteractiveBook, "InteractiveBook");
2990
3575
 
3576
+ // src/blocks/Slide.tsx
3577
+ import { useEffect as useEffect15 } from "react";
3578
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3579
+ function Slide(props) {
3580
+ validateCompoundChildren("Slide", props.children);
3581
+ const { track } = useLessonkit();
3582
+ const lessonId = useEnclosingLessonId();
3583
+ useEffect15(() => {
3584
+ if (props.hidden || !lessonId || props.parentType) return;
3585
+ track(
3586
+ "compound_page_viewed",
3587
+ {
3588
+ blockId: props.blockId,
3589
+ pageIndex: props.slideIndex ?? 0,
3590
+ parentType: props.parentType
3591
+ },
3592
+ { lessonId }
3593
+ );
3594
+ }, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
3595
+ return /* @__PURE__ */ jsxs12(
3596
+ "section",
3597
+ {
3598
+ "aria-label": props.title ?? "Slide",
3599
+ "data-lk-block-id": props.blockId,
3600
+ "data-testid": `slide-${props.blockId}`,
3601
+ hidden: props.hidden ? true : void 0,
3602
+ children: [
3603
+ props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
3604
+ /* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
3605
+ ]
3606
+ }
3607
+ );
3608
+ }
3609
+ setLessonkitBlockType(Slide, "Slide");
3610
+
3611
+ // src/blocks/SlideDeck.tsx
3612
+ import React21, { forwardRef as forwardRef9, useCallback as useCallback9, useEffect as useEffect17, useMemo as useMemo15, useRef as useRef14, useState as useState12 } from "react";
3613
+
3614
+ // src/compound/useCompoundKeyboardNav.ts
3615
+ import { useEffect as useEffect16 } from "react";
3616
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
3617
+ function isEditableTarget(target) {
3618
+ if (!(target instanceof HTMLElement)) return false;
3619
+ if (INTERACTIVE_TAGS.has(target.tagName)) return true;
3620
+ if (target.isContentEditable) return true;
3621
+ if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
3622
+ return true;
3623
+ }
3624
+ return false;
3625
+ }
3626
+ function useCompoundKeyboardNav(opts) {
3627
+ const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
3628
+ useEffect16(() => {
3629
+ const el = containerRef.current;
3630
+ if (!el || pageCount === 0) return;
3631
+ const onKeyDown = (event) => {
3632
+ if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
3633
+ return;
3634
+ }
3635
+ if (isEditableTarget(event.target)) return;
3636
+ switch (event.key) {
3637
+ case "ArrowRight":
3638
+ case "ArrowDown":
3639
+ if (visibleIndex < pageCount - 1) {
3640
+ event.preventDefault();
3641
+ goNext();
3642
+ }
3643
+ break;
3644
+ case "ArrowLeft":
3645
+ case "ArrowUp":
3646
+ if (visibleIndex > 0) {
3647
+ event.preventDefault();
3648
+ goPrev();
3649
+ }
3650
+ break;
3651
+ case "Home":
3652
+ if (visibleIndex !== 0) {
3653
+ event.preventDefault();
3654
+ setIndex(0);
3655
+ }
3656
+ break;
3657
+ case "End":
3658
+ if (visibleIndex !== pageCount - 1) {
3659
+ event.preventDefault();
3660
+ setIndex(pageCount - 1);
3661
+ }
3662
+ break;
3663
+ default:
3664
+ break;
3665
+ }
3666
+ };
3667
+ el.addEventListener("keydown", onKeyDown);
3668
+ return () => el.removeEventListener("keydown", onKeyDown);
3669
+ }, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
3670
+ }
3671
+
3672
+ // src/blocks/SlideDeck.tsx
3673
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3674
+ var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
3675
+ const { blockId, slides, index, setIndex, persistEnabled } = props;
3676
+ validateCompoundChildren("SlideDeck", slides);
3677
+ const { config, track } = useLessonkit();
3678
+ const lessonId = useEnclosingLessonId();
3679
+ const containerRef = useRef14(null);
3680
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3681
+ courseId: config.courseId,
3682
+ compoundId: blockId,
3683
+ pageCount: slides.length,
3684
+ index,
3685
+ setIndex,
3686
+ persistEnabled,
3687
+ ref
3688
+ });
3689
+ const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
3690
+ useCompoundKeyboardNav({
3691
+ containerRef,
3692
+ visibleIndex,
3693
+ pageCount: slides.length,
3694
+ goNext,
3695
+ goPrev,
3696
+ setIndex: setIndexStable
3697
+ });
3698
+ const slideTitles = useMemo15(
3699
+ () => slides.map((slide) => slide.props.title),
3700
+ [slides]
3701
+ );
3702
+ useEffect17(() => {
3703
+ if (!lessonId || slides.length === 0) return;
3704
+ track(
3705
+ "slide_viewed",
3706
+ {
3707
+ blockId,
3708
+ slideIndex: visibleIndex,
3709
+ slideTitle: slideTitles[visibleIndex]
3710
+ },
3711
+ { lessonId }
3712
+ );
3713
+ }, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
3714
+ return /* @__PURE__ */ jsxs13(
3715
+ "section",
3716
+ {
3717
+ ref: containerRef,
3718
+ tabIndex: -1,
3719
+ "aria-label": props.title,
3720
+ "data-testid": "slide-deck",
3721
+ "data-lk-block-id": blockId,
3722
+ children: [
3723
+ /* @__PURE__ */ jsx20("h3", { children: props.title }),
3724
+ /* @__PURE__ */ jsxs13("p", { children: [
3725
+ "Slide ",
3726
+ progress.current,
3727
+ " of ",
3728
+ progress.total
3729
+ ] }),
3730
+ props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
3731
+ "Score: ",
3732
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3733
+ " /",
3734
+ " ",
3735
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3736
+ ] }) : null,
3737
+ /* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
3738
+ (slide, i) => React21.cloneElement(slide, {
3739
+ key: slide.key ?? slide.props.blockId,
3740
+ hidden: i !== visibleIndex,
3741
+ slideIndex: i,
3742
+ parentType: "SlideDeck"
3743
+ })
3744
+ ) }),
3745
+ /* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
3746
+ /* @__PURE__ */ jsx20(
3747
+ "button",
3748
+ {
3749
+ type: "button",
3750
+ "data-testid": "slide-prev",
3751
+ disabled: visibleIndex === 0 || slides.length === 0,
3752
+ onClick: goPrev,
3753
+ children: "Previous slide"
3754
+ }
3755
+ ),
3756
+ /* @__PURE__ */ jsx20(
3757
+ "button",
3758
+ {
3759
+ type: "button",
3760
+ "data-testid": "slide-next",
3761
+ disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
3762
+ onClick: goNext,
3763
+ children: "Next slide"
3764
+ }
3765
+ )
3766
+ ] })
3767
+ ]
3768
+ }
3769
+ );
3770
+ });
3771
+ var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
3772
+ const blockId = useMemo15(
3773
+ () => normalizeComponentId(props.blockId, "blockId"),
3774
+ [props.blockId]
3775
+ );
3776
+ const slides = React21.Children.toArray(props.children).filter(
3777
+ React21.isValidElement
3778
+ );
3779
+ const { config, storage } = useLessonkit();
3780
+ const persistEnabled = config.session?.persistCompoundState !== false;
3781
+ const initialIndex = useCompoundInitialIndex({
3782
+ courseId: config.courseId,
3783
+ compoundId: blockId,
3784
+ pageCount: slides.length,
3785
+ persistEnabled,
3786
+ storage
3787
+ });
3788
+ const [index, setIndex] = useState12(initialIndex);
3789
+ const setIndexStable = useCallback9((i) => setIndex(i), []);
3790
+ useEffect17(() => {
3791
+ setIndex(initialIndex);
3792
+ }, [config.courseId, blockId, initialIndex]);
3793
+ return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
3794
+ SlideDeckInner,
3795
+ {
3796
+ ...props,
3797
+ ref,
3798
+ blockId,
3799
+ slides,
3800
+ index,
3801
+ setIndex,
3802
+ persistEnabled
3803
+ }
3804
+ ) });
3805
+ });
3806
+ setLessonkitBlockType(SlideDeck, "SlideDeck");
3807
+
2991
3808
  // src/blocks/Accordion.tsx
2992
- import { useId as useId3, useState as useState12 } from "react";
2993
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
3809
+ import { useId as useId4, useState as useState13 } from "react";
3810
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
2994
3811
  function Accordion(props) {
2995
3812
  if (isDevEnvironment4()) {
2996
3813
  validateAccordionSections(props.sections);
2997
3814
  }
2998
- const [open, setOpen] = useState12(/* @__PURE__ */ new Set());
3815
+ const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
2999
3816
  const { track } = useLessonkit();
3000
3817
  const lessonId = useEnclosingLessonId();
3001
- const baseId = useId3();
3818
+ const baseId = useId4();
3002
3819
  const toggle = (sectionId) => {
3003
3820
  setOpen((prev) => {
3004
3821
  const next = new Set(prev);
@@ -3013,12 +3830,12 @@ function Accordion(props) {
3013
3830
  return next;
3014
3831
  });
3015
3832
  };
3016
- return /* @__PURE__ */ jsx17("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3833
+ return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3017
3834
  const expanded = open.has(section.id);
3018
3835
  const panelId = `${baseId}-${section.id}`;
3019
3836
  const triggerId = `${baseId}-trigger-${section.id}`;
3020
- return /* @__PURE__ */ jsxs12("div", { "data-testid": `accordion-section-${section.id}`, children: [
3021
- /* @__PURE__ */ jsx17("h4", { children: /* @__PURE__ */ jsx17(
3837
+ return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
3838
+ /* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
3022
3839
  "button",
3023
3840
  {
3024
3841
  id: triggerId,
@@ -3030,28 +3847,28 @@ function Accordion(props) {
3030
3847
  children: section.title
3031
3848
  }
3032
3849
  ) }),
3033
- expanded ? /* @__PURE__ */ jsx17("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3850
+ expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3034
3851
  ] }, section.id);
3035
3852
  }) });
3036
3853
  }
3037
3854
  setLessonkitBlockType(Accordion, "Accordion");
3038
3855
 
3039
3856
  // src/blocks/DialogCards.tsx
3040
- import { useState as useState13 } from "react";
3041
- import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3857
+ import { useState as useState14 } from "react";
3858
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
3042
3859
  function DialogCards(props) {
3043
- const [index, setIndex] = useState13(0);
3044
- const [flipped, setFlipped] = useState13(false);
3860
+ const [index, setIndex] = useState14(0);
3861
+ const [flipped, setFlipped] = useState14(false);
3045
3862
  const card = props.cards[index];
3046
3863
  if (!card) return null;
3047
- return /* @__PURE__ */ jsxs13("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3048
- /* @__PURE__ */ jsxs13("p", { children: [
3864
+ return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3865
+ /* @__PURE__ */ jsxs15("p", { children: [
3049
3866
  "Card ",
3050
3867
  index + 1,
3051
3868
  " of ",
3052
3869
  props.cards.length
3053
3870
  ] }),
3054
- /* @__PURE__ */ jsx18(
3871
+ /* @__PURE__ */ jsx22(
3055
3872
  "button",
3056
3873
  {
3057
3874
  type: "button",
@@ -3062,8 +3879,8 @@ function DialogCards(props) {
3062
3879
  children: flipped ? card.back : card.front
3063
3880
  }
3064
3881
  ),
3065
- /* @__PURE__ */ jsxs13("nav", { "aria-label": "Card navigation", children: [
3066
- /* @__PURE__ */ jsx18(
3882
+ /* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
3883
+ /* @__PURE__ */ jsx22(
3067
3884
  "button",
3068
3885
  {
3069
3886
  type: "button",
@@ -3076,7 +3893,7 @@ function DialogCards(props) {
3076
3893
  children: "Previous"
3077
3894
  }
3078
3895
  ),
3079
- /* @__PURE__ */ jsx18(
3896
+ /* @__PURE__ */ jsx22(
3080
3897
  "button",
3081
3898
  {
3082
3899
  type: "button",
@@ -3095,11 +3912,11 @@ function DialogCards(props) {
3095
3912
  setLessonkitBlockType(DialogCards, "DialogCards");
3096
3913
 
3097
3914
  // src/blocks/Flashcards.tsx
3098
- import { useState as useState14 } from "react";
3099
- import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3915
+ import { useState as useState15 } from "react";
3916
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
3100
3917
  function Flashcards(props) {
3101
- const [index, setIndex] = useState14(0);
3102
- const [face, setFace] = useState14("front");
3918
+ const [index, setIndex] = useState15(0);
3919
+ const [face, setFace] = useState15("front");
3103
3920
  const { track } = useLessonkit();
3104
3921
  const lessonId = useEnclosingLessonId();
3105
3922
  const card = props.cards[index];
@@ -3113,10 +3930,10 @@ function Flashcards(props) {
3113
3930
  lessonId ? { lessonId } : void 0
3114
3931
  );
3115
3932
  };
3116
- return /* @__PURE__ */ jsxs14("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3117
- /* @__PURE__ */ jsx19("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3118
- props.selfScore ? /* @__PURE__ */ jsx19("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3119
- /* @__PURE__ */ jsx19(
3933
+ return /* @__PURE__ */ jsxs16("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3934
+ /* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3935
+ props.selfScore ? /* @__PURE__ */ jsx23("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3936
+ /* @__PURE__ */ jsx23(
3120
3937
  "button",
3121
3938
  {
3122
3939
  type: "button",
@@ -3134,10 +3951,10 @@ function Flashcards(props) {
3134
3951
  setLessonkitBlockType(Flashcards, "Flashcards");
3135
3952
 
3136
3953
  // src/blocks/ImageHotspots.tsx
3137
- import { useState as useState15 } from "react";
3138
- import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
3954
+ import { useState as useState16 } from "react";
3955
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3139
3956
  function ImageHotspots(props) {
3140
- const [active, setActive] = useState15(null);
3957
+ const [active, setActive] = useState16(null);
3141
3958
  const { track } = useLessonkit();
3142
3959
  const lessonId = useEnclosingLessonId();
3143
3960
  const open = (hotspotId) => {
@@ -3148,10 +3965,10 @@ function ImageHotspots(props) {
3148
3965
  lessonId ? { lessonId } : void 0
3149
3966
  );
3150
3967
  };
3151
- return /* @__PURE__ */ jsxs15("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3152
- /* @__PURE__ */ jsxs15("div", { style: { position: "relative", display: "inline-block" }, children: [
3153
- /* @__PURE__ */ jsx20("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3154
- props.hotspots.map((h) => /* @__PURE__ */ jsx20(
3968
+ return /* @__PURE__ */ jsxs17("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3969
+ /* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
3970
+ /* @__PURE__ */ jsx24("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3971
+ props.hotspots.map((h) => /* @__PURE__ */ jsx24(
3155
3972
  "button",
3156
3973
  {
3157
3974
  type: "button",
@@ -3170,19 +3987,19 @@ function ImageHotspots(props) {
3170
3987
  h.id
3171
3988
  ))
3172
3989
  ] }),
3173
- active ? /* @__PURE__ */ jsxs15("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3990
+ active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3174
3991
  props.hotspots.find((h) => h.id === active)?.content,
3175
- /* @__PURE__ */ jsx20("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3992
+ /* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3176
3993
  ] }) : null
3177
3994
  ] });
3178
3995
  }
3179
3996
  setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3180
3997
 
3181
3998
  // src/blocks/ImageSlider.tsx
3182
- import { useState as useState16 } from "react";
3183
- import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
3999
+ import { useState as useState17 } from "react";
4000
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
3184
4001
  function ImageSlider(props) {
3185
- const [index, setIndex] = useState16(0);
4002
+ const [index, setIndex] = useState17(0);
3186
4003
  const { track } = useLessonkit();
3187
4004
  const lessonId = useEnclosingLessonId();
3188
4005
  const slide = props.slides[index];
@@ -3195,11 +4012,11 @@ function ImageSlider(props) {
3195
4012
  lessonId ? { lessonId } : void 0
3196
4013
  );
3197
4014
  };
3198
- return /* @__PURE__ */ jsxs16("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
3199
- /* @__PURE__ */ jsx21("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
3200
- slide.caption ? /* @__PURE__ */ jsx21("p", { children: slide.caption }) : null,
3201
- /* @__PURE__ */ jsxs16("nav", { "aria-label": "Slide navigation", children: [
3202
- /* @__PURE__ */ jsx21(
4015
+ return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
4016
+ /* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
4017
+ slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
4018
+ /* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
4019
+ /* @__PURE__ */ jsx25(
3203
4020
  "button",
3204
4021
  {
3205
4022
  type: "button",
@@ -3209,12 +4026,12 @@ function ImageSlider(props) {
3209
4026
  children: "Previous"
3210
4027
  }
3211
4028
  ),
3212
- /* @__PURE__ */ jsxs16("span", { children: [
4029
+ /* @__PURE__ */ jsxs18("span", { children: [
3213
4030
  index + 1,
3214
4031
  " / ",
3215
4032
  props.slides.length
3216
4033
  ] }),
3217
- /* @__PURE__ */ jsx21(
4034
+ /* @__PURE__ */ jsx25(
3218
4035
  "button",
3219
4036
  {
3220
4037
  type: "button",
@@ -3230,16 +4047,42 @@ function ImageSlider(props) {
3230
4047
  setLessonkitBlockType(ImageSlider, "ImageSlider");
3231
4048
 
3232
4049
  // src/blocks/FindHotspot.tsx
3233
- import { forwardRef as forwardRef9, useMemo as useMemo15, useState as useState17 } from "react";
3234
- import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
4050
+ import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useRef as useRef15, useState as useState18 } from "react";
4051
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
3235
4052
  var INTERACTION6 = "findHotspot";
3236
4053
  function FindHotspotInner(props, ref) {
3237
- const checkId = useMemo15(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3238
- const [selected, setSelected] = useState17(null);
3239
- const [checked, setChecked] = useState17(false);
4054
+ const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4055
+ const [selected, setSelected] = useState18(null);
4056
+ const [checked, setChecked] = useState18(false);
4057
+ const telemetryReplayedRef = useRef15(false);
3240
4058
  const assessment = useAssessmentState(props.enclosingLessonId);
4059
+ const targetIdsKey = props.targets.map((t) => t.id).join("\0");
4060
+ useEffect18(() => {
4061
+ setSelected(null);
4062
+ setChecked(false);
4063
+ telemetryReplayedRef.current = false;
4064
+ }, [checkId, props.correctTargetId, targetIdsKey]);
3241
4065
  const correct = selected === props.correctTargetId;
3242
- const handle = useMemo15(
4066
+ const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
4067
+ if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
4068
+ telemetryReplayedRef.current = true;
4069
+ assessment.answer({
4070
+ checkId,
4071
+ interactionType: INTERACTION6,
4072
+ response: nextSelected,
4073
+ correct: nextCorrect
4074
+ });
4075
+ if (nextCorrect) {
4076
+ assessment.complete({
4077
+ checkId,
4078
+ interactionType: INTERACTION6,
4079
+ score: 1,
4080
+ maxScore: 1,
4081
+ passingScore: props.passingScore ?? 1
4082
+ });
4083
+ }
4084
+ };
4085
+ const handle = useMemo16(
3243
4086
  () => buildAssessmentHandle({
3244
4087
  checkId,
3245
4088
  getScore: () => checked && correct ? 1 : 0,
@@ -3248,6 +4091,7 @@ function FindHotspotInner(props, ref) {
3248
4091
  resetTask: () => {
3249
4092
  setSelected(null);
3250
4093
  setChecked(false);
4094
+ telemetryReplayedRef.current = false;
3251
4095
  },
3252
4096
  showSolutions: () => setSelected(props.correctTargetId),
3253
4097
  getXAPIData: () => ({
@@ -3260,16 +4104,31 @@ function FindHotspotInner(props, ref) {
3260
4104
  }),
3261
4105
  getCurrentState: () => ({ selected, checked }),
3262
4106
  resume: (state) => {
3263
- const nextSelected = readStringField(state, "selected");
3264
- if (typeof nextSelected === "string") setSelected(nextSelected);
3265
- readBooleanStateField(state, "checked", setChecked);
4107
+ let nextSelected = selected;
4108
+ const rawSelected = readStringField(state, "selected");
4109
+ if (typeof rawSelected === "string" || rawSelected === null) {
4110
+ const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
4111
+ nextSelected = valid ? rawSelected : null;
4112
+ setSelected(nextSelected);
4113
+ }
4114
+ let nextChecked = checked;
4115
+ readBooleanStateField(state, "checked", (value) => {
4116
+ nextChecked = value;
4117
+ setChecked(value);
4118
+ });
4119
+ const nextCorrect = nextSelected === props.correctTargetId;
4120
+ replayTelemetry(nextSelected, nextChecked, nextCorrect);
3266
4121
  }
3267
4122
  }),
3268
- [checkId, selected, checked, correct, props.correctTargetId]
4123
+ [assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
3269
4124
  );
3270
4125
  useAssessmentHandleRegistration(checkId, handle, ref);
4126
+ const selectTarget = (id) => {
4127
+ setSelected(id);
4128
+ setChecked(false);
4129
+ };
3271
4130
  const submit = () => {
3272
- if (!selected) return;
4131
+ if (!selected || checked) return;
3273
4132
  setChecked(true);
3274
4133
  assessment.answer({
3275
4134
  checkId,
@@ -3283,14 +4142,14 @@ function FindHotspotInner(props, ref) {
3283
4142
  interactionType: INTERACTION6,
3284
4143
  score: 1,
3285
4144
  maxScore: 1,
3286
- passingScore: props.passingScore
4145
+ passingScore: props.passingScore ?? 1
3287
4146
  });
3288
4147
  }
3289
4148
  };
3290
- return /* @__PURE__ */ jsxs17("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
3291
- /* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
3292
- /* @__PURE__ */ jsx22("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3293
- props.targets.map((t) => /* @__PURE__ */ jsx22(
4149
+ return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
4150
+ /* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
4151
+ /* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4152
+ props.targets.map((t) => /* @__PURE__ */ jsx26(
3294
4153
  "button",
3295
4154
  {
3296
4155
  type: "button",
@@ -3303,30 +4162,30 @@ function FindHotspotInner(props, ref) {
3303
4162
  top: `${t.y}%`,
3304
4163
  transform: "translate(-50%, -50%)"
3305
4164
  },
3306
- onClick: () => setSelected(t.id),
4165
+ onClick: () => selectTarget(t.id),
3307
4166
  children: t.label
3308
4167
  },
3309
4168
  t.id
3310
4169
  ))
3311
4170
  ] }),
3312
- /* @__PURE__ */ jsx22("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
3313
- checked ? /* @__PURE__ */ jsx22("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4171
+ /* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
4172
+ checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
3314
4173
  ] });
3315
4174
  }
3316
- var FindHotspotInnerForwarded = forwardRef9(FindHotspotInner);
3317
- var FindHotspot = forwardRef9(function FindHotspot2(props, ref) {
3318
- return /* @__PURE__ */ jsx22(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx22(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
4175
+ var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
4176
+ var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
4177
+ return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
3319
4178
  });
3320
4179
  setLessonkitBlockType(FindHotspot, "FindHotspot");
3321
4180
 
3322
4181
  // src/blocks/FindMultipleHotspots.tsx
3323
- import { forwardRef as forwardRef10, useMemo as useMemo16, useState as useState18 } from "react";
3324
- import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
4182
+ import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
4183
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
3325
4184
  var INTERACTION7 = "findMultipleHotspots";
3326
4185
  function FindMultipleHotspotsInner(props, ref) {
3327
- const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3328
- const [selected, setSelected] = useState18(/* @__PURE__ */ new Set());
3329
- const [checked, setChecked] = useState18(false);
4186
+ const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4187
+ const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
4188
+ const [checked, setChecked] = useState19(false);
3330
4189
  const assessment = useAssessmentState(props.enclosingLessonId);
3331
4190
  const toggle = (id) => {
3332
4191
  setSelected((prev) => {
@@ -3335,9 +4194,10 @@ function FindMultipleHotspotsInner(props, ref) {
3335
4194
  else next.add(id);
3336
4195
  return next;
3337
4196
  });
4197
+ setChecked(false);
3338
4198
  };
3339
4199
  const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
3340
- const handle = useMemo16(
4200
+ const handle = useMemo17(
3341
4201
  () => buildAssessmentHandle({
3342
4202
  checkId,
3343
4203
  getScore: () => checked && correct ? 1 : 0,
@@ -3367,7 +4227,7 @@ function FindMultipleHotspotsInner(props, ref) {
3367
4227
  );
3368
4228
  useAssessmentHandleRegistration(checkId, handle, ref);
3369
4229
  const submit = () => {
3370
- if (selected.size === 0) return;
4230
+ if (selected.size === 0 || checked) return;
3371
4231
  setChecked(true);
3372
4232
  assessment.answer({
3373
4233
  checkId,
@@ -3381,14 +4241,14 @@ function FindMultipleHotspotsInner(props, ref) {
3381
4241
  interactionType: INTERACTION7,
3382
4242
  score: 1,
3383
4243
  maxScore: 1,
3384
- passingScore: props.passingScore
4244
+ passingScore: props.passingScore ?? 1
3385
4245
  });
3386
4246
  }
3387
4247
  };
3388
- return /* @__PURE__ */ jsxs18("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
3389
- /* @__PURE__ */ jsxs18("div", { style: { position: "relative", display: "inline-block" }, children: [
3390
- /* @__PURE__ */ jsx23("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3391
- props.targets.map((t) => /* @__PURE__ */ jsx23(
4248
+ return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
4249
+ /* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
4250
+ /* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4251
+ props.targets.map((t) => /* @__PURE__ */ jsx27(
3392
4252
  "button",
3393
4253
  {
3394
4254
  type: "button",
@@ -3407,14 +4267,14 @@ function FindMultipleHotspotsInner(props, ref) {
3407
4267
  t.id
3408
4268
  ))
3409
4269
  ] }),
3410
- /* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
3411
- checked ? /* @__PURE__ */ jsx23("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4270
+ /* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
4271
+ checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
3412
4272
  ] });
3413
4273
  }
3414
- var FindMultipleHotspotsInnerForwarded = forwardRef10(FindMultipleHotspotsInner);
3415
- var FindMultipleHotspots = forwardRef10(
4274
+ var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
4275
+ var FindMultipleHotspots = forwardRef11(
3416
4276
  function FindMultipleHotspots2(props, ref) {
3417
- return /* @__PURE__ */ jsx23(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx23(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
4277
+ return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
3418
4278
  }
3419
4279
  );
3420
4280
  setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
@@ -3431,14 +4291,14 @@ import {
3431
4291
  } from "@lessonkit/core";
3432
4292
 
3433
4293
  // src/theme/ThemeProvider.tsx
3434
- import React25, {
3435
- createContext as createContext4,
3436
- useCallback as useCallback9,
3437
- useContext as useContext4,
4294
+ import React29, {
4295
+ createContext as createContext6,
4296
+ useCallback as useCallback10,
4297
+ useContext as useContext8,
3438
4298
  useLayoutEffect as useLayoutEffect2,
3439
- useMemo as useMemo17,
3440
- useRef as useRef12,
3441
- useState as useState19
4299
+ useMemo as useMemo18,
4300
+ useRef as useRef16,
4301
+ useState as useState20
3442
4302
  } from "react";
3443
4303
  import {
3444
4304
  brandThemeOverrides,
@@ -3465,11 +4325,11 @@ function applyCssVariables(target, vars, previousKeys) {
3465
4325
  }
3466
4326
 
3467
4327
  // src/theme/ThemeProvider.tsx
3468
- import { jsx as jsx24 } from "react/jsx-runtime";
3469
- var ThemeContext = createContext4(null);
4328
+ import { jsx as jsx28 } from "react/jsx-runtime";
4329
+ var ThemeContext = createContext6(null);
3470
4330
  var useIsoLayoutEffect2 = (
3471
4331
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
3472
- typeof window !== "undefined" ? useLayoutEffect2 : React25.useEffect
4332
+ typeof window !== "undefined" ? useLayoutEffect2 : React29.useEffect
3473
4333
  );
3474
4334
  function getSystemMode() {
3475
4335
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -3488,7 +4348,7 @@ function ThemeProvider(props) {
3488
4348
  const preset = props.preset ?? "default";
3489
4349
  const mode = props.mode ?? "light";
3490
4350
  const targetKind = props.target ?? "document";
3491
- const [resolvedMode, setResolvedMode] = useState19(
4351
+ const [resolvedMode, setResolvedMode] = useState20(
3492
4352
  () => mode === "system" ? getSystemMode() : mode
3493
4353
  );
3494
4354
  useIsoLayoutEffect2(() => {
@@ -3504,20 +4364,20 @@ function ThemeProvider(props) {
3504
4364
  return () => mq.removeEventListener("change", onChange);
3505
4365
  }, [mode]);
3506
4366
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
3507
- const effectiveTheme = useMemo17(() => {
4367
+ const effectiveTheme = useMemo18(() => {
3508
4368
  const modeBase = resolveModeBase(mode, dataTheme);
3509
4369
  const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
3510
4370
  return mergeThemes(base, props.theme ?? {});
3511
4371
  }, [preset, mode, dataTheme, props.theme]);
3512
- const hostRef = useRef12(null);
3513
- const appliedKeysRef = useRef12(/* @__PURE__ */ new Set());
4372
+ const hostRef = useRef16(null);
4373
+ const appliedKeysRef = useRef16(/* @__PURE__ */ new Set());
3514
4374
  useIsoLayoutEffect2(() => {
3515
4375
  if (targetKind === "document" && typeof document !== "undefined") {
3516
4376
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
3517
4377
  return () => document.documentElement.removeAttribute("data-lk-theme");
3518
4378
  }
3519
4379
  }, [targetKind, dataTheme]);
3520
- const inject = useCallback9(() => {
4380
+ const inject = useCallback10(() => {
3521
4381
  const vars = themeToCssVariables(effectiveTheme);
3522
4382
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
3523
4383
  if (!el) return;
@@ -3534,7 +4394,7 @@ function ThemeProvider(props) {
3534
4394
  appliedKeysRef.current = /* @__PURE__ */ new Set();
3535
4395
  };
3536
4396
  }, [inject, targetKind]);
3537
- const value = useMemo17(
4397
+ const value = useMemo18(
3538
4398
  () => ({
3539
4399
  theme: effectiveTheme,
3540
4400
  preset,
@@ -3544,12 +4404,12 @@ function ThemeProvider(props) {
3544
4404
  [effectiveTheme, preset, mode, dataTheme]
3545
4405
  );
3546
4406
  if (targetKind === "document") {
3547
- return /* @__PURE__ */ jsx24(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx24("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
4407
+ return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
3548
4408
  }
3549
- return /* @__PURE__ */ jsx24(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx24("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
4409
+ return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
3550
4410
  }
3551
4411
  function useTheme() {
3552
- const ctx = useContext4(ThemeContext);
4412
+ const ctx = useContext8(ThemeContext);
3553
4413
  if (!ctx) {
3554
4414
  throw new Error("useTheme must be used within a ThemeProvider");
3555
4415
  }
@@ -3561,9 +4421,18 @@ import {
3561
4421
  ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
3562
4422
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
3563
4423
  PAGE_ALLOWED_CHILD_TYPES,
4424
+ SLIDE_ALLOWED_CHILD_TYPES,
4425
+ SLIDE_DECK_ALLOWED_CHILD_TYPES,
3564
4426
  COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
3565
4427
  } from "@lessonkit/core";
3566
- var COMPOUND_PARENTS = ["Lesson", "Page", "InteractiveBook", "AssessmentSequence"];
4428
+ var COMPOUND_PARENTS = [
4429
+ "Lesson",
4430
+ "Page",
4431
+ "InteractiveBook",
4432
+ "Slide",
4433
+ "SlideDeck",
4434
+ "AssessmentSequence"
4435
+ ];
3567
4436
  function extendParents(entry) {
3568
4437
  if (!entry.parentConstraints?.length) return entry;
3569
4438
  const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
@@ -3667,6 +4536,53 @@ var v3CompoundAndContentEntries = [
3667
4536
  theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
3668
4537
  telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
3669
4538
  },
4539
+ {
4540
+ type: "Slide",
4541
+ category: "container",
4542
+ compoundContract: true,
4543
+ h5pMachineName: "H5P.CoursePresentation",
4544
+ h5pAlias: "Course Presentation slide",
4545
+ description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
4546
+ allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
4547
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Slide,
4548
+ props: [
4549
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4550
+ { name: "title", type: "string", required: false, description: "Slide title." },
4551
+ { name: "children", type: "ReactNode", required: true, description: "Slide content." }
4552
+ ],
4553
+ requiredIds: [],
4554
+ optionalIds: ["blockId"],
4555
+ parentConstraints: ["SlideDeck"],
4556
+ a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
4557
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
4558
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
4559
+ },
4560
+ {
4561
+ type: "SlideDeck",
4562
+ category: "container",
4563
+ compoundContract: true,
4564
+ h5pMachineName: "H5P.CoursePresentation",
4565
+ h5pAlias: "Course Presentation",
4566
+ description: "Multi-slide presentation with keyboard navigation.",
4567
+ allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
4568
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.SlideDeck,
4569
+ props: [
4570
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4571
+ { name: "title", type: "string", required: true, description: "Deck title." },
4572
+ { name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
4573
+ { name: "children", type: "Slide[]", required: true, description: "Slides." }
4574
+ ],
4575
+ requiredIds: ["blockId"],
4576
+ parentConstraints: ["Lesson"],
4577
+ a11y: {
4578
+ element: "section",
4579
+ ariaLabel: "Slide deck",
4580
+ keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
4581
+ notes: "H5P Course Presentation equivalent."
4582
+ },
4583
+ theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
4584
+ telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
4585
+ },
3670
4586
  {
3671
4587
  type: "Accordion",
3672
4588
  category: "content",
@@ -4212,6 +5128,8 @@ export {
4212
5128
  Quiz,
4213
5129
  Reflection,
4214
5130
  Scenario,
5131
+ Slide,
5132
+ SlideDeck,
4215
5133
  Text,
4216
5134
  ThemeProvider,
4217
5135
  TrueFalse,