@lessonkit/react 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  Accordion: () => Accordion,
34
+ ArithmeticQuiz: () => ArithmeticQuiz,
34
35
  AssessmentSequence: () => AssessmentSequence,
35
36
  BLOCK_CATALOG: () => BLOCK_CATALOG,
36
37
  BLOCK_CATALOG_V2: () => BLOCK_CATALOG_V2,
@@ -39,6 +40,7 @@ __export(index_exports, {
39
40
  DialogCards: () => DialogCards,
40
41
  DragAndDrop: () => DragAndDrop,
41
42
  DragTheWords: () => DragTheWords,
43
+ Essay: () => Essay,
42
44
  FillInTheBlanks: () => FillInTheBlanks,
43
45
  FindHotspot: () => FindHotspot,
44
46
  FindMultipleHotspots: () => FindMultipleHotspots,
@@ -46,36 +48,48 @@ __export(index_exports, {
46
48
  Heading: () => Heading,
47
49
  Image: () => Image,
48
50
  ImageHotspots: () => ImageHotspots,
51
+ ImagePairing: () => ImagePairing,
52
+ ImageSequencing: () => ImageSequencing,
49
53
  ImageSlider: () => ImageSlider,
54
+ InformationWall: () => InformationWall,
50
55
  InteractiveBook: () => InteractiveBook,
56
+ InteractiveVideo: () => InteractiveVideo,
51
57
  KnowledgeCheck: () => KnowledgeCheck,
52
58
  Lesson: () => Lesson,
53
59
  LessonkitProvider: () => LessonkitProvider,
54
60
  MarkTheWords: () => MarkTheWords,
61
+ MemoryGame: () => MemoryGame,
55
62
  Page: () => Page,
63
+ ParallaxSlideshow: () => ParallaxSlideshow,
56
64
  ProgressTracker: () => ProgressTracker,
65
+ Questionnaire: () => Questionnaire,
57
66
  Quiz: () => Quiz,
58
67
  Reflection: () => Reflection,
59
68
  Scenario: () => Scenario,
60
69
  Slide: () => Slide,
61
70
  SlideDeck: () => SlideDeck,
71
+ Summary: () => Summary,
62
72
  Text: () => Text,
63
73
  ThemeProvider: () => ThemeProvider,
74
+ TimedCue: () => TimedCue,
64
75
  TrueFalse: () => TrueFalse,
76
+ Video: () => Video,
77
+ assertProductionCourseConfig: () => assertProductionCourseConfig,
65
78
  blockCatalogV2Version: () => blockCatalogV2Version,
66
79
  blockCatalogV3Version: () => blockCatalogV3Version,
67
80
  blockCatalogVersion: () => blockCatalogVersion,
68
81
  buildBlockCatalog: () => buildBlockCatalog,
69
- buildTelemetryEvent: () => import_core19.buildTelemetryEvent,
70
- createLessonkitRuntime: () => import_core19.createLessonkitRuntime,
71
- createPluginRegistry: () => import_core19.createPluginRegistry,
72
- createTelemetryPipeline: () => import_core19.createTelemetryPipeline,
73
- defineAssessmentPlugin: () => import_core19.defineAssessmentPlugin,
74
- defineLifecyclePlugin: () => import_core19.defineLifecyclePlugin,
75
- defineTelemetryPlugin: () => import_core19.defineTelemetryPlugin,
82
+ buildTelemetryEvent: () => import_core21.buildTelemetryEvent,
83
+ createLessonkitRuntime: () => import_core21.createLessonkitRuntime,
84
+ createPluginRegistry: () => import_core21.createPluginRegistry,
85
+ createTelemetryPipeline: () => import_core21.createTelemetryPipeline,
86
+ defineAssessmentPlugin: () => import_core21.defineAssessmentPlugin,
87
+ defineLifecyclePlugin: () => import_core21.defineLifecyclePlugin,
88
+ defineTelemetryPlugin: () => import_core21.defineTelemetryPlugin,
76
89
  getBlockCatalogEntry: () => getBlockCatalogEntry,
77
90
  resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
78
91
  resetQuizWarningsForTests: () => resetQuizWarningsForTests,
92
+ shouldEnforceProductionGuard: () => shouldEnforceProductionGuard,
79
93
  useAssessmentState: () => useAssessmentState,
80
94
  useCompletion: () => useCompletion,
81
95
  useLessonkit: () => useLessonkit,
@@ -99,15 +113,11 @@ var import_core8 = require("@lessonkit/core");
99
113
 
100
114
  // src/runtime/observability.ts
101
115
  var import_xapi = require("@lessonkit/xapi");
102
- var import_meta = {};
103
- function createXapiQueueFromObservability(observability) {
104
- const opts = {};
105
- if (observability?.onXapiQueueDepth) {
106
- opts.onDepth = observability.onXapiQueueDepth;
107
- }
108
- if (observability?.onXapiQueueCap) {
109
- opts.onCap = observability.onXapiQueueCap;
110
- }
116
+ function createXapiQueueFromObservability(getObservability) {
117
+ const opts = {
118
+ onDepth: (size) => getObservability?.()?.onXapiQueueDepth?.(size),
119
+ onCap: () => getObservability?.()?.onXapiQueueCap?.()
120
+ };
111
121
  return (0, import_xapi.createInMemoryXAPIQueue)(opts);
112
122
  }
113
123
  function wrapBatchSink(batchSink, observability) {
@@ -122,32 +132,6 @@ function wrapBatchSink(batchSink, observability) {
122
132
  }
123
133
  };
124
134
  }
125
- function warnMissingProductionObservability(observability, opts) {
126
- let isProduction = false;
127
- try {
128
- isProduction = import_meta.env?.PROD === true;
129
- } catch {
130
- }
131
- if (!isProduction) {
132
- const g = globalThis;
133
- isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
134
- }
135
- if (!isProduction) return;
136
- if (!opts.trackingEnabled && !opts.xapiEnabled) return;
137
- const hooks = [
138
- observability?.onTelemetrySinkError,
139
- observability?.onTelemetryBufferDrop,
140
- observability?.onXapiQueueDepth,
141
- observability?.onXapiQueueCap,
142
- observability?.onLxpackBridgeMiss
143
- ];
144
- if (hooks.some(Boolean)) return;
145
- if (typeof console !== "undefined") {
146
- console.warn(
147
- "[lessonkit] Production deployment without observability hooks \u2014 telemetry/xAPI failures and buffer drops will be silent. See https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html"
148
- );
149
- }
150
- }
151
135
  function wrapTrackingSink(sink, observability) {
152
136
  if (!sink || !observability?.onTelemetrySinkError) return sink;
153
137
  const onError = observability.onTelemetrySinkError;
@@ -157,16 +141,108 @@ function wrapTrackingSink(sink, observability) {
157
141
  if (result != null && typeof result.catch === "function") {
158
142
  return result.catch((err) => {
159
143
  onError(err, { sinkId: "tracking" });
144
+ throw err;
160
145
  });
161
146
  }
162
147
  return result;
163
148
  } catch (err) {
164
149
  onError(err, { sinkId: "tracking" });
165
- return void 0;
150
+ throw err;
166
151
  }
167
152
  });
168
153
  }
169
154
 
155
+ // src/runtime/productionGuard.ts
156
+ var import_meta = {};
157
+ function isProductionEnvironment() {
158
+ try {
159
+ if (import_meta.env?.PROD === true) return true;
160
+ } catch {
161
+ }
162
+ const g = globalThis;
163
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
164
+ }
165
+ function shouldEnforceProductionGuard() {
166
+ try {
167
+ if (import_meta.env?.MODE === "test") return false;
168
+ } catch {
169
+ }
170
+ return isProductionEnvironment();
171
+ }
172
+ function looksLikeConsoleSink(fn) {
173
+ if (typeof fn !== "function") return false;
174
+ const src = Function.prototype.toString.call(fn);
175
+ return /console\.(log|debug|info)\s*\(/.test(src);
176
+ }
177
+ function isTrackingDeliveryConfigured(tracking) {
178
+ if (!tracking || tracking.enabled === false) return false;
179
+ return Boolean(tracking.sink || tracking.batchSink);
180
+ }
181
+ function isXapiDeliveryConfigured(xapi) {
182
+ if (!xapi || xapi.enabled === false) return false;
183
+ if (xapi.client) return true;
184
+ return typeof xapi.transport === "function";
185
+ }
186
+ function trackingUsesConsole(config) {
187
+ const tracking = config.tracking;
188
+ if (!tracking || tracking.enabled === false) return false;
189
+ if (tracking.batchSink && looksLikeConsoleSink(tracking.batchSink)) return true;
190
+ if (tracking.sink && looksLikeConsoleSink(tracking.sink)) return true;
191
+ return false;
192
+ }
193
+ function xapiUsesConsole(config) {
194
+ const xapi = config.xapi;
195
+ if (!xapi || xapi.enabled === false || xapi.client) return false;
196
+ return typeof xapi.transport === "function" && looksLikeConsoleSink(xapi.transport);
197
+ }
198
+ function observabilityIncomplete(observability, opts) {
199
+ if (!opts.trackingEnabled && !opts.xapiEnabled) return false;
200
+ const required = [observability?.onLxpackBridgeMiss];
201
+ if (opts.trackingEnabled) {
202
+ required.push(observability?.onTelemetrySinkError, observability?.onTelemetryBufferDrop);
203
+ }
204
+ if (opts.xapiEnabled) {
205
+ required.push(
206
+ observability?.onXapiQueueDepth,
207
+ observability?.onXapiQueueCap,
208
+ observability?.onXapiTransportError
209
+ );
210
+ }
211
+ return required.some((hook) => !hook);
212
+ }
213
+ function requiredObservabilityHookCount(opts) {
214
+ let count = 1;
215
+ if (opts.trackingEnabled) count += 2;
216
+ if (opts.xapiEnabled) count += 3;
217
+ return count;
218
+ }
219
+ function assertProductionCourseConfig(config) {
220
+ if (!isProductionEnvironment()) return;
221
+ if (config.tracking && config.tracking.enabled !== false && !isTrackingDeliveryConfigured(config.tracking)) {
222
+ throw new Error(
223
+ "[lessonkit] Production build has tracking enabled but no sink or batchSink configured."
224
+ );
225
+ }
226
+ const trackingEnabled = isTrackingDeliveryConfigured(config.tracking);
227
+ const xapiEnabled = isXapiDeliveryConfigured(config.xapi);
228
+ if (trackingUsesConsole(config)) {
229
+ throw new Error(
230
+ "[lessonkit] Production build uses console telemetry sinks. Wire createFetchBatchSink or a real sink. See production checklist."
231
+ );
232
+ }
233
+ if (xapiUsesConsole(config)) {
234
+ throw new Error(
235
+ "[lessonkit] Production build uses console xAPI transport. Wire createFetchTransport to your LRS proxy. See production checklist."
236
+ );
237
+ }
238
+ if (observabilityIncomplete(config.observability, { trackingEnabled, xapiEnabled })) {
239
+ const hookCount = requiredObservabilityHookCount({ trackingEnabled, xapiEnabled });
240
+ throw new Error(
241
+ `[lessonkit] Production build missing observability hooks. Wire all ${hookCount} config.observability callbacks before go-live.`
242
+ );
243
+ }
244
+ }
245
+
170
246
  // src/provider/useLessonkitProviderRuntime.ts
171
247
  var import_xapi5 = require("@lessonkit/xapi");
172
248
 
@@ -265,7 +341,7 @@ var import_core4 = require("@lessonkit/core");
265
341
 
266
342
  // src/runtime/xapi.ts
267
343
  var import_xapi3 = require("@lessonkit/xapi");
268
- function createXapiClientFromConfig(config, queue) {
344
+ function createXapiClientFromConfig(config, queue, observability) {
269
345
  if (config.xapi?.enabled === false) return null;
270
346
  if (config.xapi?.client) return config.xapi.client;
271
347
  if (!config.courseId) return null;
@@ -275,7 +351,9 @@ function createXapiClientFromConfig(config, queue) {
275
351
  courseId: config.courseId,
276
352
  transport: config.xapi?.transport,
277
353
  exitTransport: config.xapi?.exitTransport,
278
- queue
354
+ abortInFlight: config.xapi?.abortInFlight,
355
+ queue,
356
+ onTransportError: observability?.onXapiTransportError
279
357
  });
280
358
  }
281
359
 
@@ -359,12 +437,26 @@ function emitTelemetryWithPlugins(opts) {
359
437
  }
360
438
 
361
439
  // src/provider/courseStarted/emit.ts
440
+ function resolveTrackingClient(source) {
441
+ return typeof source === "function" ? source() : source;
442
+ }
362
443
  var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
444
+ var courseStartedEmitFlights = /* @__PURE__ */ new Map();
363
445
  function isTrackingActive(tracking) {
364
446
  return tracking?.enabled !== false;
365
447
  }
366
448
  function isCourseStartedSinkSettled(result) {
367
- return result === "emitted";
449
+ return result === "emitted" || result === "filtered";
450
+ }
451
+ async function deliverToTracking(client, event) {
452
+ if (client.deliver) {
453
+ return client.deliver(event);
454
+ }
455
+ client.track(event);
456
+ const flushed = await client.flush?.();
457
+ if (flushed === false) return false;
458
+ if (flushed === true) return true;
459
+ return false;
368
460
  }
369
461
  function buildCourseStartedEvent(opts) {
370
462
  const pluginCtx = buildPluginContext({
@@ -389,8 +481,7 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
389
481
  }
390
482
  const existing = courseStartedTrackingFlights.get(flightKey);
391
483
  if (existing) {
392
- const settled = await existing;
393
- if (settled) return true;
484
+ return existing;
394
485
  }
395
486
  let resolveFlight;
396
487
  const flight = new Promise((resolve) => {
@@ -403,9 +494,13 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
403
494
  resolveFlight(false);
404
495
  return;
405
496
  }
406
- tracking.track(event);
407
- const delivered = await tracking.flush?.();
408
- if (delivered === false) {
497
+ const client = resolveTrackingClient(tracking);
498
+ const delivered = await deliverToTracking(client, event);
499
+ if (shouldCommit && !shouldCommit()) {
500
+ resolveFlight(false);
501
+ return;
502
+ }
503
+ if (!delivered) {
409
504
  resolveFlight(false);
410
505
  return;
411
506
  }
@@ -417,7 +512,9 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
417
512
  } catch {
418
513
  resolveFlight(false);
419
514
  } finally {
420
- courseStartedTrackingFlights.delete(flightKey);
515
+ if (courseStartedTrackingFlights.get(flightKey) === flight) {
516
+ courseStartedTrackingFlights.delete(flightKey);
517
+ }
421
518
  }
422
519
  })();
423
520
  return flight;
@@ -497,12 +594,55 @@ async function emitCourseStartedToTrackingOnly(opts) {
497
594
  }
498
595
  }
499
596
  async function emitPendingCourseStarted(opts) {
597
+ const flightKey = `${opts.sessionId}:${opts.courseId}`;
598
+ for (let attempt = 0; attempt < 2; attempt += 1) {
599
+ const existing = courseStartedEmitFlights.get(flightKey);
600
+ const flight = existing ?? startPendingCourseStartedFlight(opts, flightKey);
601
+ const result = await flight;
602
+ if (result !== "failed") return result;
603
+ const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
604
+ const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
605
+ opts.storage,
606
+ opts.sessionId,
607
+ opts.courseId
608
+ );
609
+ const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
610
+ opts.storage,
611
+ opts.sessionId,
612
+ opts.courseId
613
+ );
614
+ if (sessionStarted && trackingEmitted && pipelineDelivered) {
615
+ return "emitted";
616
+ }
617
+ if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
618
+ }
619
+ return "failed";
620
+ }
621
+ function startPendingCourseStartedFlight(opts, flightKey) {
622
+ const flight = emitPendingCourseStartedInner(opts);
623
+ courseStartedEmitFlights.set(flightKey, flight);
624
+ void flight.finally(() => {
625
+ if (courseStartedEmitFlights.get(flightKey) === flight) {
626
+ courseStartedEmitFlights.delete(flightKey);
627
+ }
628
+ });
629
+ return flight;
630
+ }
631
+ async function emitPendingCourseStartedInner(opts) {
500
632
  const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
501
633
  opts.storage,
502
634
  opts.sessionId,
503
635
  opts.courseId
504
636
  );
505
637
  const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
638
+ const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
639
+ opts.storage,
640
+ opts.sessionId,
641
+ opts.courseId
642
+ );
643
+ if (sessionStarted && trackingEmitted && pipelineDelivered) {
644
+ return "emitted";
645
+ }
506
646
  if (sessionStarted && !trackingEmitted) {
507
647
  return emitCourseStartedToTrackingOnly(opts);
508
648
  }
@@ -514,14 +654,6 @@ async function emitPendingCourseStarted(opts) {
514
654
  if (!trackingEmitted && !sessionStarted) {
515
655
  return emitCourseStarted(opts);
516
656
  }
517
- const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
518
- opts.storage,
519
- opts.sessionId,
520
- opts.courseId
521
- );
522
- if (sessionStarted && trackingEmitted && pipelineDelivered) {
523
- return "emitted";
524
- }
525
657
  if (sessionStarted && trackingEmitted && !pipelineDelivered) {
526
658
  const event = buildCourseStartedEvent(opts);
527
659
  if (event === null) return "filtered";
@@ -532,7 +664,7 @@ async function emitPendingCourseStarted(opts) {
532
664
  onXapiStatementSent: opts.onXapiStatementSent
533
665
  });
534
666
  }
535
- return "emitted";
667
+ return "failed";
536
668
  }
537
669
  function assertTrackingSinkConfig(tracking) {
538
670
  if (!tracking?.sink || !tracking?.batchSink) return;
@@ -580,6 +712,9 @@ function useLessonkitProviderRuntime(config) {
580
712
  () => ({ ...config, courseId: normalizedCourseId }),
581
713
  [config, normalizedCourseId]
582
714
  );
715
+ if (shouldEnforceProductionGuard()) {
716
+ assertProductionCourseConfig(normalizedConfig);
717
+ }
583
718
  const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
584
719
  (0, import_react.useEffect)(() => {
585
720
  if (useV2Runtime) return;
@@ -675,7 +810,7 @@ function useLessonkitProviderRuntime(config) {
675
810
  }, []);
676
811
  const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
677
812
  activeLessonIdRef.current = progress.activeLessonId;
678
- const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(normalizedConfig.observability));
813
+ const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(() => observabilityRef.current));
679
814
  const xapiRef = (0, import_react.useRef)(null);
680
815
  const [xapi, setXapi] = (0, import_react.useState)(null);
681
816
  const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
@@ -696,13 +831,17 @@ function useLessonkitProviderRuntime(config) {
696
831
  }
697
832
  void xapiRef.current?.flush();
698
833
  }
699
- xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
834
+ xapiQueueRef.current = createXapiQueueFromObservability(() => observabilityRef.current);
700
835
  prevXapiCourseIdRef.current = courseId;
701
836
  xapiCourseStartedSentOnClientRef.current = false;
702
837
  xapiBootstrapSendRef.current = false;
703
838
  }
704
839
  const prev = xapiRef.current;
705
- const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
840
+ const next = createXapiClientFromConfig(
841
+ normalizedConfig,
842
+ xapiQueueRef.current,
843
+ observabilityRef.current
844
+ );
706
845
  xapiRef.current = next;
707
846
  setXapi(next);
708
847
  let bootstrapSent = false;
@@ -759,7 +898,12 @@ function useLessonkitProviderRuntime(config) {
759
898
  })();
760
899
  return () => {
761
900
  cancelled = true;
762
- void prev?.flush();
901
+ void (async () => {
902
+ try {
903
+ await prev?.flush();
904
+ } catch {
905
+ }
906
+ })();
763
907
  };
764
908
  }, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
765
909
  const trackingRef = (0, import_react.useRef)((0, import_core8.createTrackingClient)());
@@ -823,13 +967,13 @@ function useLessonkitProviderRuntime(config) {
823
967
  } else if (courseStartedFullySettled) {
824
968
  courseStartedEmittedToSinkRef.current = true;
825
969
  } else if (!courseStartedEmittedToSinkRef.current) {
826
- const generation = ++courseStartedEmitGenerationRef.current;
970
+ const generation = courseStartedEmitGenerationRef.current;
827
971
  const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
828
972
  void (async () => {
829
973
  if (generation !== courseStartedEmitGenerationRef.current) return;
830
974
  const result = await emitPendingCourseStarted({
831
975
  pluginHost: pluginHostRef.current,
832
- tracking: next,
976
+ tracking: () => trackingRef.current,
833
977
  xapi: xapiRef.current,
834
978
  storage: defaultStorage,
835
979
  sessionId,
@@ -839,7 +983,7 @@ function useLessonkitProviderRuntime(config) {
839
983
  lxpackBridge: lxpackBridgeModeRef.current,
840
984
  onLxpackBridgeMiss,
841
985
  extraSinks: extraSinksRef.current,
842
- skipXapi: xapiCourseStartedSentOnClientRef.current,
986
+ skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
843
987
  onXapiStatementSent: () => {
844
988
  xapiCourseStartedSentOnClientRef.current = true;
845
989
  },
@@ -850,7 +994,6 @@ function useLessonkitProviderRuntime(config) {
850
994
  })();
851
995
  }
852
996
  return () => {
853
- courseStartedEmitGenerationRef.current += 1;
854
997
  if (prev !== trackingRef.current) {
855
998
  void disposeTrackingClient(prev);
856
999
  }
@@ -928,9 +1071,11 @@ function useLessonkitProviderRuntime(config) {
928
1071
  } catch {
929
1072
  }
930
1073
  if (!courseStartedEmittedToSinkRef.current) {
1074
+ const generation = courseStartedEmitGenerationRef.current;
1075
+ const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
931
1076
  const result = await emitPendingCourseStarted({
932
1077
  pluginHost: pluginHostRef.current,
933
- tracking: trackingRef.current,
1078
+ tracking: () => trackingRef.current,
934
1079
  xapi: xapiRef.current,
935
1080
  storage: defaultStorage,
936
1081
  sessionId,
@@ -940,11 +1085,13 @@ function useLessonkitProviderRuntime(config) {
940
1085
  lxpackBridge: lxpackBridgeModeRef.current,
941
1086
  onLxpackBridgeMiss,
942
1087
  extraSinks: extraSinksRef.current,
943
- skipXapi: xapiCourseStartedSentOnClientRef.current,
1088
+ skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
944
1089
  onXapiStatementSent: () => {
945
1090
  xapiCourseStartedSentOnClientRef.current = true;
946
- }
1091
+ },
1092
+ shouldCommit
947
1093
  });
1094
+ if (generation !== courseStartedEmitGenerationRef.current) return;
948
1095
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
949
1096
  }
950
1097
  })();
@@ -1018,20 +1165,6 @@ function useLessonkitProviderRuntime(config) {
1018
1165
  window.removeEventListener("pagehide", flushOnPageExit);
1019
1166
  };
1020
1167
  }, []);
1021
- (0, import_react.useEffect)(() => {
1022
- warnMissingProductionObservability(observabilityRef.current, {
1023
- trackingEnabled: isTrackingActive(normalizedConfig.tracking),
1024
- xapiEnabled: normalizedConfig.xapi?.enabled !== false && Boolean(
1025
- normalizedConfig.xapi?.client || normalizedConfig.xapi?.transport || normalizedConfig.xapi?.enabled === true
1026
- )
1027
- });
1028
- }, [
1029
- normalizedConfig.tracking,
1030
- normalizedConfig.xapi?.enabled,
1031
- normalizedConfig.xapi?.client,
1032
- normalizedConfig.xapi?.transport,
1033
- normalizedConfig.observability
1034
- ]);
1035
1168
  const setActiveLesson = (0, import_react.useCallback)(
1036
1169
  (lessonId) => {
1037
1170
  if (useV2Runtime && headlessRef.current) {
@@ -3062,6 +3195,8 @@ function useCompoundPersistence(opts) {
3062
3195
  }, [ctx, opts.index, opts.pageCount]);
3063
3196
  const buildStateRef = (0, import_react21.useRef)(buildState);
3064
3197
  buildStateRef.current = buildState;
3198
+ const transformStateRef = (0, import_react21.useRef)(opts.transformState);
3199
+ transformStateRef.current = opts.transformState;
3065
3200
  const persistNowRef = (0, import_react21.useRef)(() => {
3066
3201
  });
3067
3202
  const finalizeHydration = (0, import_react21.useCallback)(
@@ -3127,7 +3262,9 @@ function useCompoundPersistence(opts) {
3127
3262
  const persistNow = (0, import_react21.useCallback)(() => {
3128
3263
  if (!opts.enabled || !opts.courseId) return;
3129
3264
  if (skipSaveUntilHydratedRef.current) return;
3130
- saveResume(buildStateRef.current());
3265
+ const built = buildStateRef.current();
3266
+ const state = transformStateRef.current ? transformStateRef.current(built) : built;
3267
+ saveResume(state);
3131
3268
  }, [opts.enabled, opts.courseId, saveResume]);
3132
3269
  (0, import_react21.useEffect)(() => {
3133
3270
  persistNowRef.current = persistNow;
@@ -3183,7 +3320,8 @@ function useCompoundShell(opts) {
3183
3320
  index: opts.index,
3184
3321
  setIndex: opts.setIndex,
3185
3322
  enabled: opts.persistEnabled,
3186
- storage: opts.storage
3323
+ storage: opts.storage,
3324
+ transformState: opts.transformState
3187
3325
  });
3188
3326
  const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
3189
3327
  const visibleIndex = (0, import_core15.clampCompoundPageIndex)(opts.index, opts.pageCount);
@@ -3238,6 +3376,8 @@ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
3238
3376
  "InteractiveBook",
3239
3377
  "Slide",
3240
3378
  "SlideDeck",
3379
+ "TimedCue",
3380
+ "InteractiveVideo",
3241
3381
  "AssessmentSequence"
3242
3382
  ]);
3243
3383
  function warnOrThrow(msg, strict) {
@@ -3466,14 +3606,40 @@ function Image(props) {
3466
3606
  }
3467
3607
  setLessonkitBlockType(Image, "Image");
3468
3608
 
3469
- // src/blocks/Page.tsx
3609
+ // src/blocks/Video.tsx
3470
3610
  var import_react26 = require("react");
3471
3611
  var import_jsx_runtime17 = require("react/jsx-runtime");
3612
+ function Video(props) {
3613
+ const blockId = (0, import_react26.useMemo)(
3614
+ () => normalizeComponentId(props.blockId, "blockId"),
3615
+ [props.blockId]
3616
+ );
3617
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { "aria-label": props.title ?? "Video", "data-lk-block-id": blockId, "data-testid": "video", children: [
3618
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { "data-testid": "video-title", children: props.title }) : null,
3619
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3620
+ "video",
3621
+ {
3622
+ controls: true,
3623
+ preload: "metadata",
3624
+ poster: props.poster,
3625
+ src: props.src,
3626
+ "data-testid": "video-player",
3627
+ style: { maxWidth: "100%" },
3628
+ children: props.captions ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("track", { kind: "captions", src: props.captions, srcLang: "en", label: "Captions", default: true }) : null
3629
+ }
3630
+ )
3631
+ ] });
3632
+ }
3633
+ setLessonkitBlockType(Video, "Video");
3634
+
3635
+ // src/blocks/Page.tsx
3636
+ var import_react27 = require("react");
3637
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3472
3638
  function Page(props) {
3473
3639
  validateCompoundChildren("Page", props.children);
3474
3640
  const { track } = useLessonkit();
3475
3641
  const lessonId = useEnclosingLessonId();
3476
- (0, import_react26.useEffect)(() => {
3642
+ (0, import_react27.useEffect)(() => {
3477
3643
  if (props.hidden || !lessonId || props.parentType) return;
3478
3644
  track(
3479
3645
  "compound_page_viewed",
@@ -3485,7 +3651,7 @@ function Page(props) {
3485
3651
  { lessonId }
3486
3652
  );
3487
3653
  }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
3488
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3654
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3489
3655
  "section",
3490
3656
  {
3491
3657
  "aria-label": props.title ?? "Page",
@@ -3493,8 +3659,8 @@ function Page(props) {
3493
3659
  "data-testid": `page-${props.blockId}`,
3494
3660
  hidden: props.hidden ? true : void 0,
3495
3661
  children: [
3496
- props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { children: props.title }) : null,
3497
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: props.children }) })
3662
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }) : null,
3663
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { children: props.children }) })
3498
3664
  ]
3499
3665
  }
3500
3666
  );
@@ -3502,9 +3668,9 @@ function Page(props) {
3502
3668
  setLessonkitBlockType(Page, "Page");
3503
3669
 
3504
3670
  // src/blocks/InteractiveBook.tsx
3505
- var import_react27 = __toESM(require("react"), 1);
3506
- var import_jsx_runtime18 = require("react/jsx-runtime");
3507
- var InteractiveBookInner = (0, import_react27.forwardRef)(
3671
+ var import_react28 = __toESM(require("react"), 1);
3672
+ var import_jsx_runtime19 = require("react/jsx-runtime");
3673
+ var InteractiveBookInner = (0, import_react28.forwardRef)(
3508
3674
  function InteractiveBookInner2(props, ref) {
3509
3675
  const { blockId, pages, index, setIndex, persistEnabled } = props;
3510
3676
  validateCompoundChildren("InteractiveBook", pages);
@@ -3519,11 +3685,11 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3519
3685
  persistEnabled,
3520
3686
  ref
3521
3687
  });
3522
- const pageTitles = (0, import_react27.useMemo)(
3688
+ const pageTitles = (0, import_react28.useMemo)(
3523
3689
  () => pages.map((page) => page.props.title),
3524
3690
  [pages]
3525
3691
  );
3526
- (0, import_react27.useEffect)(() => {
3692
+ (0, import_react28.useEffect)(() => {
3527
3693
  if (!lessonId || pages.length === 0) return;
3528
3694
  track(
3529
3695
  "book_page_viewed",
@@ -3535,31 +3701,31 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3535
3701
  { lessonId }
3536
3702
  );
3537
3703
  }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
3538
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
3539
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }),
3540
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { children: [
3704
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
3705
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }),
3706
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { children: [
3541
3707
  "Page ",
3542
3708
  progress.current,
3543
3709
  " of ",
3544
3710
  progress.total
3545
3711
  ] }),
3546
- props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { "data-testid": "book-score", children: [
3712
+ props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { "data-testid": "book-score", children: [
3547
3713
  "Score: ",
3548
3714
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3549
3715
  " /",
3550
3716
  " ",
3551
3717
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3552
3718
  ] }) : null,
3553
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
3554
- (page, i) => import_react27.default.cloneElement(page, {
3719
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
3720
+ (page, i) => import_react28.default.cloneElement(page, {
3555
3721
  key: page.key ?? page.props.blockId,
3556
3722
  hidden: i !== visibleIndex,
3557
3723
  pageIndex: i,
3558
3724
  parentType: "InteractiveBook"
3559
3725
  })
3560
3726
  ) }),
3561
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("nav", { "aria-label": "Book navigation", children: [
3562
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3727
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("nav", { "aria-label": "Book navigation", children: [
3728
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3563
3729
  "button",
3564
3730
  {
3565
3731
  type: "button",
@@ -3569,7 +3735,7 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3569
3735
  children: "Previous"
3570
3736
  }
3571
3737
  ),
3572
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3738
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3573
3739
  "button",
3574
3740
  {
3575
3741
  type: "button",
@@ -3583,13 +3749,13 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3583
3749
  ] });
3584
3750
  }
3585
3751
  );
3586
- var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(props, ref) {
3587
- const blockId = (0, import_react27.useMemo)(
3752
+ var InteractiveBook = (0, import_react28.forwardRef)(function InteractiveBook2(props, ref) {
3753
+ const blockId = (0, import_react28.useMemo)(
3588
3754
  () => normalizeComponentId(props.blockId, "blockId"),
3589
3755
  [props.blockId]
3590
3756
  );
3591
- const pages = import_react27.default.Children.toArray(props.children).filter(
3592
- import_react27.default.isValidElement
3757
+ const pages = import_react28.default.Children.toArray(props.children).filter(
3758
+ import_react28.default.isValidElement
3593
3759
  );
3594
3760
  const { config, storage } = useLessonkit();
3595
3761
  const persistEnabled = config.session?.persistCompoundState !== false;
@@ -3600,12 +3766,12 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
3600
3766
  persistEnabled,
3601
3767
  storage
3602
3768
  });
3603
- const [index, setIndex] = (0, import_react27.useState)(initialIndex);
3604
- const setIndexStable = (0, import_react27.useCallback)((i) => setIndex(i), []);
3605
- (0, import_react27.useEffect)(() => {
3769
+ const [index, setIndex] = (0, import_react28.useState)(initialIndex);
3770
+ const setIndexStable = (0, import_react28.useCallback)((i) => setIndex(i), []);
3771
+ (0, import_react28.useEffect)(() => {
3606
3772
  setIndex(initialIndex);
3607
3773
  }, [config.courseId, blockId, initialIndex]);
3608
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3774
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3609
3775
  InteractiveBookInner,
3610
3776
  {
3611
3777
  ...props,
@@ -3621,13 +3787,13 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
3621
3787
  setLessonkitBlockType(InteractiveBook, "InteractiveBook");
3622
3788
 
3623
3789
  // src/blocks/Slide.tsx
3624
- var import_react28 = require("react");
3625
- var import_jsx_runtime19 = require("react/jsx-runtime");
3790
+ var import_react29 = require("react");
3791
+ var import_jsx_runtime20 = require("react/jsx-runtime");
3626
3792
  function Slide(props) {
3627
3793
  validateCompoundChildren("Slide", props.children);
3628
3794
  const { track } = useLessonkit();
3629
3795
  const lessonId = useEnclosingLessonId();
3630
- (0, import_react28.useEffect)(() => {
3796
+ (0, import_react29.useEffect)(() => {
3631
3797
  if (props.hidden || !lessonId || props.parentType) return;
3632
3798
  track(
3633
3799
  "compound_page_viewed",
@@ -3639,7 +3805,7 @@ function Slide(props) {
3639
3805
  { lessonId }
3640
3806
  );
3641
3807
  }, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
3642
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
3808
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
3643
3809
  "section",
3644
3810
  {
3645
3811
  "aria-label": props.title ?? "Slide",
@@ -3647,8 +3813,8 @@ function Slide(props) {
3647
3813
  "data-testid": `slide-${props.blockId}`,
3648
3814
  hidden: props.hidden ? true : void 0,
3649
3815
  children: [
3650
- props.title ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }) : null,
3651
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { children: props.children }) })
3816
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }) : null,
3817
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { children: props.children }) })
3652
3818
  ]
3653
3819
  }
3654
3820
  );
@@ -3656,10 +3822,10 @@ function Slide(props) {
3656
3822
  setLessonkitBlockType(Slide, "Slide");
3657
3823
 
3658
3824
  // src/blocks/SlideDeck.tsx
3659
- var import_react30 = __toESM(require("react"), 1);
3825
+ var import_react31 = __toESM(require("react"), 1);
3660
3826
 
3661
3827
  // src/compound/useCompoundKeyboardNav.ts
3662
- var import_react29 = require("react");
3828
+ var import_react30 = require("react");
3663
3829
  var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
3664
3830
  function isEditableTarget(target) {
3665
3831
  if (!(target instanceof HTMLElement)) return false;
@@ -3672,7 +3838,7 @@ function isEditableTarget(target) {
3672
3838
  }
3673
3839
  function useCompoundKeyboardNav(opts) {
3674
3840
  const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
3675
- (0, import_react29.useEffect)(() => {
3841
+ (0, import_react30.useEffect)(() => {
3676
3842
  const el = containerRef.current;
3677
3843
  if (!el || pageCount === 0) return;
3678
3844
  const onKeyDown = (event) => {
@@ -3717,13 +3883,13 @@ function useCompoundKeyboardNav(opts) {
3717
3883
  }
3718
3884
 
3719
3885
  // src/blocks/SlideDeck.tsx
3720
- var import_jsx_runtime20 = require("react/jsx-runtime");
3721
- var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(props, ref) {
3886
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3887
+ var SlideDeckInner = (0, import_react31.forwardRef)(function SlideDeckInner2(props, ref) {
3722
3888
  const { blockId, slides, index, setIndex, persistEnabled } = props;
3723
3889
  validateCompoundChildren("SlideDeck", slides);
3724
3890
  const { config, track } = useLessonkit();
3725
3891
  const lessonId = useEnclosingLessonId();
3726
- const containerRef = (0, import_react30.useRef)(null);
3892
+ const containerRef = (0, import_react31.useRef)(null);
3727
3893
  const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3728
3894
  courseId: config.courseId,
3729
3895
  compoundId: blockId,
@@ -3733,7 +3899,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3733
3899
  persistEnabled,
3734
3900
  ref
3735
3901
  });
3736
- const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), [setIndex]);
3902
+ const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), [setIndex]);
3737
3903
  useCompoundKeyboardNav({
3738
3904
  containerRef,
3739
3905
  visibleIndex,
@@ -3742,11 +3908,11 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3742
3908
  goPrev,
3743
3909
  setIndex: setIndexStable
3744
3910
  });
3745
- const slideTitles = (0, import_react30.useMemo)(
3911
+ const slideTitles = (0, import_react31.useMemo)(
3746
3912
  () => slides.map((slide) => slide.props.title),
3747
3913
  [slides]
3748
3914
  );
3749
- (0, import_react30.useEffect)(() => {
3915
+ (0, import_react31.useEffect)(() => {
3750
3916
  if (!lessonId || slides.length === 0) return;
3751
3917
  track(
3752
3918
  "slide_viewed",
@@ -3758,7 +3924,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3758
3924
  { lessonId }
3759
3925
  );
3760
3926
  }, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
3761
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
3927
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3762
3928
  "section",
3763
3929
  {
3764
3930
  ref: containerRef,
@@ -3767,30 +3933,30 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3767
3933
  "data-testid": "slide-deck",
3768
3934
  "data-lk-block-id": blockId,
3769
3935
  children: [
3770
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }),
3771
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { children: [
3936
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h3", { children: props.title }),
3937
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { children: [
3772
3938
  "Slide ",
3773
3939
  progress.current,
3774
3940
  " of ",
3775
3941
  progress.total
3776
3942
  ] }),
3777
- props.showDeckScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { "data-testid": "deck-score", children: [
3943
+ props.showDeckScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { "data-testid": "deck-score", children: [
3778
3944
  "Score: ",
3779
3945
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3780
3946
  " /",
3781
3947
  " ",
3782
3948
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3783
3949
  ] }) : null,
3784
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
3785
- (slide, i) => import_react30.default.cloneElement(slide, {
3950
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
3951
+ (slide, i) => import_react31.default.cloneElement(slide, {
3786
3952
  key: slide.key ?? slide.props.blockId,
3787
3953
  hidden: i !== visibleIndex,
3788
3954
  slideIndex: i,
3789
3955
  parentType: "SlideDeck"
3790
3956
  })
3791
3957
  ) }),
3792
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3793
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3958
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3959
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3794
3960
  "button",
3795
3961
  {
3796
3962
  type: "button",
@@ -3800,7 +3966,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3800
3966
  children: "Previous slide"
3801
3967
  }
3802
3968
  ),
3803
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3969
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3804
3970
  "button",
3805
3971
  {
3806
3972
  type: "button",
@@ -3815,13 +3981,13 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3815
3981
  }
3816
3982
  );
3817
3983
  });
3818
- var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3819
- const blockId = (0, import_react30.useMemo)(
3984
+ var SlideDeck = (0, import_react31.forwardRef)(function SlideDeck2(props, ref) {
3985
+ const blockId = (0, import_react31.useMemo)(
3820
3986
  () => normalizeComponentId(props.blockId, "blockId"),
3821
3987
  [props.blockId]
3822
3988
  );
3823
- const slides = import_react30.default.Children.toArray(props.children).filter(
3824
- import_react30.default.isValidElement
3989
+ const slides = import_react31.default.Children.toArray(props.children).filter(
3990
+ import_react31.default.isValidElement
3825
3991
  );
3826
3992
  const { config, storage } = useLessonkit();
3827
3993
  const persistEnabled = config.session?.persistCompoundState !== false;
@@ -3832,12 +3998,12 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3832
3998
  persistEnabled,
3833
3999
  storage
3834
4000
  });
3835
- const [index, setIndex] = (0, import_react30.useState)(initialIndex);
3836
- const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), []);
3837
- (0, import_react30.useEffect)(() => {
4001
+ const [index, setIndex] = (0, import_react31.useState)(initialIndex);
4002
+ const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), []);
4003
+ (0, import_react31.useEffect)(() => {
3838
4004
  setIndex(initialIndex);
3839
4005
  }, [config.courseId, blockId, initialIndex]);
3840
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4006
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3841
4007
  SlideDeckInner,
3842
4008
  {
3843
4009
  ...props,
@@ -3852,95 +4018,1698 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3852
4018
  });
3853
4019
  setLessonkitBlockType(SlideDeck, "SlideDeck");
3854
4020
 
3855
- // src/blocks/Accordion.tsx
3856
- var import_react31 = require("react");
3857
- var import_jsx_runtime21 = require("react/jsx-runtime");
3858
- function Accordion(props) {
3859
- if (isDevEnvironment4()) {
3860
- validateAccordionSections(props.sections);
3861
- }
3862
- const [open, setOpen] = (0, import_react31.useState)(/* @__PURE__ */ new Set());
3863
- const { track } = useLessonkit();
4021
+ // src/blocks/TimedCue.tsx
4022
+ var import_react32 = __toESM(require("react"), 1);
4023
+ var import_accessibility3 = require("@lessonkit/accessibility");
4024
+ var import_jsx_runtime22 = require("react/jsx-runtime");
4025
+ function TimedCue(props) {
4026
+ validateCompoundChildren("TimedCue", props.children, true);
4027
+ const child = import_react32.default.Children.only(props.children);
4028
+ const overlayRef = (0, import_react32.useRef)(null);
4029
+ (0, import_react32.useEffect)(() => {
4030
+ if (props.hidden || !overlayRef.current) return;
4031
+ const trap = (0, import_accessibility3.trapFocus)(overlayRef.current, { restoreFocus: false });
4032
+ trap.activate();
4033
+ const firstFocusable = overlayRef.current.querySelector(
4034
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
4035
+ );
4036
+ firstFocusable?.focus();
4037
+ return () => trap.deactivate();
4038
+ }, [props.hidden, props.cueIndex]);
4039
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
4040
+ "div",
4041
+ {
4042
+ ref: overlayRef,
4043
+ role: "dialog",
4044
+ "aria-modal": props.hidden ? void 0 : true,
4045
+ "aria-hidden": props.hidden ? true : void 0,
4046
+ hidden: props.hidden ? true : void 0,
4047
+ "aria-label": props.label ?? `Interaction at ${props.atSeconds} seconds`,
4048
+ "data-testid": `timed-cue-${props.cueIndex ?? 0}`,
4049
+ "data-lk-cue-at": props.atSeconds,
4050
+ className: "lk-timed-cue-overlay",
4051
+ style: {
4052
+ position: "relative",
4053
+ zIndex: 2,
4054
+ background: "var(--lk-surface, #fff)",
4055
+ padding: "1rem",
4056
+ border: "1px solid var(--lk-border, #ccc)",
4057
+ marginTop: "0.5rem"
4058
+ },
4059
+ children: [
4060
+ props.hidden ? null : props.label ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { "data-testid": "timed-cue-label", children: props.label }) : null,
4061
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CompoundPageIndexProvider, { pageIndex: props.cueIndex ?? 0, children: child })
4062
+ ]
4063
+ }
4064
+ );
4065
+ }
4066
+ setLessonkitBlockType(TimedCue, "TimedCue");
4067
+
4068
+ // src/blocks/InteractiveVideo.tsx
4069
+ var import_react33 = __toESM(require("react"), 1);
4070
+ var import_core19 = require("@lessonkit/core");
4071
+
4072
+ // src/compound/useCompoundVideoShell.ts
4073
+ var import_core18 = require("@lessonkit/core");
4074
+ var IV_META_KEY = "__lk_iv__";
4075
+ function readInteractiveVideoMeta(childStates) {
4076
+ const raw = childStates[IV_META_KEY];
4077
+ if (!raw || typeof raw !== "object") return null;
4078
+ const currentTime = typeof raw.currentTime === "number" ? raw.currentTime : 0;
4079
+ const completedCueIndices = Array.isArray(raw.completedCueIndices) ? raw.completedCueIndices.filter((n) => typeof n === "number") : [];
4080
+ return { currentTime, completedCueIndices };
4081
+ }
4082
+ function mergeVideoMetaIntoState(state, meta) {
4083
+ return {
4084
+ ...state,
4085
+ childStates: {
4086
+ ...state.childStates,
4087
+ [IV_META_KEY]: meta
4088
+ }
4089
+ };
4090
+ }
4091
+
4092
+ // src/blocks/InteractiveVideo.tsx
4093
+ var import_jsx_runtime23 = require("react/jsx-runtime");
4094
+ function loadVideoMeta(storage, courseId, blockId, enabled) {
4095
+ if (!enabled || !courseId) return { currentTime: 0, completedCueIndices: [] };
4096
+ const saved = (0, import_core19.loadCompoundState)(storage, courseId, blockId);
4097
+ if (!saved) return { currentTime: 0, completedCueIndices: [] };
4098
+ const meta = readInteractiveVideoMeta(saved.childStates);
4099
+ return meta ?? { currentTime: 0, completedCueIndices: [] };
4100
+ }
4101
+ function getCueChildCheckId(cue) {
4102
+ const child = import_react33.default.Children.only(cue.props.children);
4103
+ if (!import_react33.default.isValidElement(child)) return null;
4104
+ const props = child.props;
4105
+ if (typeof props.checkId !== "string") return null;
4106
+ return normalizeComponentId(props.checkId, "checkId");
4107
+ }
4108
+ function cueRequiresAnswer(cue) {
4109
+ return Boolean(cue.props.mustComplete && getCueChildCheckId(cue));
4110
+ }
4111
+ var InteractiveVideoInner = (0, import_react33.forwardRef)(function InteractiveVideoInner2(props, ref) {
4112
+ const { blockId, cues, index, setIndex, persistEnabled, initialMeta } = props;
4113
+ validateCompoundChildren("InteractiveVideo", cues);
4114
+ const { config, track, storage } = useLessonkit();
3864
4115
  const lessonId = useEnclosingLessonId();
3865
- const baseId = (0, import_react31.useId)();
3866
- const toggle = (sectionId) => {
3867
- setOpen((prev) => {
3868
- const next = new Set(prev);
3869
- const expanded = !next.has(sectionId);
3870
- if (expanded) next.add(sectionId);
3871
- else next.delete(sectionId);
4116
+ const videoRef = (0, import_react33.useRef)(null);
4117
+ const completedCuesRef = (0, import_react33.useRef)(new Set(initialMeta.completedCueIndices));
4118
+ const [completedCues, setCompletedCues] = (0, import_react33.useState)(
4119
+ () => new Set(initialMeta.completedCueIndices)
4120
+ );
4121
+ const [overlayActive, setOverlayActive] = (0, import_react33.useState)(false);
4122
+ const firedCuesRef = (0, import_react33.useRef)(new Set(initialMeta.completedCueIndices));
4123
+ const resumeOverlayCheckedRef = (0, import_react33.useRef)(false);
4124
+ const sortedCues = (0, import_react33.useMemo)(
4125
+ () => [...cues].sort((a, b) => (a.props.atSeconds ?? 0) - (b.props.atSeconds ?? 0)),
4126
+ [cues]
4127
+ );
4128
+ (0, import_react33.useEffect)(() => {
4129
+ completedCuesRef.current = completedCues;
4130
+ }, [completedCues]);
4131
+ const transformState = (0, import_react33.useCallback)(
4132
+ (state) => mergeVideoMetaIntoState(state, {
4133
+ currentTime: videoRef.current?.currentTime ?? initialMeta.currentTime,
4134
+ completedCueIndices: [...completedCuesRef.current]
4135
+ }),
4136
+ [initialMeta.currentTime]
4137
+ );
4138
+ const { ctx } = useCompoundShell({
4139
+ courseId: config.courseId,
4140
+ compoundId: blockId,
4141
+ pageCount: sortedCues.length,
4142
+ index,
4143
+ setIndex,
4144
+ persistEnabled,
4145
+ ref,
4146
+ storage,
4147
+ transformState
4148
+ });
4149
+ const activeCue = sortedCues[index];
4150
+ const cueCanContinue = (0, import_react33.useCallback)(
4151
+ (cue) => {
4152
+ if (!cue || !cueRequiresAnswer(cue)) return true;
4153
+ const checkId = getCueChildCheckId(cue);
4154
+ if (!checkId) return true;
4155
+ const entry = ctx?.getRegisteredHandles().get(checkId);
4156
+ if (!entry) return false;
4157
+ return entry.handle.getAnswerGiven();
4158
+ },
4159
+ [ctx]
4160
+ );
4161
+ const canContinueActiveCue = cueCanContinue(activeCue);
4162
+ (0, import_react33.useEffect)(() => {
4163
+ const video = videoRef.current;
4164
+ if (!video || initialMeta.currentTime <= 0) return;
4165
+ video.currentTime = initialMeta.currentTime;
4166
+ }, [initialMeta.currentTime]);
4167
+ (0, import_react33.useEffect)(() => {
4168
+ if (resumeOverlayCheckedRef.current || sortedCues.length === 0) return;
4169
+ resumeOverlayCheckedRef.current = true;
4170
+ const hasSavedProgress = initialMeta.currentTime > 0 || initialMeta.completedCueIndices.length > 0 || persistEnabled && config.courseId && (0, import_core19.loadCompoundState)(storage, config.courseId, blockId) !== null;
4171
+ if (!hasSavedProgress) return;
4172
+ const video = videoRef.current;
4173
+ if (!video) return;
4174
+ const cue = sortedCues[index];
4175
+ if (!cue || completedCues.has(index)) return;
4176
+ setOverlayActive(true);
4177
+ video.pause();
4178
+ const at = cue.props.atSeconds ?? 0;
4179
+ if (video.currentTime < at) {
4180
+ video.currentTime = at;
4181
+ }
4182
+ }, [
4183
+ blockId,
4184
+ completedCues,
4185
+ config.courseId,
4186
+ index,
4187
+ initialMeta.completedCueIndices.length,
4188
+ initialMeta.currentTime,
4189
+ persistEnabled,
4190
+ sortedCues,
4191
+ storage
4192
+ ]);
4193
+ const mandatoryIncompleteBefore = (0, import_react33.useCallback)(
4194
+ (time) => {
4195
+ for (let i = 0; i < sortedCues.length; i++) {
4196
+ const cue = sortedCues[i];
4197
+ if ((cue.props.atSeconds ?? 0) >= time) break;
4198
+ if (cue.props.mustComplete && !completedCues.has(i)) return cue.props.atSeconds ?? 0;
4199
+ }
4200
+ return null;
4201
+ },
4202
+ [sortedCues, completedCues]
4203
+ );
4204
+ const activateCue = (0, import_react33.useCallback)(
4205
+ (i) => {
4206
+ const cue = sortedCues[i];
4207
+ if (!cue || firedCuesRef.current.has(i)) return;
4208
+ firedCuesRef.current.add(i);
4209
+ videoRef.current?.pause();
4210
+ setIndex(i);
4211
+ setOverlayActive(true);
4212
+ if (lessonId) {
4213
+ track(
4214
+ "video_cue_reached",
4215
+ { blockId, cueIndex: i, atSeconds: cue.props.atSeconds ?? 0, cueLabel: cue.props.label },
4216
+ { lessonId }
4217
+ );
4218
+ }
4219
+ },
4220
+ [blockId, lessonId, setIndex, sortedCues, track]
4221
+ );
4222
+ const onTimeUpdate = () => {
4223
+ const video = videoRef.current;
4224
+ if (!video || overlayActive) return;
4225
+ const t = video.currentTime;
4226
+ const blockSeek = mandatoryIncompleteBefore(t);
4227
+ if (blockSeek !== null && t > blockSeek + 0.5) {
4228
+ video.currentTime = blockSeek;
4229
+ return;
4230
+ }
4231
+ for (let i = 0; i < sortedCues.length; i++) {
4232
+ if (firedCuesRef.current.has(i)) continue;
4233
+ const at = sortedCues[i]?.props.atSeconds ?? 0;
4234
+ if (t >= at) {
4235
+ activateCue(i);
4236
+ break;
4237
+ }
4238
+ }
4239
+ };
4240
+ const completeCue = () => {
4241
+ const cue = sortedCues[index];
4242
+ if (!cue || !cueCanContinue(cue)) return;
4243
+ setCompletedCues((prev) => {
4244
+ const next = /* @__PURE__ */ new Set([...prev, index]);
4245
+ completedCuesRef.current = next;
4246
+ return next;
4247
+ });
4248
+ setOverlayActive(false);
4249
+ if (lessonId) {
3872
4250
  track(
3873
- "accordion_section_toggled",
3874
- { blockId: props.blockId, sectionId, expanded },
3875
- lessonId ? { lessonId } : void 0
4251
+ "video_segment_completed",
4252
+ {
4253
+ blockId,
4254
+ segmentIndex: index,
4255
+ atSeconds: cue.props.atSeconds ?? 0,
4256
+ segmentLabel: cue.props.label
4257
+ },
4258
+ { lessonId }
3876
4259
  );
3877
- return next;
4260
+ }
4261
+ videoRef.current?.play().catch(() => {
3878
4262
  });
3879
4263
  };
3880
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3881
- const expanded = open.has(section.id);
3882
- const panelId = `${baseId}-${section.id}`;
3883
- const triggerId = `${baseId}-trigger-${section.id}`;
3884
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
3885
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3886
- "button",
3887
- {
3888
- id: triggerId,
3889
- type: "button",
3890
- "aria-expanded": expanded,
3891
- "aria-controls": panelId,
3892
- "data-testid": `accordion-trigger-${section.id}`,
3893
- onClick: () => toggle(section.id),
3894
- children: section.title
3895
- }
3896
- ) }),
3897
- expanded ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3898
- ] }, section.id);
3899
- }) });
3900
- }
3901
- setLessonkitBlockType(Accordion, "Accordion");
3902
-
3903
- // src/blocks/DialogCards.tsx
3904
- var import_react32 = require("react");
3905
- var import_jsx_runtime22 = require("react/jsx-runtime");
3906
- function DialogCards(props) {
3907
- const [index, setIndex] = (0, import_react32.useState)(0);
3908
- const [flipped, setFlipped] = (0, import_react32.useState)(false);
3909
- const card = props.cards[index];
3910
- if (!card) return null;
3911
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3912
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { children: [
3913
- "Card ",
3914
- index + 1,
3915
- " of ",
3916
- props.cards.length
3917
- ] }),
3918
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3919
- "button",
4264
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-video", "data-lk-block-id": blockId, children: [
4265
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h3", { children: props.title }),
4266
+ props.showVideoScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { "data-testid": "video-score", children: [
4267
+ "Score: ",
4268
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
4269
+ " /",
4270
+ " ",
4271
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
4272
+ ] }) : null,
4273
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4274
+ "video",
3920
4275
  {
3921
- type: "button",
3922
- "data-testid": "dialog-card-flip",
3923
- "aria-pressed": flipped,
3924
- onClick: () => setFlipped((f) => !f),
3925
- style: { minHeight: "6rem", width: "100%" },
3926
- children: flipped ? card.back : card.front
4276
+ ref: videoRef,
4277
+ src: props.src,
4278
+ poster: props.poster,
4279
+ controls: true,
4280
+ "data-testid": "interactive-video-player",
4281
+ onTimeUpdate,
4282
+ onSeeking: () => {
4283
+ const video = videoRef.current;
4284
+ if (!video) return;
4285
+ const blockSeek = mandatoryIncompleteBefore(video.currentTime);
4286
+ if (blockSeek !== null && video.currentTime > blockSeek + 0.5) {
4287
+ video.currentTime = blockSeek;
4288
+ }
4289
+ },
4290
+ children: props.captions ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("track", { kind: "captions", src: props.captions, srcLang: "en", label: "Captions", default: true }) : null
3927
4291
  }
3928
- ),
3929
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("nav", { "aria-label": "Card navigation", children: [
3930
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4292
+ ) }),
4293
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "data-testid": "interactive-video-cues", children: sortedCues.map(
4294
+ (cue, i) => import_react33.default.cloneElement(cue, {
4295
+ key: cue.key ?? i,
4296
+ hidden: !overlayActive || i !== index,
4297
+ cueIndex: i,
4298
+ parentType: "InteractiveVideo"
4299
+ })
4300
+ ) }),
4301
+ overlayActive ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
4302
+ activeCue?.props.mustComplete && !canContinueActiveCue ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "status", "data-testid": "cue-must-complete-hint", children: "Complete the interaction to continue." }) : null,
4303
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3931
4304
  "button",
3932
4305
  {
3933
4306
  type: "button",
3934
- "data-testid": "dialog-prev",
3935
- disabled: index === 0,
3936
- onClick: () => {
3937
- setIndex((i) => i - 1);
3938
- setFlipped(false);
3939
- },
4307
+ "data-testid": "cue-continue",
4308
+ disabled: !canContinueActiveCue,
4309
+ "aria-disabled": !canContinueActiveCue,
4310
+ onClick: completeCue,
4311
+ children: "Continue video"
4312
+ }
4313
+ )
4314
+ ] }) : null
4315
+ ] });
4316
+ });
4317
+ var InteractiveVideo = (0, import_react33.forwardRef)(
4318
+ function InteractiveVideo2(props, ref) {
4319
+ const blockId = (0, import_react33.useMemo)(
4320
+ () => normalizeComponentId(props.blockId, "blockId"),
4321
+ [props.blockId]
4322
+ );
4323
+ const cues = import_react33.default.Children.toArray(props.children).filter(
4324
+ import_react33.default.isValidElement
4325
+ );
4326
+ const { config, storage } = useLessonkit();
4327
+ const persistEnabled = config.session?.persistCompoundState !== false;
4328
+ const initialMeta = (0, import_react33.useMemo)(
4329
+ () => loadVideoMeta(storage, config.courseId, blockId, persistEnabled),
4330
+ [storage, config.courseId, blockId, persistEnabled]
4331
+ );
4332
+ const initialIndex = useCompoundInitialIndex({
4333
+ courseId: config.courseId,
4334
+ compoundId: blockId,
4335
+ pageCount: cues.length,
4336
+ persistEnabled,
4337
+ storage
4338
+ });
4339
+ const [index, setIndex] = (0, import_react33.useState)(initialIndex);
4340
+ const setIndexStable = (0, import_react33.useCallback)((i) => setIndex(i), []);
4341
+ (0, import_react33.useEffect)(() => {
4342
+ setIndex(initialIndex);
4343
+ }, [config.courseId, blockId, initialIndex]);
4344
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4345
+ InteractiveVideoInner,
4346
+ {
4347
+ ...props,
4348
+ ref,
4349
+ blockId,
4350
+ cues,
4351
+ index,
4352
+ setIndex,
4353
+ persistEnabled,
4354
+ initialMeta
4355
+ }
4356
+ ) });
4357
+ }
4358
+ );
4359
+ setLessonkitBlockType(InteractiveVideo, "InteractiveVideo");
4360
+
4361
+ // src/blocks/Summary.tsx
4362
+ var import_react34 = require("react");
4363
+ var import_jsx_runtime24 = require("react/jsx-runtime");
4364
+ var INTERACTION6 = "summary";
4365
+ function SummaryInner(props, ref) {
4366
+ const checkId = (0, import_react34.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4367
+ const assessment = useAssessmentState(props.enclosingLessonId);
4368
+ const [selectedIndices, setSelectedIndices] = (0, import_react34.useState)([]);
4369
+ const [passed, setPassed] = (0, import_react34.useState)(false);
4370
+ const [checked, setChecked] = (0, import_react34.useState)(false);
4371
+ const completedRef = (0, import_react34.useRef)(false);
4372
+ const telemetryReplayedRef = (0, import_react34.useRef)(false);
4373
+ const correctKey = props.correct.join("\0");
4374
+ const statementsKey = props.statements.join("\0");
4375
+ const selected = selectedIndices.map((i) => props.statements[i] ?? "");
4376
+ const reset = () => {
4377
+ completedRef.current = false;
4378
+ telemetryReplayedRef.current = false;
4379
+ setSelectedIndices([]);
4380
+ setPassed(false);
4381
+ setChecked(false);
4382
+ };
4383
+ (0, import_react34.useEffect)(() => {
4384
+ reset();
4385
+ }, [checkId, correctKey, statementsKey]);
4386
+ const isCorrect = selected.length === props.correct.length && selected.every((s, i) => s === props.correct[i]);
4387
+ const maxScore = props.correct.length || 1;
4388
+ const score = isCorrect ? maxScore : 0;
4389
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
4390
+ const availableIndices = props.statements.map((_, i) => i).filter((i) => !selectedIndices.includes(i));
4391
+ const handle = (0, import_react34.useMemo)(
4392
+ () => buildAssessmentHandle({
4393
+ checkId,
4394
+ getScore: () => passed ? score : 0,
4395
+ getMaxScore: () => maxScore,
4396
+ getAnswerGiven: () => selectedIndices.length > 0,
4397
+ resetTask: reset,
4398
+ showSolutions: () => {
4399
+ },
4400
+ getXAPIData: () => ({
4401
+ checkId,
4402
+ interactionType: INTERACTION6,
4403
+ response: selected,
4404
+ correct: passedThreshold,
4405
+ score: passed ? score : 0,
4406
+ maxScore
4407
+ }),
4408
+ getCurrentState: () => ({ selectedIndices, passed, checked }),
4409
+ resume: (state) => {
4410
+ let nextIndices = [];
4411
+ if (Array.isArray(state.selectedIndices)) {
4412
+ nextIndices = [...state.selectedIndices];
4413
+ } else if (Array.isArray(state.selected)) {
4414
+ const legacy = state.selected;
4415
+ nextIndices = legacy.map((text) => props.statements.indexOf(text)).filter((i) => i >= 0);
4416
+ }
4417
+ setSelectedIndices(nextIndices);
4418
+ const nextSelected = nextIndices.map((i) => props.statements[i] ?? "");
4419
+ const nextIsCorrect = nextSelected.length === props.correct.length && nextSelected.every((s, i) => s === props.correct[i]);
4420
+ const nextScore = nextIsCorrect ? maxScore : 0;
4421
+ readBooleanStateField(state, "passed", (value) => {
4422
+ setPassed(value);
4423
+ completedRef.current = value;
4424
+ if (value) {
4425
+ if (!telemetryReplayedRef.current) {
4426
+ telemetryReplayedRef.current = true;
4427
+ assessment.answer({
4428
+ checkId,
4429
+ interactionType: INTERACTION6,
4430
+ response: nextSelected,
4431
+ correct: true
4432
+ });
4433
+ assessment.complete({
4434
+ checkId,
4435
+ interactionType: INTERACTION6,
4436
+ score: nextScore,
4437
+ maxScore,
4438
+ passingScore: props.passingScore ?? maxScore
4439
+ });
4440
+ }
4441
+ }
4442
+ });
4443
+ readBooleanStateField(state, "checked", setChecked);
4444
+ }
4445
+ }),
4446
+ [
4447
+ assessment,
4448
+ checkId,
4449
+ checked,
4450
+ maxScore,
4451
+ passed,
4452
+ passedThreshold,
4453
+ props.passingScore,
4454
+ props.statements,
4455
+ score,
4456
+ selected,
4457
+ selectedIndices.length
4458
+ ]
4459
+ );
4460
+ useAssessmentHandleRegistration(checkId, handle, ref);
4461
+ const addStatement = (statementIndex) => {
4462
+ if (passed && !props.enableRetry) return;
4463
+ setChecked(false);
4464
+ setSelectedIndices((prev) => [...prev, statementIndex]);
4465
+ };
4466
+ const removeLast = () => {
4467
+ if (passed && !props.enableRetry) return;
4468
+ setChecked(false);
4469
+ setSelectedIndices((prev) => prev.slice(0, -1));
4470
+ };
4471
+ const check = () => {
4472
+ if (selectedIndices.length === 0) return;
4473
+ setChecked(true);
4474
+ assessment.answer({
4475
+ checkId,
4476
+ interactionType: INTERACTION6,
4477
+ response: selected,
4478
+ correct: passedThreshold
4479
+ });
4480
+ if (passedThreshold && !completedRef.current) {
4481
+ completedRef.current = true;
4482
+ setPassed(true);
4483
+ assessment.complete({
4484
+ checkId,
4485
+ interactionType: INTERACTION6,
4486
+ score,
4487
+ maxScore,
4488
+ passingScore: props.passingScore ?? maxScore
4489
+ });
4490
+ }
4491
+ };
4492
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Summary", "data-lk-check-id": checkId, "data-testid": "summary", children: [
4493
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { children: "Select statements in order to build the summary." }),
4494
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("ol", { "data-testid": "summary-selected", children: selected.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { children: s }, `${i}-${selectedIndices[i]}`)) }),
4495
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { role: "group", "aria-label": "Available statements", children: availableIndices.map((statementIndex) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
4496
+ "button",
4497
+ {
4498
+ type: "button",
4499
+ "data-testid": `summary-statement-${statementIndex}`,
4500
+ disabled: passed && !props.enableRetry,
4501
+ onClick: () => addStatement(statementIndex),
4502
+ style: { display: "block", margin: "0.25rem 0" },
4503
+ children: props.statements[statementIndex]
4504
+ },
4505
+ statementIndex
4506
+ )) }),
4507
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
4508
+ "button",
4509
+ {
4510
+ type: "button",
4511
+ "data-testid": "summary-undo",
4512
+ disabled: passed && !props.enableRetry || selectedIndices.length === 0,
4513
+ onClick: removeLast,
4514
+ children: "Remove last"
4515
+ }
4516
+ ),
4517
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
4518
+ "button",
4519
+ {
4520
+ type: "button",
4521
+ "data-testid": "summary-check",
4522
+ disabled: selectedIndices.length === 0 || passed && !props.enableRetry,
4523
+ onClick: check,
4524
+ children: "Check"
4525
+ }
4526
+ ),
4527
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "summary-feedback", children: passedThreshold ? "Correct" : "Try again" }) : null,
4528
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", "data-testid": "summary-retry", onClick: reset, children: "Try again" }) : null
4529
+ ] });
4530
+ }
4531
+ var SummaryInnerForwarded = (0, import_react34.forwardRef)(SummaryInner);
4532
+ var Summary = (0, import_react34.forwardRef)(function Summary2(props, ref) {
4533
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(AssessmentLessonGuard, { blockLabel: "Summary", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SummaryInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
4534
+ });
4535
+ setLessonkitBlockType(Summary, "Summary");
4536
+
4537
+ // src/blocks/ImagePairing.tsx
4538
+ var import_react35 = require("react");
4539
+ var import_jsx_runtime25 = require("react/jsx-runtime");
4540
+ var INTERACTION7 = "imagePairing";
4541
+ function shuffleCards(cards) {
4542
+ const next = [...cards];
4543
+ for (let i = next.length - 1; i > 0; i -= 1) {
4544
+ const j = Math.floor(Math.random() * (i + 1));
4545
+ [next[i], next[j]] = [next[j], next[i]];
4546
+ }
4547
+ return next;
4548
+ }
4549
+ function buildDeck(pairs) {
4550
+ const cards = pairs.flatMap(
4551
+ (pair) => [0, 1].map((copy) => ({
4552
+ cardKey: `${pair.id}-${copy}`,
4553
+ pairId: pair.id,
4554
+ label: pair.label,
4555
+ imageSrc: pair.imageSrc
4556
+ }))
4557
+ );
4558
+ return shuffleCards(cards);
4559
+ }
4560
+ function ImagePairingInner(props, ref) {
4561
+ const checkId = (0, import_react35.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4562
+ const assessment = useAssessmentState(props.enclosingLessonId);
4563
+ const pairsKey = props.pairs.map((p) => p.id).join("\0");
4564
+ const [cards, setCards] = (0, import_react35.useState)(() => buildDeck(props.pairs));
4565
+ const [matched, setMatched] = (0, import_react35.useState)(() => /* @__PURE__ */ new Set());
4566
+ const [revealed, setRevealed] = (0, import_react35.useState)(() => /* @__PURE__ */ new Set());
4567
+ const [keyboardSelection, setKeyboardSelection] = (0, import_react35.useState)(null);
4568
+ const [passed, setPassed] = (0, import_react35.useState)(false);
4569
+ const completedRef = (0, import_react35.useRef)(false);
4570
+ const telemetryReplayedRef = (0, import_react35.useRef)(false);
4571
+ const reset = () => {
4572
+ completedRef.current = false;
4573
+ telemetryReplayedRef.current = false;
4574
+ setCards(buildDeck(props.pairs));
4575
+ setMatched(/* @__PURE__ */ new Set());
4576
+ setRevealed(/* @__PURE__ */ new Set());
4577
+ setKeyboardSelection(null);
4578
+ setPassed(false);
4579
+ };
4580
+ (0, import_react35.useEffect)(() => {
4581
+ reset();
4582
+ }, [checkId, pairsKey]);
4583
+ const totalPairs = props.pairs.length;
4584
+ const matchedCount = matched.size;
4585
+ const maxScore = totalPairs || 1;
4586
+ const score = matchedCount;
4587
+ const allMatched = totalPairs > 0 && matchedCount === totalPairs;
4588
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
4589
+ const completeIfReady = (nextMatched) => {
4590
+ if (nextMatched.size === totalPairs && totalPairs > 0 && !completedRef.current) {
4591
+ const finalScore = nextMatched.size;
4592
+ const finalPassed = meetsPassingThreshold(finalScore, maxScore, props.passingScore);
4593
+ completedRef.current = true;
4594
+ setPassed(true);
4595
+ assessment.answer({
4596
+ checkId,
4597
+ interactionType: INTERACTION7,
4598
+ response: { matchedPairIds: [...nextMatched] },
4599
+ correct: finalPassed
4600
+ });
4601
+ assessment.complete({
4602
+ checkId,
4603
+ interactionType: INTERACTION7,
4604
+ score: finalScore,
4605
+ maxScore,
4606
+ passingScore: props.passingScore ?? maxScore
4607
+ });
4608
+ }
4609
+ };
4610
+ const tryMatch = (firstKey, secondKey) => {
4611
+ if (firstKey === secondKey) return;
4612
+ const first = cards.find((c) => c.cardKey === firstKey);
4613
+ const second = cards.find((c) => c.cardKey === secondKey);
4614
+ if (!first || !second) return;
4615
+ setRevealed((prev) => /* @__PURE__ */ new Set([...prev, firstKey, secondKey]));
4616
+ if (first.pairId === second.pairId) {
4617
+ setMatched((prev) => {
4618
+ const next = /* @__PURE__ */ new Set([...prev, first.pairId]);
4619
+ completeIfReady(next);
4620
+ return next;
4621
+ });
4622
+ setRevealed(/* @__PURE__ */ new Set());
4623
+ setKeyboardSelection(null);
4624
+ } else {
4625
+ window.setTimeout(() => {
4626
+ setRevealed((prev) => {
4627
+ const next = new Set(prev);
4628
+ next.delete(firstKey);
4629
+ next.delete(secondKey);
4630
+ return next;
4631
+ });
4632
+ setKeyboardSelection(null);
4633
+ }, 800);
4634
+ }
4635
+ };
4636
+ const selectCard = (cardKey) => {
4637
+ if (passed && !props.enableRetry) return;
4638
+ if (matched.has(cards.find((c) => c.cardKey === cardKey)?.pairId ?? "")) return;
4639
+ if (keyboardSelection === null) {
4640
+ setKeyboardSelection(cardKey);
4641
+ setRevealed((prev) => /* @__PURE__ */ new Set([...prev, cardKey]));
4642
+ return;
4643
+ }
4644
+ if (keyboardSelection === cardKey) {
4645
+ setKeyboardSelection(null);
4646
+ setRevealed((prev) => {
4647
+ const next = new Set(prev);
4648
+ next.delete(cardKey);
4649
+ return next;
4650
+ });
4651
+ return;
4652
+ }
4653
+ tryMatch(keyboardSelection, cardKey);
4654
+ };
4655
+ const handle = (0, import_react35.useMemo)(
4656
+ () => buildAssessmentHandle({
4657
+ checkId,
4658
+ getScore: () => score,
4659
+ getMaxScore: () => maxScore,
4660
+ getAnswerGiven: () => matchedCount > 0,
4661
+ resetTask: reset,
4662
+ showSolutions: () => {
4663
+ },
4664
+ getXAPIData: () => ({
4665
+ checkId,
4666
+ interactionType: INTERACTION7,
4667
+ response: { matchedPairIds: [...matched] },
4668
+ correct: allMatched && passedThreshold,
4669
+ score,
4670
+ maxScore
4671
+ }),
4672
+ getCurrentState: () => ({
4673
+ matched: [...matched],
4674
+ revealed: [...revealed],
4675
+ keyboardSelection,
4676
+ passed
4677
+ }),
4678
+ resume: (state) => {
4679
+ if (Array.isArray(state.matched)) setMatched(new Set(state.matched));
4680
+ if (Array.isArray(state.revealed)) setRevealed(new Set(state.revealed));
4681
+ const sel = state.keyboardSelection;
4682
+ if (sel === null || typeof sel === "string") setKeyboardSelection(sel ?? null);
4683
+ readBooleanStateField(state, "passed", (value) => {
4684
+ setPassed(value);
4685
+ completedRef.current = value;
4686
+ if (value && !telemetryReplayedRef.current) {
4687
+ telemetryReplayedRef.current = true;
4688
+ const matchedIds = Array.isArray(state.matched) ? state.matched : [...matched];
4689
+ const finalScore = matchedIds.length;
4690
+ assessment.answer({
4691
+ checkId,
4692
+ interactionType: INTERACTION7,
4693
+ response: { matchedPairIds: matchedIds },
4694
+ correct: true
4695
+ });
4696
+ assessment.complete({
4697
+ checkId,
4698
+ interactionType: INTERACTION7,
4699
+ score: finalScore,
4700
+ maxScore,
4701
+ passingScore: props.passingScore ?? maxScore
4702
+ });
4703
+ }
4704
+ });
4705
+ }
4706
+ }),
4707
+ [allMatched, checkId, keyboardSelection, matched, matchedCount, maxScore, passed, passedThreshold, revealed, score]
4708
+ );
4709
+ useAssessmentHandleRegistration(checkId, handle, ref);
4710
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image Pairing", "data-lk-check-id": checkId, "data-testid": "image-pairing", children: [
4711
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: "Match the image pairs (select two cards with keyboard or click)." }),
4712
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { role: "list", "aria-label": "Image cards", "data-testid": "image-pairing-grid", children: cards.map((card) => {
4713
+ const isMatched = matched.has(card.pairId);
4714
+ const isRevealed = isMatched || revealed.has(card.cardKey);
4715
+ const isSelected = keyboardSelection === card.cardKey;
4716
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
4717
+ "button",
4718
+ {
4719
+ type: "button",
4720
+ role: "listitem",
4721
+ "data-testid": `pairing-card-${card.cardKey}`,
4722
+ "aria-pressed": isSelected,
4723
+ disabled: isMatched || passed && !props.enableRetry,
4724
+ onClick: () => selectCard(card.cardKey),
4725
+ style: {
4726
+ margin: "0.25rem",
4727
+ minWidth: "6rem",
4728
+ minHeight: "6rem",
4729
+ border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
4730
+ },
4731
+ children: isRevealed ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
4732
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: card.imageSrc, alt: card.label, style: { maxWidth: "5rem", maxHeight: "5rem" } }),
4733
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "lk-visually-hidden", children: card.label })
4734
+ ] }) : "?"
4735
+ },
4736
+ card.cardKey
4737
+ );
4738
+ }) }),
4739
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("p", { role: "status", "aria-live": "polite", "data-testid": "image-pairing-progress", children: [
4740
+ matchedCount,
4741
+ " / ",
4742
+ totalPairs,
4743
+ " pairs matched"
4744
+ ] }),
4745
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("button", { type: "button", "data-testid": "image-pairing-retry", onClick: reset, children: "Try again" }) : null
4746
+ ] });
4747
+ }
4748
+ var ImagePairingInnerForwarded = (0, import_react35.forwardRef)(ImagePairingInner);
4749
+ var ImagePairing = (0, import_react35.forwardRef)(function ImagePairing2(props, ref) {
4750
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(AssessmentLessonGuard, { blockLabel: "ImagePairing", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ImagePairingInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
4751
+ });
4752
+ setLessonkitBlockType(ImagePairing, "ImagePairing");
4753
+
4754
+ // src/blocks/ImageSequencing.tsx
4755
+ var import_react36 = require("react");
4756
+ var import_jsx_runtime26 = require("react/jsx-runtime");
4757
+ var INTERACTION8 = "imageSequencing";
4758
+ function ImageSequencingInner(props, ref) {
4759
+ const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4760
+ const assessment = useAssessmentState(props.enclosingLessonId);
4761
+ const imagesKey = props.images.map((i) => i.id).join("\0");
4762
+ const orderKey = props.correctOrder.join("\0");
4763
+ const [order, setOrder] = (0, import_react36.useState)(() => props.images.map((i) => i.id));
4764
+ const [passed, setPassed] = (0, import_react36.useState)(false);
4765
+ const [checked, setChecked] = (0, import_react36.useState)(false);
4766
+ const completedRef = (0, import_react36.useRef)(false);
4767
+ const telemetryReplayedRef = (0, import_react36.useRef)(false);
4768
+ const reset = () => {
4769
+ completedRef.current = false;
4770
+ telemetryReplayedRef.current = false;
4771
+ setOrder(props.images.map((i) => i.id));
4772
+ setPassed(false);
4773
+ setChecked(false);
4774
+ };
4775
+ (0, import_react36.useEffect)(() => {
4776
+ reset();
4777
+ }, [checkId, imagesKey, orderKey]);
4778
+ const isCorrect = order.every((id, i) => id === props.correctOrder[i]);
4779
+ const maxScore = props.correctOrder.length || 1;
4780
+ const score = isCorrect ? maxScore : 0;
4781
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
4782
+ const move = (index, direction) => {
4783
+ if (passed && !props.enableRetry) return;
4784
+ setChecked(false);
4785
+ const nextIndex = index + direction;
4786
+ if (nextIndex < 0 || nextIndex >= order.length) return;
4787
+ setOrder((prev) => {
4788
+ const next = [...prev];
4789
+ [next[index], next[nextIndex]] = [next[nextIndex], next[index]];
4790
+ return next;
4791
+ });
4792
+ };
4793
+ const handle = (0, import_react36.useMemo)(
4794
+ () => buildAssessmentHandle({
4795
+ checkId,
4796
+ getScore: () => passed ? score : 0,
4797
+ getMaxScore: () => maxScore,
4798
+ getAnswerGiven: () => order.length > 0,
4799
+ resetTask: reset,
4800
+ showSolutions: () => {
4801
+ },
4802
+ getXAPIData: () => ({
4803
+ checkId,
4804
+ interactionType: INTERACTION8,
4805
+ response: order,
4806
+ correct: passedThreshold,
4807
+ score: passed ? score : 0,
4808
+ maxScore
4809
+ }),
4810
+ getCurrentState: () => ({ order, passed, checked }),
4811
+ resume: (state) => {
4812
+ let nextOrder = order;
4813
+ if (Array.isArray(state.order)) {
4814
+ nextOrder = [...state.order];
4815
+ setOrder(nextOrder);
4816
+ }
4817
+ readBooleanStateField(state, "passed", (value) => {
4818
+ setPassed(value);
4819
+ completedRef.current = value;
4820
+ if (value && !telemetryReplayedRef.current) {
4821
+ telemetryReplayedRef.current = true;
4822
+ const nextIsCorrect = nextOrder.every((id, i) => id === props.correctOrder[i]);
4823
+ const nextScore = nextIsCorrect ? maxScore : 0;
4824
+ assessment.answer({
4825
+ checkId,
4826
+ interactionType: INTERACTION8,
4827
+ response: nextOrder,
4828
+ correct: nextIsCorrect
4829
+ });
4830
+ assessment.complete({
4831
+ checkId,
4832
+ interactionType: INTERACTION8,
4833
+ score: nextScore,
4834
+ maxScore,
4835
+ passingScore: props.passingScore ?? maxScore
4836
+ });
4837
+ }
4838
+ });
4839
+ readBooleanStateField(state, "checked", setChecked);
4840
+ }
4841
+ }),
4842
+ [checkId, checked, maxScore, order, passed, passedThreshold, score]
4843
+ );
4844
+ useAssessmentHandleRegistration(checkId, handle, ref);
4845
+ const check = () => {
4846
+ setChecked(true);
4847
+ assessment.answer({
4848
+ checkId,
4849
+ interactionType: INTERACTION8,
4850
+ response: order,
4851
+ correct: passedThreshold
4852
+ });
4853
+ if (passedThreshold && !completedRef.current) {
4854
+ completedRef.current = true;
4855
+ setPassed(true);
4856
+ assessment.complete({
4857
+ checkId,
4858
+ interactionType: INTERACTION8,
4859
+ score,
4860
+ maxScore,
4861
+ passingScore: props.passingScore ?? maxScore
4862
+ });
4863
+ }
4864
+ };
4865
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Image Sequencing", "data-lk-check-id": checkId, "data-testid": "image-sequencing", children: [
4866
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { children: "Reorder the images into the correct sequence." }),
4867
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ol", { "data-testid": "image-sequencing-list", children: order.map((id, index) => {
4868
+ const image = props.images.find((i) => i.id === id);
4869
+ if (!image) return null;
4870
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { "data-testid": `sequencing-item-${id}`, children: [
4871
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: image.src, alt: image.alt, style: { maxWidth: "8rem", verticalAlign: "middle" } }),
4872
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
4873
+ "button",
4874
+ {
4875
+ type: "button",
4876
+ "data-testid": `sequencing-up-${id}`,
4877
+ "aria-label": `Move ${image.alt} up`,
4878
+ disabled: index === 0 || passed && !props.enableRetry,
4879
+ onClick: () => move(index, -1),
4880
+ children: "Up"
4881
+ }
4882
+ ),
4883
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
4884
+ "button",
4885
+ {
4886
+ type: "button",
4887
+ "data-testid": `sequencing-down-${id}`,
4888
+ "aria-label": `Move ${image.alt} down`,
4889
+ disabled: index >= order.length - 1 || passed && !props.enableRetry,
4890
+ onClick: () => move(index, 1),
4891
+ children: "Down"
4892
+ }
4893
+ )
4894
+ ] }, id);
4895
+ }) }),
4896
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
4897
+ "button",
4898
+ {
4899
+ type: "button",
4900
+ "data-testid": "image-sequencing-check",
4901
+ disabled: passed && !props.enableRetry,
4902
+ onClick: check,
4903
+ children: "Check"
4904
+ }
4905
+ ),
4906
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "image-sequencing-feedback", children: passedThreshold ? "Correct" : "Try again" }) : null,
4907
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "image-sequencing-retry", onClick: reset, children: "Try again" }) : null
4908
+ ] });
4909
+ }
4910
+ var ImageSequencingInnerForwarded = (0, import_react36.forwardRef)(ImageSequencingInner);
4911
+ var ImageSequencing = (0, import_react36.forwardRef)(
4912
+ function ImageSequencing2(props, ref) {
4913
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AssessmentLessonGuard, { blockLabel: "ImageSequencing", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ImageSequencingInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
4914
+ }
4915
+ );
4916
+ setLessonkitBlockType(ImageSequencing, "ImageSequencing");
4917
+
4918
+ // src/blocks/ArithmeticQuiz.tsx
4919
+ var import_react37 = require("react");
4920
+ var import_jsx_runtime27 = require("react/jsx-runtime");
4921
+ var INTERACTION9 = "arithmeticQuiz";
4922
+ function ArithmeticQuizInner(props, ref) {
4923
+ const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4924
+ const assessment = useAssessmentState(props.enclosingLessonId);
4925
+ const problemsKey = props.problems.map((p) => `${p.question}\0${p.answer}`).join("|");
4926
+ const [answers, setAnswers] = (0, import_react37.useState)(
4927
+ () => Object.fromEntries(props.problems.map((_, i) => [i, ""]))
4928
+ );
4929
+ const [passed, setPassed] = (0, import_react37.useState)(false);
4930
+ const [checked, setChecked] = (0, import_react37.useState)(false);
4931
+ const [timeLeft, setTimeLeft] = (0, import_react37.useState)(
4932
+ props.timeLimitSeconds ?? null
4933
+ );
4934
+ const completedRef = (0, import_react37.useRef)(false);
4935
+ const telemetryReplayedRef = (0, import_react37.useRef)(false);
4936
+ const reset = () => {
4937
+ completedRef.current = false;
4938
+ telemetryReplayedRef.current = false;
4939
+ setAnswers(Object.fromEntries(props.problems.map((_, i) => [i, ""])));
4940
+ setPassed(false);
4941
+ setChecked(false);
4942
+ setTimeLeft(props.timeLimitSeconds ?? null);
4943
+ };
4944
+ (0, import_react37.useEffect)(() => {
4945
+ reset();
4946
+ }, [checkId, problemsKey, props.timeLimitSeconds]);
4947
+ let score = 0;
4948
+ props.problems.forEach((p, i) => {
4949
+ if ((answers[i] ?? "").trim() === p.answer.trim()) score += 1;
4950
+ });
4951
+ const maxScore = props.problems.length || 1;
4952
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
4953
+ const allFilled = props.problems.every((_, i) => (answers[i] ?? "").trim().length > 0);
4954
+ const runCheck = (0, import_react37.useCallback)(
4955
+ (force = false) => {
4956
+ if (!force && !allFilled) return;
4957
+ setChecked(true);
4958
+ assessment.answer({
4959
+ checkId,
4960
+ interactionType: INTERACTION9,
4961
+ response: answers,
4962
+ correct: passedThreshold
4963
+ });
4964
+ if (passedThreshold && !completedRef.current) {
4965
+ completedRef.current = true;
4966
+ setPassed(true);
4967
+ assessment.complete({
4968
+ checkId,
4969
+ interactionType: INTERACTION9,
4970
+ score,
4971
+ maxScore,
4972
+ passingScore: props.passingScore ?? maxScore
4973
+ });
4974
+ }
4975
+ },
4976
+ [allFilled, answers, assessment, checkId, maxScore, passedThreshold, props.passingScore, score]
4977
+ );
4978
+ (0, import_react37.useEffect)(() => {
4979
+ if (timeLeft === null || passed || checked) return;
4980
+ if (timeLeft <= 0) {
4981
+ runCheck(true);
4982
+ return;
4983
+ }
4984
+ const id = window.setTimeout(() => setTimeLeft((t) => t !== null ? t - 1 : t), 1e3);
4985
+ return () => window.clearTimeout(id);
4986
+ }, [checked, passed, runCheck, timeLeft]);
4987
+ const handle = (0, import_react37.useMemo)(
4988
+ () => buildAssessmentHandle({
4989
+ checkId,
4990
+ getScore: () => passed ? score : 0,
4991
+ getMaxScore: () => maxScore,
4992
+ getAnswerGiven: () => allFilled,
4993
+ resetTask: reset,
4994
+ showSolutions: () => {
4995
+ },
4996
+ getXAPIData: () => ({
4997
+ checkId,
4998
+ interactionType: INTERACTION9,
4999
+ response: answers,
5000
+ correct: passedThreshold,
5001
+ score: passed ? score : 0,
5002
+ maxScore
5003
+ }),
5004
+ getCurrentState: () => ({ answers, passed, checked, timeLeft }),
5005
+ resume: (state) => {
5006
+ const raw = state.answers;
5007
+ let nextAnswers = answers;
5008
+ if (raw && typeof raw === "object") {
5009
+ nextAnswers = { ...raw };
5010
+ setAnswers(nextAnswers);
5011
+ }
5012
+ readBooleanStateField(state, "passed", (value) => {
5013
+ setPassed(value);
5014
+ completedRef.current = value;
5015
+ if (value && !telemetryReplayedRef.current) {
5016
+ telemetryReplayedRef.current = true;
5017
+ let nextScore = 0;
5018
+ props.problems.forEach((p, i) => {
5019
+ if ((nextAnswers[i] ?? "").trim() === p.answer.trim()) nextScore += 1;
5020
+ });
5021
+ assessment.answer({
5022
+ checkId,
5023
+ interactionType: INTERACTION9,
5024
+ response: nextAnswers,
5025
+ correct: true
5026
+ });
5027
+ assessment.complete({
5028
+ checkId,
5029
+ interactionType: INTERACTION9,
5030
+ score: nextScore,
5031
+ maxScore,
5032
+ passingScore: props.passingScore ?? maxScore
5033
+ });
5034
+ }
5035
+ });
5036
+ readBooleanStateField(state, "checked", setChecked);
5037
+ if (typeof state.timeLeft === "number") setTimeLeft(state.timeLeft);
5038
+ }
5039
+ }),
5040
+ [allFilled, answers, checkId, checked, maxScore, passed, passedThreshold, score, timeLeft]
5041
+ );
5042
+ useAssessmentHandleRegistration(checkId, handle, ref);
5043
+ const onInput = (index, value) => {
5044
+ if (passed && !props.enableRetry) return;
5045
+ setChecked(false);
5046
+ setAnswers((prev) => ({ ...prev, [index]: value }));
5047
+ };
5048
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Arithmetic Quiz", "data-lk-check-id": checkId, "data-testid": "arithmetic-quiz", children: [
5049
+ props.timeLimitSeconds ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { "data-testid": "arithmetic-timer", role: "timer", "aria-live": "polite", children: [
5050
+ "Time left: ",
5051
+ timeLeft ?? 0,
5052
+ "s"
5053
+ ] }) : null,
5054
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ol", { "data-testid": "arithmetic-problems", children: props.problems.map((problem, index) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { children: [
5055
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("label", { htmlFor: `${checkId}-problem-${index}`, children: problem.question }),
5056
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
5057
+ "input",
5058
+ {
5059
+ id: `${checkId}-problem-${index}`,
5060
+ type: "text",
5061
+ inputMode: "numeric",
5062
+ "data-testid": `arithmetic-answer-${index}`,
5063
+ value: answers[index] ?? "",
5064
+ disabled: passed && !props.enableRetry,
5065
+ onChange: (e) => onInput(index, e.target.value)
5066
+ }
5067
+ )
5068
+ ] }, index)) }),
5069
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
5070
+ "button",
5071
+ {
5072
+ type: "button",
5073
+ "data-testid": "arithmetic-check",
5074
+ disabled: !allFilled && timeLeft !== 0 || passed && !props.enableRetry,
5075
+ onClick: () => runCheck(),
5076
+ children: "Check"
5077
+ }
5078
+ ),
5079
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { role: "status", "aria-live": "polite", "data-testid": "arithmetic-feedback", children: [
5080
+ passedThreshold ? "Correct" : "Try again",
5081
+ " (",
5082
+ score,
5083
+ "/",
5084
+ maxScore,
5085
+ ")"
5086
+ ] }) : null,
5087
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "arithmetic-retry", onClick: reset, children: "Try again" }) : null
5088
+ ] });
5089
+ }
5090
+ var ArithmeticQuizInnerForwarded = (0, import_react37.forwardRef)(ArithmeticQuizInner);
5091
+ var ArithmeticQuiz = (0, import_react37.forwardRef)(
5092
+ function ArithmeticQuiz2(props, ref) {
5093
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AssessmentLessonGuard, { blockLabel: "ArithmeticQuiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ArithmeticQuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
5094
+ }
5095
+ );
5096
+ setLessonkitBlockType(ArithmeticQuiz, "ArithmeticQuiz");
5097
+
5098
+ // src/blocks/Essay.tsx
5099
+ var import_react38 = __toESM(require("react"), 1);
5100
+ var import_jsx_runtime28 = require("react/jsx-runtime");
5101
+ var INTERACTION10 = "essay";
5102
+ function EssayInner(props, ref) {
5103
+ const checkId = (0, import_react38.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
5104
+ const assessment = useAssessmentState(props.enclosingLessonId);
5105
+ const [text, setText] = (0, import_react38.useState)("");
5106
+ const [submitted, setSubmitted] = (0, import_react38.useState)(false);
5107
+ const completedRef = (0, import_react38.useRef)(false);
5108
+ const telemetryReplayedRef = (0, import_react38.useRef)(false);
5109
+ const questionId = import_react38.default.useId();
5110
+ const minLength = props.minLength ?? 0;
5111
+ const meetsMinLength = text.trim().length >= minLength;
5112
+ const reset = () => {
5113
+ completedRef.current = false;
5114
+ telemetryReplayedRef.current = false;
5115
+ setText("");
5116
+ setSubmitted(false);
5117
+ };
5118
+ (0, import_react38.useEffect)(() => {
5119
+ reset();
5120
+ }, [checkId, props.question, props.minLength]);
5121
+ const handle = (0, import_react38.useMemo)(
5122
+ () => buildAssessmentHandle({
5123
+ checkId,
5124
+ getScore: () => 0,
5125
+ getMaxScore: () => 1,
5126
+ getAnswerGiven: () => submitted && meetsMinLength,
5127
+ resetTask: reset,
5128
+ showSolutions: () => {
5129
+ },
5130
+ getXAPIData: () => ({
5131
+ checkId,
5132
+ interactionType: INTERACTION10,
5133
+ question: props.question,
5134
+ response: text,
5135
+ score: 0,
5136
+ maxScore: 1
5137
+ }),
5138
+ getCurrentState: () => ({ text, submitted }),
5139
+ resume: (state) => {
5140
+ const nextText = readStringField(state, "text");
5141
+ if (typeof nextText === "string") setText(nextText);
5142
+ readBooleanStateField(state, "submitted", (value) => {
5143
+ setSubmitted(value);
5144
+ completedRef.current = value;
5145
+ if (value && !telemetryReplayedRef.current) {
5146
+ telemetryReplayedRef.current = true;
5147
+ const response = typeof nextText === "string" ? nextText : text;
5148
+ assessment.answer({
5149
+ checkId,
5150
+ interactionType: INTERACTION10,
5151
+ question: props.question,
5152
+ response,
5153
+ correct: false
5154
+ });
5155
+ assessment.complete({
5156
+ checkId,
5157
+ interactionType: INTERACTION10,
5158
+ score: 0,
5159
+ maxScore: 1,
5160
+ passingScore: props.passingScore ?? 1
5161
+ });
5162
+ }
5163
+ });
5164
+ }
5165
+ }),
5166
+ [checkId, meetsMinLength, props.question, submitted, text]
5167
+ );
5168
+ useAssessmentHandleRegistration(checkId, handle, ref);
5169
+ const submit = () => {
5170
+ if (!meetsMinLength || submitted && !props.enableRetry) return;
5171
+ setSubmitted(true);
5172
+ if (!completedRef.current) {
5173
+ completedRef.current = true;
5174
+ assessment.answer({
5175
+ checkId,
5176
+ interactionType: INTERACTION10,
5177
+ question: props.question,
5178
+ response: text,
5179
+ correct: false
5180
+ });
5181
+ assessment.complete({
5182
+ checkId,
5183
+ interactionType: INTERACTION10,
5184
+ score: 0,
5185
+ maxScore: 1,
5186
+ passingScore: props.passingScore ?? 1
5187
+ });
5188
+ }
5189
+ };
5190
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("section", { "aria-label": "Essay", "data-lk-check-id": checkId, "data-testid": "essay", children: [
5191
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { id: questionId, children: props.question }),
5192
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5193
+ "textarea",
5194
+ {
5195
+ "aria-labelledby": questionId,
5196
+ "data-testid": "essay-textarea",
5197
+ value: text,
5198
+ disabled: submitted && !props.enableRetry,
5199
+ onChange: (e) => {
5200
+ if (submitted && !props.enableRetry) return;
5201
+ setSubmitted(false);
5202
+ completedRef.current = false;
5203
+ setText(e.target.value);
5204
+ },
5205
+ rows: 6,
5206
+ style: { width: "100%" }
5207
+ }
5208
+ ),
5209
+ minLength > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { "data-testid": "essay-min-length", children: [
5210
+ "Minimum length: ",
5211
+ minLength,
5212
+ " characters (",
5213
+ text.trim().length,
5214
+ "/",
5215
+ minLength,
5216
+ ")"
5217
+ ] }) : null,
5218
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5219
+ "button",
5220
+ {
5221
+ type: "button",
5222
+ "data-testid": "essay-submit",
5223
+ disabled: !meetsMinLength || submitted && !props.enableRetry,
5224
+ onClick: submit,
5225
+ children: "Submit"
5226
+ }
5227
+ ),
5228
+ submitted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "essay-submitted", children: "Response submitted for review." }) : null,
5229
+ props.enableRetry && submitted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("button", { type: "button", "data-testid": "essay-retry", onClick: reset, children: "Try again" }) : null
5230
+ ] });
5231
+ }
5232
+ var EssayInnerForwarded = (0, import_react38.forwardRef)(EssayInner);
5233
+ var Essay = (0, import_react38.forwardRef)(function Essay2(props, ref) {
5234
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(AssessmentLessonGuard, { blockLabel: "Essay", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(EssayInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
5235
+ });
5236
+ setLessonkitBlockType(Essay, "Essay");
5237
+
5238
+ // src/blocks/Questionnaire.tsx
5239
+ var import_react39 = require("react");
5240
+ var import_jsx_runtime29 = require("react/jsx-runtime");
5241
+ function Questionnaire(props) {
5242
+ const blockId = (0, import_react39.useMemo)(
5243
+ () => normalizeComponentId(props.blockId, "blockId"),
5244
+ [props.blockId]
5245
+ );
5246
+ const fieldsKey = props.fields.map((f) => `${f.id}:${f.type}:${f.label}`).join("|");
5247
+ const [values, setValues] = (0, import_react39.useState)(
5248
+ () => Object.fromEntries(props.fields.map((f) => [f.id, ""]))
5249
+ );
5250
+ const [submitted, setSubmitted] = (0, import_react39.useState)(false);
5251
+ const { track } = useLessonkit();
5252
+ const lessonId = useEnclosingLessonId();
5253
+ const baseId = (0, import_react39.useId)();
5254
+ (0, import_react39.useEffect)(() => {
5255
+ setValues(Object.fromEntries(props.fields.map((f) => [f.id, ""])));
5256
+ setSubmitted(false);
5257
+ }, [blockId, fieldsKey, props.fields]);
5258
+ const submit = () => {
5259
+ if (submitted) return;
5260
+ setSubmitted(true);
5261
+ if (lessonId) {
5262
+ track(
5263
+ "questionnaire_submitted",
5264
+ { blockId, fieldCount: props.fields.length },
5265
+ { lessonId }
5266
+ );
5267
+ }
5268
+ };
5269
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { "aria-label": "Questionnaire", "data-lk-block-id": blockId, "data-testid": "questionnaire", children: [
5270
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
5271
+ "form",
5272
+ {
5273
+ onSubmit: (e) => {
5274
+ e.preventDefault();
5275
+ submit();
5276
+ },
5277
+ children: [
5278
+ props.fields.map((field) => {
5279
+ const fieldId = `${baseId}-${field.id}`;
5280
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { "data-testid": `questionnaire-field-${field.id}`, children: [
5281
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: fieldId, children: field.label }),
5282
+ field.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
5283
+ "textarea",
5284
+ {
5285
+ id: fieldId,
5286
+ "data-testid": `questionnaire-input-${field.id}`,
5287
+ value: values[field.id] ?? "",
5288
+ disabled: submitted,
5289
+ rows: 4,
5290
+ style: { display: "block", width: "100%" },
5291
+ onChange: (e) => setValues((prev) => ({ ...prev, [field.id]: e.target.value }))
5292
+ }
5293
+ ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
5294
+ "input",
5295
+ {
5296
+ id: fieldId,
5297
+ type: "text",
5298
+ "data-testid": `questionnaire-input-${field.id}`,
5299
+ value: values[field.id] ?? "",
5300
+ disabled: submitted,
5301
+ style: { display: "block", width: "100%" },
5302
+ onChange: (e) => setValues((prev) => ({ ...prev, [field.id]: e.target.value }))
5303
+ }
5304
+ )
5305
+ ] }, field.id);
5306
+ }),
5307
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("button", { type: "submit", "data-testid": "questionnaire-submit", disabled: submitted, children: "Submit" })
5308
+ ]
5309
+ }
5310
+ ),
5311
+ submitted ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "questionnaire-submitted", children: "Thank you for your responses." }) : null
5312
+ ] });
5313
+ }
5314
+ setLessonkitBlockType(Questionnaire, "Questionnaire");
5315
+
5316
+ // src/blocks/MemoryGame.tsx
5317
+ var import_react40 = require("react");
5318
+ var import_jsx_runtime30 = require("react/jsx-runtime");
5319
+ function shuffleCards2(cards) {
5320
+ const next = [...cards];
5321
+ for (let i = next.length - 1; i > 0; i -= 1) {
5322
+ const j = Math.floor(Math.random() * (i + 1));
5323
+ [next[i], next[j]] = [next[j], next[i]];
5324
+ }
5325
+ return next;
5326
+ }
5327
+ function buildDeck2(pairs) {
5328
+ const cards = pairs.flatMap(
5329
+ (pair) => [0, 1].map((copy) => ({
5330
+ cardKey: `${pair.id}-${copy}`,
5331
+ pairId: pair.id,
5332
+ label: pair.label
5333
+ }))
5334
+ );
5335
+ return shuffleCards2(cards);
5336
+ }
5337
+ function MemoryGame(props) {
5338
+ const pairsKey = props.pairs.map((p) => p.id).join("\0");
5339
+ const [cards, setCards] = (0, import_react40.useState)(() => buildDeck2(props.pairs));
5340
+ const [matched, setMatched] = (0, import_react40.useState)(() => /* @__PURE__ */ new Set());
5341
+ const [revealed, setRevealed] = (0, import_react40.useState)(() => /* @__PURE__ */ new Set());
5342
+ const [selection, setSelection] = (0, import_react40.useState)(null);
5343
+ const [complete, setComplete] = (0, import_react40.useState)(false);
5344
+ const { track } = useLessonkit();
5345
+ const lessonId = useEnclosingLessonId();
5346
+ const trackOpts = lessonId ? { lessonId } : void 0;
5347
+ (0, import_react40.useEffect)(() => {
5348
+ setCards(buildDeck2(props.pairs));
5349
+ setMatched(/* @__PURE__ */ new Set());
5350
+ setRevealed(/* @__PURE__ */ new Set());
5351
+ setSelection(null);
5352
+ setComplete(false);
5353
+ }, [props.blockId, pairsKey]);
5354
+ const cardIndexByKey = (0, import_react40.useMemo)(
5355
+ () => Object.fromEntries(cards.map((c, i) => [c.cardKey, i])),
5356
+ [cards]
5357
+ );
5358
+ const flipCard = (cardKey, face) => {
5359
+ const cardIndex = cardIndexByKey[cardKey];
5360
+ if (typeof cardIndex === "number") {
5361
+ track(
5362
+ "memory_card_flipped",
5363
+ { blockId: props.blockId, cardIndex, face },
5364
+ trackOpts
5365
+ );
5366
+ }
5367
+ };
5368
+ const tryMatch = (firstKey, secondKey) => {
5369
+ const first = cards.find((c) => c.cardKey === firstKey);
5370
+ const second = cards.find((c) => c.cardKey === secondKey);
5371
+ if (!first || !second) return;
5372
+ setRevealed((prev) => /* @__PURE__ */ new Set([...prev, firstKey, secondKey]));
5373
+ flipCard(secondKey, "back");
5374
+ if (first.pairId === second.pairId) {
5375
+ setMatched((prev) => {
5376
+ const next = /* @__PURE__ */ new Set([...prev, first.pairId]);
5377
+ if (next.size === props.pairs.length) setComplete(true);
5378
+ return next;
5379
+ });
5380
+ setRevealed(/* @__PURE__ */ new Set());
5381
+ setSelection(null);
5382
+ } else {
5383
+ window.setTimeout(() => {
5384
+ setRevealed((prev) => {
5385
+ const next = new Set(prev);
5386
+ next.delete(firstKey);
5387
+ next.delete(secondKey);
5388
+ return next;
5389
+ });
5390
+ flipCard(firstKey, "front");
5391
+ flipCard(secondKey, "front");
5392
+ setSelection(null);
5393
+ }, 800);
5394
+ }
5395
+ };
5396
+ const selectCard = (cardKey) => {
5397
+ if (complete) return;
5398
+ if (matched.has(cards.find((c) => c.cardKey === cardKey)?.pairId ?? "")) return;
5399
+ if (selection === null) {
5400
+ setSelection(cardKey);
5401
+ setRevealed((prev) => /* @__PURE__ */ new Set([...prev, cardKey]));
5402
+ flipCard(cardKey, "back");
5403
+ return;
5404
+ }
5405
+ if (selection === cardKey) {
5406
+ setSelection(null);
5407
+ setRevealed((prev) => {
5408
+ const next = new Set(prev);
5409
+ next.delete(cardKey);
5410
+ return next;
5411
+ });
5412
+ flipCard(cardKey, "front");
5413
+ return;
5414
+ }
5415
+ tryMatch(selection, cardKey);
5416
+ };
5417
+ const restart = () => {
5418
+ setCards(buildDeck2(props.pairs));
5419
+ setMatched(/* @__PURE__ */ new Set());
5420
+ setRevealed(/* @__PURE__ */ new Set());
5421
+ setSelection(null);
5422
+ setComplete(false);
5423
+ };
5424
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("section", { "aria-label": "Memory Game", "data-lk-block-id": props.blockId, "data-testid": "memory-game", children: [
5425
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { role: "list", "aria-label": "Memory cards", "data-testid": "memory-game-grid", children: cards.map((card) => {
5426
+ const isMatched = matched.has(card.pairId);
5427
+ const isRevealed = isMatched || revealed.has(card.cardKey);
5428
+ const isSelected = selection === card.cardKey;
5429
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
5430
+ "button",
5431
+ {
5432
+ type: "button",
5433
+ role: "listitem",
5434
+ "data-testid": `memory-card-${card.cardKey}`,
5435
+ "aria-pressed": isSelected,
5436
+ disabled: isMatched || complete,
5437
+ onClick: () => selectCard(card.cardKey),
5438
+ style: {
5439
+ margin: "0.25rem",
5440
+ minWidth: "5rem",
5441
+ minHeight: "5rem",
5442
+ border: isSelected ? "2px solid currentColor" : "1px solid currentColor"
5443
+ },
5444
+ children: isRevealed ? card.label : "?"
5445
+ },
5446
+ card.cardKey
5447
+ );
5448
+ }) }),
5449
+ complete ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { role: "status", "aria-live": "polite", "data-testid": "memory-game-complete", children: "All pairs matched!" }) : null,
5450
+ props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { "data-testid": "memory-game-self-score", children: "Self-score mode enabled" }) : null,
5451
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("button", { type: "button", "data-testid": "memory-game-restart", onClick: restart, children: "Restart" })
5452
+ ] });
5453
+ }
5454
+ setLessonkitBlockType(MemoryGame, "MemoryGame");
5455
+
5456
+ // src/blocks/InformationWall.tsx
5457
+ var import_react41 = require("react");
5458
+ var import_jsx_runtime31 = require("react/jsx-runtime");
5459
+ function InformationWall(props) {
5460
+ const blockId = (0, import_react41.useMemo)(
5461
+ () => normalizeComponentId(props.blockId, "blockId"),
5462
+ [props.blockId]
5463
+ );
5464
+ const [query, setQuery] = (0, import_react41.useState)("");
5465
+ const { track } = useLessonkit();
5466
+ const lessonId = useEnclosingLessonId();
5467
+ const trackOpts = lessonId ? { lessonId } : void 0;
5468
+ const debounceRef = (0, import_react41.useRef)(null);
5469
+ const filtered = (0, import_react41.useMemo)(() => {
5470
+ const q = query.trim().toLowerCase();
5471
+ if (!q) return props.panels;
5472
+ return props.panels.filter(
5473
+ (panel) => panel.title.toLowerCase().includes(q) || panel.body.toLowerCase().includes(q)
5474
+ );
5475
+ }, [props.panels, query]);
5476
+ (0, import_react41.useEffect)(
5477
+ () => () => {
5478
+ if (debounceRef.current) clearTimeout(debounceRef.current);
5479
+ },
5480
+ []
5481
+ );
5482
+ const onSearch = (value) => {
5483
+ setQuery(value);
5484
+ if (debounceRef.current) clearTimeout(debounceRef.current);
5485
+ debounceRef.current = setTimeout(() => {
5486
+ const q = value.trim().toLowerCase();
5487
+ const resultCount = q ? props.panels.filter(
5488
+ (panel) => panel.title.toLowerCase().includes(q) || panel.body.toLowerCase().includes(q)
5489
+ ).length : props.panels.length;
5490
+ track(
5491
+ "information_wall_search",
5492
+ { blockId, query: value, resultCount },
5493
+ trackOpts
5494
+ );
5495
+ }, 300);
5496
+ };
5497
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("section", { "aria-label": "Information Wall", "data-lk-block-id": blockId, "data-testid": "information-wall", children: [
5498
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("label", { htmlFor: `${blockId}-search`, children: "Search panels" }),
5499
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
5500
+ "input",
5501
+ {
5502
+ id: `${blockId}-search`,
5503
+ type: "search",
5504
+ "data-testid": "information-wall-search",
5505
+ value: query,
5506
+ placeholder: "Search\u2026",
5507
+ onChange: (e) => onSearch(e.target.value)
5508
+ }
5509
+ ),
5510
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("p", { "data-testid": "information-wall-result-count", children: [
5511
+ filtered.length,
5512
+ " panel",
5513
+ filtered.length === 1 ? "" : "s"
5514
+ ] }),
5515
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { "data-testid": "information-wall-panels", children: filtered.map((panel) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("li", { "data-testid": `information-panel-${panel.id}`, children: [
5516
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("h4", { children: panel.title }),
5517
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { children: panel.body })
5518
+ ] }, panel.id)) })
5519
+ ] });
5520
+ }
5521
+ setLessonkitBlockType(InformationWall, "InformationWall");
5522
+
5523
+ // src/blocks/ParallaxSlideshow.tsx
5524
+ var import_react42 = require("react");
5525
+ var import_jsx_runtime32 = require("react/jsx-runtime");
5526
+ function usePrefersReducedMotion() {
5527
+ const [reduced, setReduced] = (0, import_react42.useState)(false);
5528
+ (0, import_react42.useEffect)(() => {
5529
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
5530
+ setReduced(mq.matches);
5531
+ const onChange = (e) => setReduced(e.matches);
5532
+ mq.addEventListener("change", onChange);
5533
+ return () => mq.removeEventListener("change", onChange);
5534
+ }, []);
5535
+ return reduced;
5536
+ }
5537
+ function ParallaxSlideshow(props) {
5538
+ const [index, setIndex] = (0, import_react42.useState)(0);
5539
+ const reducedMotion = usePrefersReducedMotion();
5540
+ const { track } = useLessonkit();
5541
+ const lessonId = useEnclosingLessonId();
5542
+ const trackOpts = lessonId ? { lessonId } : void 0;
5543
+ const slide = props.slides[index];
5544
+ (0, import_react42.useEffect)(() => {
5545
+ track(
5546
+ "parallax_slide_viewed",
5547
+ { blockId: props.blockId, slideIndex: index },
5548
+ trackOpts
5549
+ );
5550
+ }, [index, props.blockId, track, trackOpts]);
5551
+ if (!slide) return null;
5552
+ const goTo = (next) => {
5553
+ if (next < 0 || next >= props.slides.length) return;
5554
+ setIndex(next);
5555
+ };
5556
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
5557
+ "section",
5558
+ {
5559
+ "aria-label": "Parallax slideshow",
5560
+ "data-lk-block-id": props.blockId,
5561
+ "data-testid": "parallax-slideshow",
5562
+ "data-reduced-motion": reducedMotion ? "true" : "false",
5563
+ children: [
5564
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
5565
+ "article",
5566
+ {
5567
+ "data-testid": `parallax-slide-${index}`,
5568
+ style: reducedMotion ? void 0 : {
5569
+ backgroundAttachment: "fixed",
5570
+ backgroundImage: slide.imageSrc ? `url(${slide.imageSrc})` : void 0,
5571
+ backgroundPosition: "center",
5572
+ backgroundSize: "cover",
5573
+ minHeight: "12rem",
5574
+ padding: "1rem"
5575
+ },
5576
+ children: [
5577
+ reducedMotion && slide.imageSrc ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
5578
+ "img",
5579
+ {
5580
+ src: slide.imageSrc,
5581
+ alt: "",
5582
+ "data-testid": "parallax-slide-image",
5583
+ style: { maxWidth: "100%" }
5584
+ }
5585
+ ) : null,
5586
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("h3", { "data-testid": "parallax-slide-title", children: slide.title }),
5587
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { "data-testid": "parallax-slide-body", children: slide.body })
5588
+ ]
5589
+ }
5590
+ ),
5591
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("nav", { "aria-label": "Slide navigation", children: [
5592
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
5593
+ "button",
5594
+ {
5595
+ type: "button",
5596
+ "data-testid": "parallax-prev",
5597
+ disabled: index === 0,
5598
+ onClick: () => goTo(index - 1),
5599
+ children: "Previous"
5600
+ }
5601
+ ),
5602
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { "data-testid": "parallax-progress", children: [
5603
+ index + 1,
5604
+ " / ",
5605
+ props.slides.length
5606
+ ] }),
5607
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
5608
+ "button",
5609
+ {
5610
+ type: "button",
5611
+ "data-testid": "parallax-next",
5612
+ disabled: index >= props.slides.length - 1,
5613
+ onClick: () => goTo(index + 1),
5614
+ children: "Next"
5615
+ }
5616
+ )
5617
+ ] })
5618
+ ]
5619
+ }
5620
+ );
5621
+ }
5622
+ setLessonkitBlockType(ParallaxSlideshow, "ParallaxSlideshow");
5623
+
5624
+ // src/blocks/Accordion.tsx
5625
+ var import_react43 = require("react");
5626
+ var import_jsx_runtime33 = require("react/jsx-runtime");
5627
+ function Accordion(props) {
5628
+ if (isDevEnvironment4()) {
5629
+ validateAccordionSections(props.sections);
5630
+ }
5631
+ const [open, setOpen] = (0, import_react43.useState)(/* @__PURE__ */ new Set());
5632
+ const { track } = useLessonkit();
5633
+ const lessonId = useEnclosingLessonId();
5634
+ const baseId = (0, import_react43.useId)();
5635
+ const toggle = (sectionId) => {
5636
+ setOpen((prev) => {
5637
+ const next = new Set(prev);
5638
+ const expanded = !next.has(sectionId);
5639
+ if (expanded) next.add(sectionId);
5640
+ else next.delete(sectionId);
5641
+ track(
5642
+ "accordion_section_toggled",
5643
+ { blockId: props.blockId, sectionId, expanded },
5644
+ lessonId ? { lessonId } : void 0
5645
+ );
5646
+ return next;
5647
+ });
5648
+ };
5649
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
5650
+ const expanded = open.has(section.id);
5651
+ const panelId = `${baseId}-${section.id}`;
5652
+ const triggerId = `${baseId}-trigger-${section.id}`;
5653
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
5654
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
5655
+ "button",
5656
+ {
5657
+ id: triggerId,
5658
+ type: "button",
5659
+ "aria-expanded": expanded,
5660
+ "aria-controls": panelId,
5661
+ "data-testid": `accordion-trigger-${section.id}`,
5662
+ onClick: () => toggle(section.id),
5663
+ children: section.title
5664
+ }
5665
+ ) }),
5666
+ expanded ? /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
5667
+ ] }, section.id);
5668
+ }) });
5669
+ }
5670
+ setLessonkitBlockType(Accordion, "Accordion");
5671
+
5672
+ // src/blocks/DialogCards.tsx
5673
+ var import_react44 = require("react");
5674
+ var import_jsx_runtime34 = require("react/jsx-runtime");
5675
+ function DialogCards(props) {
5676
+ const [index, setIndex] = (0, import_react44.useState)(0);
5677
+ const [flipped, setFlipped] = (0, import_react44.useState)(false);
5678
+ const card = props.cards[index];
5679
+ if (!card) return null;
5680
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
5681
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("p", { children: [
5682
+ "Card ",
5683
+ index + 1,
5684
+ " of ",
5685
+ props.cards.length
5686
+ ] }),
5687
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
5688
+ "button",
5689
+ {
5690
+ type: "button",
5691
+ "data-testid": "dialog-card-flip",
5692
+ "aria-pressed": flipped,
5693
+ onClick: () => setFlipped((f) => !f),
5694
+ style: { minHeight: "6rem", width: "100%" },
5695
+ children: flipped ? card.back : card.front
5696
+ }
5697
+ ),
5698
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("nav", { "aria-label": "Card navigation", children: [
5699
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
5700
+ "button",
5701
+ {
5702
+ type: "button",
5703
+ "data-testid": "dialog-prev",
5704
+ disabled: index === 0,
5705
+ onClick: () => {
5706
+ setIndex((i) => i - 1);
5707
+ setFlipped(false);
5708
+ },
3940
5709
  children: "Previous"
3941
5710
  }
3942
5711
  ),
3943
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5712
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3944
5713
  "button",
3945
5714
  {
3946
5715
  type: "button",
@@ -3959,11 +5728,11 @@ function DialogCards(props) {
3959
5728
  setLessonkitBlockType(DialogCards, "DialogCards");
3960
5729
 
3961
5730
  // src/blocks/Flashcards.tsx
3962
- var import_react33 = require("react");
3963
- var import_jsx_runtime23 = require("react/jsx-runtime");
5731
+ var import_react45 = require("react");
5732
+ var import_jsx_runtime35 = require("react/jsx-runtime");
3964
5733
  function Flashcards(props) {
3965
- const [index, setIndex] = (0, import_react33.useState)(0);
3966
- const [face, setFace] = (0, import_react33.useState)("front");
5734
+ const [index, setIndex] = (0, import_react45.useState)(0);
5735
+ const [face, setFace] = (0, import_react45.useState)("front");
3967
5736
  const { track } = useLessonkit();
3968
5737
  const lessonId = useEnclosingLessonId();
3969
5738
  const card = props.cards[index];
@@ -3977,10 +5746,10 @@ function Flashcards(props) {
3977
5746
  lessonId ? { lessonId } : void 0
3978
5747
  );
3979
5748
  };
3980
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3981
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3982
- props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3983
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5749
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
5750
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
5751
+ props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
5752
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
3984
5753
  "button",
3985
5754
  {
3986
5755
  type: "button",
@@ -3998,10 +5767,10 @@ function Flashcards(props) {
3998
5767
  setLessonkitBlockType(Flashcards, "Flashcards");
3999
5768
 
4000
5769
  // src/blocks/ImageHotspots.tsx
4001
- var import_react34 = require("react");
4002
- var import_jsx_runtime24 = require("react/jsx-runtime");
5770
+ var import_react46 = require("react");
5771
+ var import_jsx_runtime36 = require("react/jsx-runtime");
4003
5772
  function ImageHotspots(props) {
4004
- const [active, setActive] = (0, import_react34.useState)(null);
5773
+ const [active, setActive] = (0, import_react46.useState)(null);
4005
5774
  const { track } = useLessonkit();
4006
5775
  const lessonId = useEnclosingLessonId();
4007
5776
  const open = (hotspotId) => {
@@ -4012,10 +5781,10 @@ function ImageHotspots(props) {
4012
5781
  lessonId ? { lessonId } : void 0
4013
5782
  );
4014
5783
  };
4015
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
4016
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4017
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4018
- props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5784
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
5785
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
5786
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
5787
+ props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
4019
5788
  "button",
4020
5789
  {
4021
5790
  type: "button",
@@ -4034,19 +5803,19 @@ function ImageHotspots(props) {
4034
5803
  h.id
4035
5804
  ))
4036
5805
  ] }),
4037
- active ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
5806
+ active ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
4038
5807
  props.hotspots.find((h) => h.id === active)?.content,
4039
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
5808
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
4040
5809
  ] }) : null
4041
5810
  ] });
4042
5811
  }
4043
5812
  setLessonkitBlockType(ImageHotspots, "ImageHotspots");
4044
5813
 
4045
5814
  // src/blocks/ImageSlider.tsx
4046
- var import_react35 = require("react");
4047
- var import_jsx_runtime25 = require("react/jsx-runtime");
5815
+ var import_react47 = require("react");
5816
+ var import_jsx_runtime37 = require("react/jsx-runtime");
4048
5817
  function ImageSlider(props) {
4049
- const [index, setIndex] = (0, import_react35.useState)(0);
5818
+ const [index, setIndex] = (0, import_react47.useState)(0);
4050
5819
  const { track } = useLessonkit();
4051
5820
  const lessonId = useEnclosingLessonId();
4052
5821
  const slide = props.slides[index];
@@ -4059,11 +5828,11 @@ function ImageSlider(props) {
4059
5828
  lessonId ? { lessonId } : void 0
4060
5829
  );
4061
5830
  };
4062
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
4063
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
4064
- slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: slide.caption }) : null,
4065
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("nav", { "aria-label": "Slide navigation", children: [
4066
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5831
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
5832
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
5833
+ slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { children: slide.caption }) : null,
5834
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("nav", { "aria-label": "Slide navigation", children: [
5835
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
4067
5836
  "button",
4068
5837
  {
4069
5838
  type: "button",
@@ -4073,12 +5842,12 @@ function ImageSlider(props) {
4073
5842
  children: "Previous"
4074
5843
  }
4075
5844
  ),
4076
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { children: [
5845
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
4077
5846
  index + 1,
4078
5847
  " / ",
4079
5848
  props.slides.length
4080
5849
  ] }),
4081
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5850
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
4082
5851
  "button",
4083
5852
  {
4084
5853
  type: "button",
@@ -4094,17 +5863,17 @@ function ImageSlider(props) {
4094
5863
  setLessonkitBlockType(ImageSlider, "ImageSlider");
4095
5864
 
4096
5865
  // src/blocks/FindHotspot.tsx
4097
- var import_react36 = require("react");
4098
- var import_jsx_runtime26 = require("react/jsx-runtime");
4099
- var INTERACTION6 = "findHotspot";
5866
+ var import_react48 = require("react");
5867
+ var import_jsx_runtime38 = require("react/jsx-runtime");
5868
+ var INTERACTION11 = "findHotspot";
4100
5869
  function FindHotspotInner(props, ref) {
4101
- const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4102
- const [selected, setSelected] = (0, import_react36.useState)(null);
4103
- const [checked, setChecked] = (0, import_react36.useState)(false);
4104
- const telemetryReplayedRef = (0, import_react36.useRef)(false);
5870
+ const checkId = (0, import_react48.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
5871
+ const [selected, setSelected] = (0, import_react48.useState)(null);
5872
+ const [checked, setChecked] = (0, import_react48.useState)(false);
5873
+ const telemetryReplayedRef = (0, import_react48.useRef)(false);
4105
5874
  const assessment = useAssessmentState(props.enclosingLessonId);
4106
5875
  const targetIdsKey = props.targets.map((t) => t.id).join("\0");
4107
- (0, import_react36.useEffect)(() => {
5876
+ (0, import_react48.useEffect)(() => {
4108
5877
  setSelected(null);
4109
5878
  setChecked(false);
4110
5879
  telemetryReplayedRef.current = false;
@@ -4115,21 +5884,21 @@ function FindHotspotInner(props, ref) {
4115
5884
  telemetryReplayedRef.current = true;
4116
5885
  assessment.answer({
4117
5886
  checkId,
4118
- interactionType: INTERACTION6,
5887
+ interactionType: INTERACTION11,
4119
5888
  response: nextSelected,
4120
5889
  correct: nextCorrect
4121
5890
  });
4122
5891
  if (nextCorrect) {
4123
5892
  assessment.complete({
4124
5893
  checkId,
4125
- interactionType: INTERACTION6,
5894
+ interactionType: INTERACTION11,
4126
5895
  score: 1,
4127
5896
  maxScore: 1,
4128
5897
  passingScore: props.passingScore ?? 1
4129
5898
  });
4130
5899
  }
4131
5900
  };
4132
- const handle = (0, import_react36.useMemo)(
5901
+ const handle = (0, import_react48.useMemo)(
4133
5902
  () => buildAssessmentHandle({
4134
5903
  checkId,
4135
5904
  getScore: () => checked && correct ? 1 : 0,
@@ -4143,7 +5912,7 @@ function FindHotspotInner(props, ref) {
4143
5912
  showSolutions: () => setSelected(props.correctTargetId),
4144
5913
  getXAPIData: () => ({
4145
5914
  checkId,
4146
- interactionType: INTERACTION6,
5915
+ interactionType: INTERACTION11,
4147
5916
  response: selected ?? void 0,
4148
5917
  correct: checked ? correct : void 0,
4149
5918
  score: checked && correct ? 1 : 0,
@@ -4179,24 +5948,24 @@ function FindHotspotInner(props, ref) {
4179
5948
  setChecked(true);
4180
5949
  assessment.answer({
4181
5950
  checkId,
4182
- interactionType: INTERACTION6,
5951
+ interactionType: INTERACTION11,
4183
5952
  response: selected,
4184
5953
  correct
4185
5954
  });
4186
5955
  if (correct) {
4187
5956
  assessment.complete({
4188
5957
  checkId,
4189
- interactionType: INTERACTION6,
5958
+ interactionType: INTERACTION11,
4190
5959
  score: 1,
4191
5960
  maxScore: 1,
4192
5961
  passingScore: props.passingScore ?? 1
4193
5962
  });
4194
5963
  }
4195
5964
  };
4196
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
4197
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4198
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4199
- props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
5965
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
5966
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
5967
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
5968
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
4200
5969
  "button",
4201
5970
  {
4202
5971
  type: "button",
@@ -4215,24 +5984,24 @@ function FindHotspotInner(props, ref) {
4215
5984
  t.id
4216
5985
  ))
4217
5986
  ] }),
4218
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
4219
- checked ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
5987
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
5988
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4220
5989
  ] });
4221
5990
  }
4222
- var FindHotspotInnerForwarded = (0, import_react36.forwardRef)(FindHotspotInner);
4223
- var FindHotspot = (0, import_react36.forwardRef)(function FindHotspot2(props, ref) {
4224
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
5991
+ var FindHotspotInnerForwarded = (0, import_react48.forwardRef)(FindHotspotInner);
5992
+ var FindHotspot = (0, import_react48.forwardRef)(function FindHotspot2(props, ref) {
5993
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
4225
5994
  });
4226
5995
  setLessonkitBlockType(FindHotspot, "FindHotspot");
4227
5996
 
4228
5997
  // src/blocks/FindMultipleHotspots.tsx
4229
- var import_react37 = require("react");
4230
- var import_jsx_runtime27 = require("react/jsx-runtime");
4231
- var INTERACTION7 = "findMultipleHotspots";
5998
+ var import_react49 = require("react");
5999
+ var import_jsx_runtime39 = require("react/jsx-runtime");
6000
+ var INTERACTION12 = "findMultipleHotspots";
4232
6001
  function FindMultipleHotspotsInner(props, ref) {
4233
- const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4234
- const [selected, setSelected] = (0, import_react37.useState)(/* @__PURE__ */ new Set());
4235
- const [checked, setChecked] = (0, import_react37.useState)(false);
6002
+ const checkId = (0, import_react49.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
6003
+ const [selected, setSelected] = (0, import_react49.useState)(/* @__PURE__ */ new Set());
6004
+ const [checked, setChecked] = (0, import_react49.useState)(false);
4236
6005
  const assessment = useAssessmentState(props.enclosingLessonId);
4237
6006
  const toggle = (id) => {
4238
6007
  setSelected((prev) => {
@@ -4244,7 +6013,7 @@ function FindMultipleHotspotsInner(props, ref) {
4244
6013
  setChecked(false);
4245
6014
  };
4246
6015
  const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
4247
- const handle = (0, import_react37.useMemo)(
6016
+ const handle = (0, import_react49.useMemo)(
4248
6017
  () => buildAssessmentHandle({
4249
6018
  checkId,
4250
6019
  getScore: () => checked && correct ? 1 : 0,
@@ -4257,7 +6026,7 @@ function FindMultipleHotspotsInner(props, ref) {
4257
6026
  showSolutions: () => setSelected(new Set(props.correctTargetIds)),
4258
6027
  getXAPIData: () => ({
4259
6028
  checkId,
4260
- interactionType: INTERACTION7,
6029
+ interactionType: INTERACTION12,
4261
6030
  response: [...selected],
4262
6031
  correct: checked ? correct : void 0,
4263
6032
  score: checked && correct ? 1 : 0,
@@ -4278,24 +6047,24 @@ function FindMultipleHotspotsInner(props, ref) {
4278
6047
  setChecked(true);
4279
6048
  assessment.answer({
4280
6049
  checkId,
4281
- interactionType: INTERACTION7,
6050
+ interactionType: INTERACTION12,
4282
6051
  response: [...selected],
4283
6052
  correct
4284
6053
  });
4285
6054
  if (correct) {
4286
6055
  assessment.complete({
4287
6056
  checkId,
4288
- interactionType: INTERACTION7,
6057
+ interactionType: INTERACTION12,
4289
6058
  score: 1,
4290
6059
  maxScore: 1,
4291
6060
  passingScore: props.passingScore ?? 1
4292
6061
  });
4293
6062
  }
4294
6063
  };
4295
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
4296
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4297
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4298
- props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
6064
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
6065
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
6066
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
6067
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
4299
6068
  "button",
4300
6069
  {
4301
6070
  type: "button",
@@ -4314,23 +6083,23 @@ function FindMultipleHotspotsInner(props, ref) {
4314
6083
  t.id
4315
6084
  ))
4316
6085
  ] }),
4317
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
4318
- checked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
6086
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
6087
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4319
6088
  ] });
4320
6089
  }
4321
- var FindMultipleHotspotsInnerForwarded = (0, import_react37.forwardRef)(FindMultipleHotspotsInner);
4322
- var FindMultipleHotspots = (0, import_react37.forwardRef)(
6090
+ var FindMultipleHotspotsInnerForwarded = (0, import_react49.forwardRef)(FindMultipleHotspotsInner);
6091
+ var FindMultipleHotspots = (0, import_react49.forwardRef)(
4323
6092
  function FindMultipleHotspots2(props, ref) {
4324
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
6093
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
4325
6094
  }
4326
6095
  );
4327
6096
  setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
4328
6097
 
4329
6098
  // src/index.tsx
4330
- var import_core19 = require("@lessonkit/core");
6099
+ var import_core21 = require("@lessonkit/core");
4331
6100
 
4332
6101
  // src/theme/ThemeProvider.tsx
4333
- var import_react38 = __toESM(require("react"), 1);
6102
+ var import_react50 = __toESM(require("react"), 1);
4334
6103
  var import_themes = require("@lessonkit/themes");
4335
6104
 
4336
6105
  // src/theme/applyCssVariables.ts
@@ -4349,11 +6118,11 @@ function applyCssVariables(target, vars, previousKeys) {
4349
6118
  }
4350
6119
 
4351
6120
  // src/theme/ThemeProvider.tsx
4352
- var import_jsx_runtime28 = require("react/jsx-runtime");
4353
- var ThemeContext = (0, import_react38.createContext)(null);
6121
+ var import_jsx_runtime40 = require("react/jsx-runtime");
6122
+ var ThemeContext = (0, import_react50.createContext)(null);
4354
6123
  var useIsoLayoutEffect2 = (
4355
6124
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
4356
- typeof window !== "undefined" ? import_react38.useLayoutEffect : import_react38.default.useEffect
6125
+ typeof window !== "undefined" ? import_react50.useLayoutEffect : import_react50.default.useEffect
4357
6126
  );
4358
6127
  function getSystemMode() {
4359
6128
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -4372,7 +6141,7 @@ function ThemeProvider(props) {
4372
6141
  const preset = props.preset ?? "default";
4373
6142
  const mode = props.mode ?? "light";
4374
6143
  const targetKind = props.target ?? "document";
4375
- const [resolvedMode, setResolvedMode] = (0, import_react38.useState)(
6144
+ const [resolvedMode, setResolvedMode] = (0, import_react50.useState)(
4376
6145
  () => mode === "system" ? getSystemMode() : mode
4377
6146
  );
4378
6147
  useIsoLayoutEffect2(() => {
@@ -4388,20 +6157,20 @@ function ThemeProvider(props) {
4388
6157
  return () => mq.removeEventListener("change", onChange);
4389
6158
  }, [mode]);
4390
6159
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
4391
- const effectiveTheme = (0, import_react38.useMemo)(() => {
6160
+ const effectiveTheme = (0, import_react50.useMemo)(() => {
4392
6161
  const modeBase = resolveModeBase(mode, dataTheme);
4393
6162
  const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
4394
6163
  return (0, import_themes.mergeThemes)(base, props.theme ?? {});
4395
6164
  }, [preset, mode, dataTheme, props.theme]);
4396
- const hostRef = (0, import_react38.useRef)(null);
4397
- const appliedKeysRef = (0, import_react38.useRef)(/* @__PURE__ */ new Set());
6165
+ const hostRef = (0, import_react50.useRef)(null);
6166
+ const appliedKeysRef = (0, import_react50.useRef)(/* @__PURE__ */ new Set());
4398
6167
  useIsoLayoutEffect2(() => {
4399
6168
  if (targetKind === "document" && typeof document !== "undefined") {
4400
6169
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
4401
6170
  return () => document.documentElement.removeAttribute("data-lk-theme");
4402
6171
  }
4403
6172
  }, [targetKind, dataTheme]);
4404
- const inject = (0, import_react38.useCallback)(() => {
6173
+ const inject = (0, import_react50.useCallback)(() => {
4405
6174
  const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
4406
6175
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
4407
6176
  if (!el) return;
@@ -4418,7 +6187,7 @@ function ThemeProvider(props) {
4418
6187
  appliedKeysRef.current = /* @__PURE__ */ new Set();
4419
6188
  };
4420
6189
  }, [inject, targetKind]);
4421
- const value = (0, import_react38.useMemo)(
6190
+ const value = (0, import_react50.useMemo)(
4422
6191
  () => ({
4423
6192
  theme: effectiveTheme,
4424
6193
  preset,
@@ -4428,12 +6197,12 @@ function ThemeProvider(props) {
4428
6197
  [effectiveTheme, preset, mode, dataTheme]
4429
6198
  );
4430
6199
  if (targetKind === "document") {
4431
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
6200
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
4432
6201
  }
4433
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
6202
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
4434
6203
  }
4435
6204
  function useTheme() {
4436
- const ctx = (0, import_react38.useContext)(ThemeContext);
6205
+ const ctx = (0, import_react50.useContext)(ThemeContext);
4437
6206
  if (!ctx) {
4438
6207
  throw new Error("useTheme must be used within a ThemeProvider");
4439
6208
  }
@@ -4441,13 +6210,15 @@ function useTheme() {
4441
6210
  }
4442
6211
 
4443
6212
  // src/catalogV3Entries.ts
4444
- var import_core18 = require("@lessonkit/core");
6213
+ var import_core20 = require("@lessonkit/core");
4445
6214
  var COMPOUND_PARENTS = [
4446
6215
  "Lesson",
4447
6216
  "Page",
4448
6217
  "InteractiveBook",
4449
6218
  "Slide",
4450
6219
  "SlideDeck",
6220
+ "TimedCue",
6221
+ "InteractiveVideo",
4451
6222
  "AssessmentSequence"
4452
6223
  ];
4453
6224
  function extendParents(entry) {
@@ -4506,6 +6277,23 @@ var v3CompoundAndContentEntries = [
4506
6277
  theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
4507
6278
  telemetry: { emits: [] }
4508
6279
  },
6280
+ {
6281
+ type: "Video",
6282
+ category: "content",
6283
+ description: "Self-hosted video with native controls and optional captions.",
6284
+ props: [
6285
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
6286
+ { name: "src", type: "string", required: true, description: "Video URL." },
6287
+ { name: "poster", type: "string", required: false, description: "Poster image URL." },
6288
+ { name: "captions", type: "string", required: false, description: "WebVTT captions URL." },
6289
+ { name: "title", type: "string", required: false, description: "Accessible title." }
6290
+ ],
6291
+ requiredIds: ["blockId"],
6292
+ parentConstraints: [...COMPOUND_PARENTS],
6293
+ a11y: { element: "video", ariaLabel: "Video", keyboard: "Native video controls.", notes: "No autoplay with sound." },
6294
+ theming: { surface: "global-inherit", stylingNotes: "Responsive video." },
6295
+ telemetry: { emits: [] }
6296
+ },
4509
6297
  {
4510
6298
  type: "Page",
4511
6299
  category: "container",
@@ -4513,8 +6301,8 @@ var v3CompoundAndContentEntries = [
4513
6301
  h5pMachineName: "H5P.Column",
4514
6302
  h5pAlias: "Column",
4515
6303
  description: "Column layout container (H5P Column / Page).",
4516
- allowedChildTypes: [...import_core18.PAGE_ALLOWED_CHILD_TYPES],
4517
- maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Page,
6304
+ allowedChildTypes: [...import_core20.PAGE_ALLOWED_CHILD_TYPES],
6305
+ maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.Page,
4518
6306
  props: [
4519
6307
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4520
6308
  { name: "title", type: "string", required: false, description: "Page title." },
@@ -4534,8 +6322,8 @@ var v3CompoundAndContentEntries = [
4534
6322
  h5pMachineName: "H5P.InteractiveBook",
4535
6323
  h5pAlias: "Interactive Book",
4536
6324
  description: "Multi-page book with chapter navigation.",
4537
- allowedChildTypes: [...import_core18.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
4538
- maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
6325
+ allowedChildTypes: [...import_core20.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
6326
+ maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
4539
6327
  props: [
4540
6328
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4541
6329
  { name: "title", type: "string", required: true, description: "Book title." },
@@ -4559,9 +6347,9 @@ var v3CompoundAndContentEntries = [
4559
6347
  compoundContract: true,
4560
6348
  h5pMachineName: "H5P.CoursePresentation",
4561
6349
  h5pAlias: "Course Presentation slide",
4562
- description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
4563
- allowedChildTypes: [...import_core18.SLIDE_ALLOWED_CHILD_TYPES],
4564
- maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Slide,
6350
+ description: "Single slide row in a SlideDeck. Supports Video, Summary, and 1.4 blocks.",
6351
+ allowedChildTypes: [...import_core20.SLIDE_ALLOWED_CHILD_TYPES],
6352
+ maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.Slide,
4565
6353
  props: [
4566
6354
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4567
6355
  { name: "title", type: "string", required: false, description: "Slide title." },
@@ -4581,8 +6369,8 @@ var v3CompoundAndContentEntries = [
4581
6369
  h5pMachineName: "H5P.CoursePresentation",
4582
6370
  h5pAlias: "Course Presentation",
4583
6371
  description: "Multi-slide presentation with keyboard navigation.",
4584
- allowedChildTypes: [...import_core18.SLIDE_DECK_ALLOWED_CHILD_TYPES],
4585
- maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
6372
+ allowedChildTypes: [...import_core20.SLIDE_DECK_ALLOWED_CHILD_TYPES],
6373
+ maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
4586
6374
  props: [
4587
6375
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4588
6376
  { name: "title", type: "string", required: true, description: "Deck title." },
@@ -4600,6 +6388,121 @@ var v3CompoundAndContentEntries = [
4600
6388
  theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
4601
6389
  telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
4602
6390
  },
6391
+ {
6392
+ type: "TimedCue",
6393
+ category: "container",
6394
+ compoundContract: true,
6395
+ h5pMachineName: "H5P.InteractiveVideo",
6396
+ h5pAlias: "Interactive Video timed cue",
6397
+ description: "Timed overlay cue within InteractiveVideo.",
6398
+ allowedChildTypes: [...import_core20.TIMED_CUE_ALLOWED_CHILD_TYPES],
6399
+ maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.TimedCue,
6400
+ props: [
6401
+ { name: "atSeconds", type: "number", required: true, description: "Cue time in seconds." },
6402
+ { name: "label", type: "string", required: false, description: "Cue label." },
6403
+ { name: "mustComplete", type: "boolean", required: false, description: "Block seek until completed." },
6404
+ { name: "children", type: "ReactNode", required: true, description: "Single allowed child block." }
6405
+ ],
6406
+ requiredIds: [],
6407
+ parentConstraints: ["InteractiveVideo"],
6408
+ a11y: { element: "dialog", ariaLabel: "Timed cue", keyboard: "Focus moves to overlay content.", notes: "Pauses parent video." },
6409
+ theming: { surface: "global-inherit", stylingNotes: "Overlay panel." },
6410
+ telemetry: { emits: [] }
6411
+ },
6412
+ {
6413
+ type: "InteractiveVideo",
6414
+ category: "container",
6415
+ compoundContract: true,
6416
+ h5pMachineName: "H5P.InteractiveVideo",
6417
+ h5pAlias: "Interactive Video",
6418
+ description: "Video with timed interaction overlays.",
6419
+ allowedChildTypes: [...import_core20.INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES],
6420
+ maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.InteractiveVideo,
6421
+ props: [
6422
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
6423
+ { name: "title", type: "string", required: true, description: "Video title." },
6424
+ { name: "src", type: "string", required: true, description: "Video URL." },
6425
+ { name: "poster", type: "string", required: false, description: "Poster image." },
6426
+ { name: "captions", type: "string", required: false, description: "WebVTT captions." },
6427
+ { name: "showVideoScore", type: "boolean", required: false, description: "Show aggregate score." },
6428
+ { name: "children", type: "TimedCue[]", required: true, description: "Timed cues." }
6429
+ ],
6430
+ requiredIds: ["blockId"],
6431
+ parentConstraints: ["Lesson"],
6432
+ a11y: {
6433
+ element: "section",
6434
+ ariaLabel: "Interactive video",
6435
+ keyboard: "Native video controls; overlay interactions when paused.",
6436
+ notes: "H5P Interactive Video equivalent."
6437
+ },
6438
+ theming: { surface: "global-inherit", stylingNotes: "Video + overlay." },
6439
+ telemetry: { emits: ["video_cue_reached", "video_segment_completed"], requiresActiveLesson: true }
6440
+ },
6441
+ {
6442
+ type: "Questionnaire",
6443
+ category: "content",
6444
+ h5pMachineName: "H5P.Questionnaire",
6445
+ h5pAlias: "Questionnaire",
6446
+ description: "Unscored multi-field survey.",
6447
+ props: [
6448
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
6449
+ { name: "fields", type: "QuestionnaireField[]", required: true, description: "Form fields." }
6450
+ ],
6451
+ requiredIds: ["blockId"],
6452
+ parentConstraints: [...COMPOUND_PARENTS],
6453
+ a11y: { element: "form", ariaLabel: "Questionnaire", keyboard: "Tab through fields.", notes: "Unscored survey." },
6454
+ theming: { surface: "global-inherit", stylingNotes: "Form layout." },
6455
+ telemetry: { emits: ["questionnaire_submitted"], requiresActiveLesson: true }
6456
+ },
6457
+ {
6458
+ type: "MemoryGame",
6459
+ category: "content",
6460
+ h5pMachineName: "H5P.MemoryGame",
6461
+ h5pAlias: "Memory Game",
6462
+ description: "Card flip memory matching game.",
6463
+ props: [
6464
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
6465
+ { name: "pairs", type: "MemoryPair[]", required: true, description: "Card pairs." },
6466
+ { name: "selfScore", type: "boolean", required: false, description: "Optional self-score mode." }
6467
+ ],
6468
+ requiredIds: ["blockId"],
6469
+ parentConstraints: [...COMPOUND_PARENTS],
6470
+ a11y: { element: "section", ariaLabel: "Memory game", keyboard: "Flip cards with buttons.", notes: "Reduced motion safe." },
6471
+ theming: { surface: "global-inherit", stylingNotes: "Card grid." },
6472
+ telemetry: { emits: ["memory_card_flipped"] }
6473
+ },
6474
+ {
6475
+ type: "InformationWall",
6476
+ category: "content",
6477
+ h5pMachineName: "H5P.InformationWall",
6478
+ h5pAlias: "Information Wall",
6479
+ description: "Searchable information panels.",
6480
+ props: [
6481
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
6482
+ { name: "panels", type: "InformationPanel[]", required: true, description: "Content panels." }
6483
+ ],
6484
+ requiredIds: ["blockId"],
6485
+ parentConstraints: [...COMPOUND_PARENTS],
6486
+ a11y: { element: "section", ariaLabel: "Information wall", keyboard: "Search and browse panels.", notes: "Filterable grid." },
6487
+ theming: { surface: "global-inherit", stylingNotes: "Panel grid." },
6488
+ telemetry: { emits: ["information_wall_search"] }
6489
+ },
6490
+ {
6491
+ type: "ParallaxSlideshow",
6492
+ category: "content",
6493
+ h5pMachineName: "H5P.ImpressivePresentation",
6494
+ h5pAlias: "Slideshow (parallax)",
6495
+ description: "Slideshow with parallax; static fallback when reduced motion.",
6496
+ props: [
6497
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
6498
+ { name: "slides", type: "ParallaxSlide[]", required: true, description: "Slides." }
6499
+ ],
6500
+ requiredIds: ["blockId"],
6501
+ parentConstraints: [...COMPOUND_PARENTS],
6502
+ a11y: { element: "section", ariaLabel: "Slideshow", keyboard: "Previous/next slide.", notes: "prefers-reduced-motion fallback." },
6503
+ theming: { surface: "global-inherit", stylingNotes: "Slide deck." },
6504
+ telemetry: { emits: ["parallax_slide_viewed"] }
6505
+ },
4603
6506
  {
4604
6507
  type: "Accordion",
4605
6508
  category: "content",
@@ -4733,8 +6636,8 @@ function buildV3CatalogFromV2(v2) {
4733
6636
  return {
4734
6637
  ...base,
4735
6638
  compoundContract: true,
4736
- allowedChildTypes: [...import_core18.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
4737
- maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
6639
+ allowedChildTypes: [...import_core20.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
6640
+ maxNestingDepth: import_core20.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
4738
6641
  };
4739
6642
  }
4740
6643
  return base;
@@ -5081,6 +6984,100 @@ var v2AssessmentEntries = [
5081
6984
  },
5082
6985
  theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
5083
6986
  telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
6987
+ },
6988
+ {
6989
+ type: "Summary",
6990
+ category: "assessment",
6991
+ assessmentContract: true,
6992
+ h5pMachineName: "H5P.Summary",
6993
+ h5pAlias: "Summary",
6994
+ description: "Construct a summary from a statement bank in correct order.",
6995
+ props: [
6996
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
6997
+ { name: "statements", type: "string[]", required: true, description: "Available statements." },
6998
+ { name: "correct", type: "string[]", required: true, description: "Correct ordered summary." },
6999
+ ...assessmentBehaviourProps2
7000
+ ],
7001
+ requiredIds: ["checkId"],
7002
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
7003
+ a11y: { element: "section", ariaLabel: "Summary", keyboard: "Select statements in order.", notes: "H5P Summary equivalent." },
7004
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
7005
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
7006
+ },
7007
+ {
7008
+ type: "ImagePairing",
7009
+ category: "assessment",
7010
+ assessmentContract: true,
7011
+ h5pMachineName: "H5P.ImagePair",
7012
+ h5pAlias: "Image Pairing",
7013
+ description: "Match image pairs in a memory-style task.",
7014
+ props: [
7015
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
7016
+ { name: "pairs", type: "ImagePair[]", required: true, description: "Image pairs to match." },
7017
+ ...assessmentBehaviourProps2
7018
+ ],
7019
+ requiredIds: ["checkId"],
7020
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
7021
+ a11y: { element: "section", ariaLabel: "Image Pairing", keyboard: "Select two cards to match.", notes: "H5P Image Pairing equivalent." },
7022
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
7023
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
7024
+ },
7025
+ {
7026
+ type: "ImageSequencing",
7027
+ category: "assessment",
7028
+ assessmentContract: true,
7029
+ h5pMachineName: "H5P.ImageSequencing",
7030
+ h5pAlias: "Image Sequencing",
7031
+ description: "Order images in the correct sequence.",
7032
+ props: [
7033
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
7034
+ { name: "images", type: "SequencingImage[]", required: true, description: "Images to order." },
7035
+ { name: "correctOrder", type: "string[]", required: true, description: "Correct id order." },
7036
+ ...assessmentBehaviourProps2
7037
+ ],
7038
+ requiredIds: ["checkId"],
7039
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
7040
+ a11y: { element: "section", ariaLabel: "Image Sequencing", keyboard: "Reorder with up/down.", notes: "H5P Image Sequencing equivalent." },
7041
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
7042
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
7043
+ },
7044
+ {
7045
+ type: "ArithmeticQuiz",
7046
+ category: "assessment",
7047
+ assessmentContract: true,
7048
+ h5pMachineName: "H5P.ArithmeticQuiz",
7049
+ h5pAlias: "Arithmetic Quiz",
7050
+ description: "Timed arithmetic problems with optional timer.",
7051
+ props: [
7052
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
7053
+ { name: "problems", type: "ArithmeticProblem[]", required: true, description: "Math problems." },
7054
+ { name: "timeLimitSeconds", type: "number", required: false, description: "Optional time limit." },
7055
+ ...assessmentBehaviourProps2
7056
+ ],
7057
+ requiredIds: ["checkId"],
7058
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
7059
+ a11y: { element: "section", ariaLabel: "Arithmetic Quiz", keyboard: "Text input per problem.", notes: "H5P Arithmetic Quiz equivalent." },
7060
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
7061
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
7062
+ },
7063
+ {
7064
+ type: "Essay",
7065
+ category: "assessment",
7066
+ assessmentContract: true,
7067
+ h5pMachineName: "H5P.Essay",
7068
+ h5pAlias: "Essay",
7069
+ description: "Open text response; manual or plugin grading.",
7070
+ props: [
7071
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
7072
+ { name: "question", type: "string", required: true, description: "Essay prompt." },
7073
+ { name: "minLength", type: "number", required: false, description: "Minimum character length." },
7074
+ ...assessmentBehaviourProps2
7075
+ ],
7076
+ requiredIds: ["checkId"],
7077
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
7078
+ a11y: { element: "section", ariaLabel: "Essay", keyboard: "Textarea input.", notes: "H5P Essay equivalent." },
7079
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
7080
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
5084
7081
  }
5085
7082
  ];
5086
7083
  var BLOCK_CATALOG_V2 = [
@@ -5120,6 +7117,7 @@ function getBlockCatalogEntry(type, opts) {
5120
7117
  // Annotate the CommonJS export names for ESM import in node:
5121
7118
  0 && (module.exports = {
5122
7119
  Accordion,
7120
+ ArithmeticQuiz,
5123
7121
  AssessmentSequence,
5124
7122
  BLOCK_CATALOG,
5125
7123
  BLOCK_CATALOG_V2,
@@ -5128,6 +7126,7 @@ function getBlockCatalogEntry(type, opts) {
5128
7126
  DialogCards,
5129
7127
  DragAndDrop,
5130
7128
  DragTheWords,
7129
+ Essay,
5131
7130
  FillInTheBlanks,
5132
7131
  FindHotspot,
5133
7132
  FindMultipleHotspots,
@@ -5135,22 +7134,33 @@ function getBlockCatalogEntry(type, opts) {
5135
7134
  Heading,
5136
7135
  Image,
5137
7136
  ImageHotspots,
7137
+ ImagePairing,
7138
+ ImageSequencing,
5138
7139
  ImageSlider,
7140
+ InformationWall,
5139
7141
  InteractiveBook,
7142
+ InteractiveVideo,
5140
7143
  KnowledgeCheck,
5141
7144
  Lesson,
5142
7145
  LessonkitProvider,
5143
7146
  MarkTheWords,
7147
+ MemoryGame,
5144
7148
  Page,
7149
+ ParallaxSlideshow,
5145
7150
  ProgressTracker,
7151
+ Questionnaire,
5146
7152
  Quiz,
5147
7153
  Reflection,
5148
7154
  Scenario,
5149
7155
  Slide,
5150
7156
  SlideDeck,
7157
+ Summary,
5151
7158
  Text,
5152
7159
  ThemeProvider,
7160
+ TimedCue,
5153
7161
  TrueFalse,
7162
+ Video,
7163
+ assertProductionCourseConfig,
5154
7164
  blockCatalogV2Version,
5155
7165
  blockCatalogV3Version,
5156
7166
  blockCatalogVersion,
@@ -5165,6 +7175,7 @@ function getBlockCatalogEntry(type, opts) {
5165
7175
  getBlockCatalogEntry,
5166
7176
  resetAssessmentWarningsForTests,
5167
7177
  resetQuizWarningsForTests,
7178
+ shouldEnforceProductionGuard,
5168
7179
  useAssessmentState,
5169
7180
  useCompletion,
5170
7181
  useLessonkit,