@lessonkit/react 1.3.0 → 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,16 +113,25 @@ var import_core8 = require("@lessonkit/core");
99
113
 
100
114
  // src/runtime/observability.ts
101
115
  var import_xapi = require("@lessonkit/xapi");
102
- function createXapiQueueFromObservability(observability) {
103
- const opts = {};
104
- if (observability?.onXapiQueueDepth) {
105
- opts.onDepth = observability.onXapiQueueDepth;
106
- }
107
- if (observability?.onXapiQueueCap) {
108
- opts.onCap = observability.onXapiQueueCap;
109
- }
116
+ function createXapiQueueFromObservability(getObservability) {
117
+ const opts = {
118
+ onDepth: (size) => getObservability?.()?.onXapiQueueDepth?.(size),
119
+ onCap: () => getObservability?.()?.onXapiQueueCap?.()
120
+ };
110
121
  return (0, import_xapi.createInMemoryXAPIQueue)(opts);
111
122
  }
123
+ function wrapBatchSink(batchSink, observability) {
124
+ if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
125
+ const onError = observability.onTelemetrySinkError;
126
+ return async (events) => {
127
+ try {
128
+ await batchSink(events);
129
+ } catch (err) {
130
+ onError(err, { sinkId: "tracking-batch" });
131
+ throw err;
132
+ }
133
+ };
134
+ }
112
135
  function wrapTrackingSink(sink, observability) {
113
136
  if (!sink || !observability?.onTelemetrySinkError) return sink;
114
137
  const onError = observability.onTelemetrySinkError;
@@ -118,16 +141,108 @@ function wrapTrackingSink(sink, observability) {
118
141
  if (result != null && typeof result.catch === "function") {
119
142
  return result.catch((err) => {
120
143
  onError(err, { sinkId: "tracking" });
144
+ throw err;
121
145
  });
122
146
  }
123
147
  return result;
124
148
  } catch (err) {
125
149
  onError(err, { sinkId: "tracking" });
126
- return void 0;
150
+ throw err;
127
151
  }
128
152
  });
129
153
  }
130
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
+
131
246
  // src/provider/useLessonkitProviderRuntime.ts
132
247
  var import_xapi5 = require("@lessonkit/xapi");
133
248
 
@@ -226,7 +341,7 @@ var import_core4 = require("@lessonkit/core");
226
341
 
227
342
  // src/runtime/xapi.ts
228
343
  var import_xapi3 = require("@lessonkit/xapi");
229
- function createXapiClientFromConfig(config, queue) {
344
+ function createXapiClientFromConfig(config, queue, observability) {
230
345
  if (config.xapi?.enabled === false) return null;
231
346
  if (config.xapi?.client) return config.xapi.client;
232
347
  if (!config.courseId) return null;
@@ -235,7 +350,10 @@ function createXapiClientFromConfig(config, queue) {
235
350
  return (0, import_xapi3.createXAPIClient)({
236
351
  courseId: config.courseId,
237
352
  transport: config.xapi?.transport,
238
- queue
353
+ exitTransport: config.xapi?.exitTransport,
354
+ abortInFlight: config.xapi?.abortInFlight,
355
+ queue,
356
+ onTransportError: observability?.onXapiTransportError
239
357
  });
240
358
  }
241
359
 
@@ -283,6 +401,7 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
283
401
  const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(opts.event);
284
402
  if (statement) {
285
403
  opts.xapi.send(statement);
404
+ await opts.xapi.flush();
286
405
  xapiStatementSent = true;
287
406
  }
288
407
  }
@@ -318,12 +437,26 @@ function emitTelemetryWithPlugins(opts) {
318
437
  }
319
438
 
320
439
  // src/provider/courseStarted/emit.ts
321
- var courseStartedTrackingFlightKey = null;
440
+ function resolveTrackingClient(source) {
441
+ return typeof source === "function" ? source() : source;
442
+ }
443
+ var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
444
+ var courseStartedEmitFlights = /* @__PURE__ */ new Map();
322
445
  function isTrackingActive(tracking) {
323
446
  return tracking?.enabled !== false;
324
447
  }
325
448
  function isCourseStartedSinkSettled(result) {
326
- 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;
327
460
  }
328
461
  function buildCourseStartedEvent(opts) {
329
462
  const pluginCtx = buildPluginContext({
@@ -346,25 +479,45 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
346
479
  if ((0, import_core5.hasCourseStartedEmittedToTracking)(storage, sessionId, courseId)) {
347
480
  return true;
348
481
  }
349
- if (courseStartedTrackingFlightKey === flightKey) {
350
- return false;
482
+ const existing = courseStartedTrackingFlights.get(flightKey);
483
+ if (existing) {
484
+ return existing;
351
485
  }
352
- courseStartedTrackingFlightKey = flightKey;
353
- try {
354
- if (shouldCommit && !shouldCommit()) return false;
355
- tracking.track(event);
356
- (0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId);
357
- const delivered = await tracking.flush?.();
358
- if (delivered === false) return false;
359
- if (shouldCommit && !shouldCommit()) return false;
360
- return true;
361
- } catch {
362
- return false;
363
- } finally {
364
- if (courseStartedTrackingFlightKey === flightKey) {
365
- courseStartedTrackingFlightKey = null;
486
+ let resolveFlight;
487
+ const flight = new Promise((resolve) => {
488
+ resolveFlight = resolve;
489
+ });
490
+ courseStartedTrackingFlights.set(flightKey, flight);
491
+ void (async () => {
492
+ try {
493
+ if (shouldCommit && !shouldCommit()) {
494
+ resolveFlight(false);
495
+ return;
496
+ }
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) {
504
+ resolveFlight(false);
505
+ return;
506
+ }
507
+ if ((0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId) === false) {
508
+ resolveFlight(false);
509
+ return;
510
+ }
511
+ resolveFlight(true);
512
+ } catch {
513
+ resolveFlight(false);
514
+ } finally {
515
+ if (courseStartedTrackingFlights.get(flightKey) === flight) {
516
+ courseStartedTrackingFlights.delete(flightKey);
517
+ }
366
518
  }
367
- }
519
+ })();
520
+ return flight;
368
521
  }
369
522
  async function emitCourseStartedPipelineOnly(opts) {
370
523
  try {
@@ -378,8 +531,10 @@ async function emitCourseStartedPipelineOnly(opts) {
378
531
  skipXapi: opts.skipXapi
379
532
  });
380
533
  if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
381
- (0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
382
- (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
534
+ if ((0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
535
+ if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
536
+ return "failed";
537
+ }
383
538
  if (xapiStatementSent) {
384
539
  opts.onXapiStatementSent?.();
385
540
  }
@@ -430,19 +585,64 @@ async function emitCourseStartedToTrackingOnly(opts) {
430
585
  extraSinks: opts.extraSinks,
431
586
  skipXapi: true
432
587
  });
433
- (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
588
+ if ((0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId) === false) {
589
+ return "failed";
590
+ }
434
591
  return "emitted";
435
592
  } catch {
436
593
  return "failed";
437
594
  }
438
595
  }
439
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) {
440
632
  const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
441
633
  opts.storage,
442
634
  opts.sessionId,
443
635
  opts.courseId
444
636
  );
445
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
+ }
446
646
  if (sessionStarted && !trackingEmitted) {
447
647
  return emitCourseStartedToTrackingOnly(opts);
448
648
  }
@@ -454,14 +654,6 @@ async function emitPendingCourseStarted(opts) {
454
654
  if (!trackingEmitted && !sessionStarted) {
455
655
  return emitCourseStarted(opts);
456
656
  }
457
- const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
458
- opts.storage,
459
- opts.sessionId,
460
- opts.courseId
461
- );
462
- if (sessionStarted && trackingEmitted && pipelineDelivered) {
463
- return "emitted";
464
- }
465
657
  if (sessionStarted && trackingEmitted && !pipelineDelivered) {
466
658
  const event = buildCourseStartedEvent(opts);
467
659
  if (event === null) return "filtered";
@@ -472,7 +664,7 @@ async function emitPendingCourseStarted(opts) {
472
664
  onXapiStatementSent: opts.onXapiStatementSent
473
665
  });
474
666
  }
475
- return "emitted";
667
+ return "failed";
476
668
  }
477
669
  function assertTrackingSinkConfig(tracking) {
478
670
  if (!tracking?.sink || !tracking?.batchSink) return;
@@ -490,6 +682,7 @@ function createTrackingClientFromConfig(config, observability) {
490
682
  sink: config.tracking?.sink,
491
683
  batchSink: config.tracking?.batchSink,
492
684
  batch: config.tracking?.batch,
685
+ exitBatchSink: config.tracking?.exitBatchSink,
493
686
  onBufferDrop: observability?.onTelemetryBufferDrop
494
687
  });
495
688
  }
@@ -519,6 +712,9 @@ function useLessonkitProviderRuntime(config) {
519
712
  () => ({ ...config, courseId: normalizedCourseId }),
520
713
  [config, normalizedCourseId]
521
714
  );
715
+ if (shouldEnforceProductionGuard()) {
716
+ assertProductionCourseConfig(normalizedConfig);
717
+ }
522
718
  const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
523
719
  (0, import_react.useEffect)(() => {
524
720
  if (useV2Runtime) return;
@@ -565,6 +761,7 @@ function useLessonkitProviderRuntime(config) {
565
761
  const pendingCourseIdResetRef = (0, import_react.useRef)(false);
566
762
  const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
567
763
  const xapiCourseStartedSentOnClientRef = (0, import_react.useRef)(false);
764
+ const xapiBootstrapSendRef = (0, import_react.useRef)(false);
568
765
  if (prevUseV2RuntimeRef.current !== useV2Runtime) {
569
766
  prevUseV2RuntimeRef.current = useV2Runtime;
570
767
  if (useV2Runtime) {
@@ -613,7 +810,7 @@ function useLessonkitProviderRuntime(config) {
613
810
  }, []);
614
811
  const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
615
812
  activeLessonIdRef.current = progress.activeLessonId;
616
- const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(normalizedConfig.observability));
813
+ const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(() => observabilityRef.current));
617
814
  const xapiRef = (0, import_react.useRef)(null);
618
815
  const [xapi, setXapi] = (0, import_react.useState)(null);
619
816
  const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
@@ -634,22 +831,29 @@ function useLessonkitProviderRuntime(config) {
634
831
  }
635
832
  void xapiRef.current?.flush();
636
833
  }
637
- xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
834
+ xapiQueueRef.current = createXapiQueueFromObservability(() => observabilityRef.current);
638
835
  prevXapiCourseIdRef.current = courseId;
639
836
  xapiCourseStartedSentOnClientRef.current = false;
837
+ xapiBootstrapSendRef.current = false;
640
838
  }
641
839
  const prev = xapiRef.current;
642
- const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
840
+ const next = createXapiClientFromConfig(
841
+ normalizedConfig,
842
+ xapiQueueRef.current,
843
+ observabilityRef.current
844
+ );
643
845
  xapiRef.current = next;
644
846
  setXapi(next);
847
+ let bootstrapSent = false;
848
+ let bootstrapAlreadyStarted = false;
645
849
  if (next) {
646
850
  const sessionId = sessionIdRef.current;
647
851
  const cid = courseIdRef.current;
648
852
  const trackingActive = isTrackingActive(normalizedConfig.tracking);
649
- const alreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
853
+ bootstrapAlreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
650
854
  const clientChanged = !prev || prev !== next;
651
- const skipBootstrap = trackingActive && !alreadyStarted;
652
- const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
855
+ const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
856
+ const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
653
857
  if (needsBootstrap) {
654
858
  try {
655
859
  const event = buildCourseStartedEvent({
@@ -660,15 +864,12 @@ function useLessonkitProviderRuntime(config) {
660
864
  user: userRef.current,
661
865
  lxpackBridge: lxpackBridgeModeRef.current
662
866
  });
663
- if (event === null) {
664
- } else {
867
+ if (event !== null) {
665
868
  const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
666
869
  if (statement) {
667
870
  next.send(statement);
668
- if (!alreadyStarted) {
669
- (0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
670
- }
671
- xapiCourseStartedSentOnClientRef.current = true;
871
+ xapiBootstrapSendRef.current = true;
872
+ bootstrapSent = true;
672
873
  }
673
874
  }
674
875
  } catch {
@@ -686,12 +887,23 @@ function useLessonkitProviderRuntime(config) {
686
887
  if (cancelled) return;
687
888
  try {
688
889
  await next?.flush();
890
+ if (bootstrapSent && !cancelled) {
891
+ if (!bootstrapAlreadyStarted) {
892
+ (0, import_core5.markCourseStarted)(defaultStorage, sessionIdRef.current, courseIdRef.current);
893
+ }
894
+ xapiCourseStartedSentOnClientRef.current = true;
895
+ }
689
896
  } catch {
690
897
  }
691
898
  })();
692
899
  return () => {
693
900
  cancelled = true;
694
- void prev?.flush();
901
+ void (async () => {
902
+ try {
903
+ await prev?.flush();
904
+ } catch {
905
+ }
906
+ })();
695
907
  };
696
908
  }, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
697
909
  const trackingRef = (0, import_react.useRef)((0, import_core8.createTrackingClient)());
@@ -714,7 +926,10 @@ function useLessonkitProviderRuntime(config) {
714
926
  useIsoLayoutEffect(() => {
715
927
  const prev = trackingRef.current;
716
928
  const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
717
- const userBatchSink = normalizedConfig.tracking?.batchSink;
929
+ const userBatchSink = wrapBatchSink(
930
+ normalizedConfig.tracking?.batchSink,
931
+ observabilityRef.current
932
+ );
718
933
  assertTrackingSinkConfig(normalizedConfig.tracking);
719
934
  const sink = pluginHostRef.current && baseSink ? (
720
935
  /* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
@@ -752,13 +967,13 @@ function useLessonkitProviderRuntime(config) {
752
967
  } else if (courseStartedFullySettled) {
753
968
  courseStartedEmittedToSinkRef.current = true;
754
969
  } else if (!courseStartedEmittedToSinkRef.current) {
755
- const generation = ++courseStartedEmitGenerationRef.current;
970
+ const generation = courseStartedEmitGenerationRef.current;
756
971
  const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
757
972
  void (async () => {
758
973
  if (generation !== courseStartedEmitGenerationRef.current) return;
759
974
  const result = await emitPendingCourseStarted({
760
975
  pluginHost: pluginHostRef.current,
761
- tracking: next,
976
+ tracking: () => trackingRef.current,
762
977
  xapi: xapiRef.current,
763
978
  storage: defaultStorage,
764
979
  sessionId,
@@ -768,7 +983,7 @@ function useLessonkitProviderRuntime(config) {
768
983
  lxpackBridge: lxpackBridgeModeRef.current,
769
984
  onLxpackBridgeMiss,
770
985
  extraSinks: extraSinksRef.current,
771
- skipXapi: xapiCourseStartedSentOnClientRef.current,
986
+ skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
772
987
  onXapiStatementSent: () => {
773
988
  xapiCourseStartedSentOnClientRef.current = true;
774
989
  },
@@ -779,7 +994,6 @@ function useLessonkitProviderRuntime(config) {
779
994
  })();
780
995
  }
781
996
  return () => {
782
- courseStartedEmitGenerationRef.current += 1;
783
997
  if (prev !== trackingRef.current) {
784
998
  void disposeTrackingClient(prev);
785
999
  }
@@ -857,9 +1071,11 @@ function useLessonkitProviderRuntime(config) {
857
1071
  } catch {
858
1072
  }
859
1073
  if (!courseStartedEmittedToSinkRef.current) {
1074
+ const generation = courseStartedEmitGenerationRef.current;
1075
+ const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
860
1076
  const result = await emitPendingCourseStarted({
861
1077
  pluginHost: pluginHostRef.current,
862
- tracking: trackingRef.current,
1078
+ tracking: () => trackingRef.current,
863
1079
  xapi: xapiRef.current,
864
1080
  storage: defaultStorage,
865
1081
  sessionId,
@@ -868,8 +1084,14 @@ function useLessonkitProviderRuntime(config) {
868
1084
  user: userRef.current,
869
1085
  lxpackBridge: lxpackBridgeModeRef.current,
870
1086
  onLxpackBridgeMiss,
871
- extraSinks: extraSinksRef.current
1087
+ extraSinks: extraSinksRef.current,
1088
+ skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
1089
+ onXapiStatementSent: () => {
1090
+ xapiCourseStartedSentOnClientRef.current = true;
1091
+ },
1092
+ shouldCommit
872
1093
  });
1094
+ if (generation !== courseStartedEmitGenerationRef.current) return;
873
1095
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
874
1096
  }
875
1097
  })();
@@ -924,18 +1146,23 @@ function useLessonkitProviderRuntime(config) {
924
1146
  }, []);
925
1147
  (0, import_react.useEffect)(() => {
926
1148
  if (typeof document === "undefined") return;
927
- const flushOnExit = () => {
928
- void xapiRef.current?.flush();
929
- void trackingRef.current?.flush?.();
1149
+ const flushOnPageExit = () => {
1150
+ try {
1151
+ xapiRef.current?.flushOnExit?.();
1152
+ trackingRef.current?.flushOnExit?.();
1153
+ } finally {
1154
+ void xapiRef.current?.flush();
1155
+ void trackingRef.current?.flush?.();
1156
+ }
930
1157
  };
931
1158
  const onVisibilityChange = () => {
932
- if (document.visibilityState === "hidden") flushOnExit();
1159
+ if (document.visibilityState === "hidden") flushOnPageExit();
933
1160
  };
934
1161
  document.addEventListener("visibilitychange", onVisibilityChange);
935
- window.addEventListener("pagehide", flushOnExit);
1162
+ window.addEventListener("pagehide", flushOnPageExit);
936
1163
  return () => {
937
1164
  document.removeEventListener("visibilitychange", onVisibilityChange);
938
- window.removeEventListener("pagehide", flushOnExit);
1165
+ window.removeEventListener("pagehide", flushOnPageExit);
939
1166
  };
940
1167
  }, []);
941
1168
  const setActiveLesson = (0, import_react.useCallback)(
@@ -2210,9 +2437,13 @@ function FillInTheBlanksInner(props, ref) {
2210
2437
  const [submitted, setSubmitted] = (0, import_react16.useState)(false);
2211
2438
  const completedRef = (0, import_react16.useRef)(false);
2212
2439
  const answeredRef = (0, import_react16.useRef)(false);
2440
+ const checkSnapshotRef = (0, import_react16.useRef)(null);
2441
+ const telemetryReplayedRef = (0, import_react16.useRef)(false);
2213
2442
  const reset = () => {
2214
2443
  completedRef.current = false;
2215
2444
  answeredRef.current = false;
2445
+ checkSnapshotRef.current = null;
2446
+ telemetryReplayedRef.current = false;
2216
2447
  setPassed(false);
2217
2448
  setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
2218
2449
  setShowSolutions(false);
@@ -2229,6 +2460,31 @@ function FillInTheBlanksInner(props, ref) {
2229
2460
  });
2230
2461
  const maxScore = blanks.length;
2231
2462
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2463
+ const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2464
+ if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2465
+ telemetryReplayedRef.current = true;
2466
+ const nextPassedThreshold = meetsPassingThreshold(
2467
+ nextScore,
2468
+ nextMaxScore || 1,
2469
+ props.passingScore
2470
+ );
2471
+ assessment.answer({
2472
+ checkId,
2473
+ interactionType: INTERACTION3,
2474
+ question: props.template,
2475
+ response: nextValues,
2476
+ correct: nextPassedThreshold
2477
+ });
2478
+ if (nextPassed || nextPassedThreshold) {
2479
+ assessment.complete({
2480
+ checkId,
2481
+ interactionType: INTERACTION3,
2482
+ score: nextScore,
2483
+ maxScore: nextMaxScore,
2484
+ passingScore: props.passingScore ?? nextMaxScore
2485
+ });
2486
+ }
2487
+ };
2232
2488
  const handle = (0, import_react16.useMemo)(
2233
2489
  () => buildAssessmentHandle({
2234
2490
  checkId,
@@ -2248,20 +2504,33 @@ function FillInTheBlanksInner(props, ref) {
2248
2504
  getCurrentState: () => ({ values, passed, showSolutions, submitted }),
2249
2505
  resume: (state) => {
2250
2506
  const raw = state.values;
2251
- if (raw && typeof raw === "object") setValues({ ...raw });
2507
+ let nextValues = values;
2508
+ if (raw && typeof raw === "object") {
2509
+ nextValues = { ...raw };
2510
+ setValues(nextValues);
2511
+ }
2512
+ let nextPassed = passed;
2513
+ let nextSubmitted = submitted;
2252
2514
  readBooleanStateField(state, "passed", (value) => {
2515
+ nextPassed = value;
2253
2516
  setPassed(value);
2254
2517
  completedRef.current = value;
2255
2518
  answeredRef.current = value;
2256
2519
  });
2257
2520
  readBooleanStateField(state, "showSolutions", setShowSolutions);
2258
2521
  readBooleanStateField(state, "submitted", (value) => {
2522
+ nextSubmitted = value;
2259
2523
  setSubmitted(value);
2260
2524
  if (value) answeredRef.current = true;
2261
2525
  });
2526
+ let nextScore = 0;
2527
+ blanks.forEach((b) => {
2528
+ if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
2529
+ });
2530
+ replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
2262
2531
  }
2263
2532
  }),
2264
- [allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, submitted, values]
2533
+ [allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
2265
2534
  );
2266
2535
  useAssessmentHandleRegistration(checkId, handle, ref);
2267
2536
  const check = () => {
@@ -2272,7 +2541,10 @@ function FillInTheBlanksInner(props, ref) {
2272
2541
  return;
2273
2542
  }
2274
2543
  if (!allFilled) return;
2275
- if (answeredRef.current || submitted) return;
2544
+ if (passed) return;
2545
+ const snapshot = JSON.stringify(values);
2546
+ if (checkSnapshotRef.current === snapshot) return;
2547
+ checkSnapshotRef.current = snapshot;
2276
2548
  answeredRef.current = true;
2277
2549
  setSubmitted(true);
2278
2550
  assessment.answer({
@@ -2297,12 +2569,13 @@ function FillInTheBlanksInner(props, ref) {
2297
2569
  (0, import_react16.useEffect)(() => {
2298
2570
  if (!allFilled) {
2299
2571
  answeredRef.current = false;
2572
+ checkSnapshotRef.current = null;
2300
2573
  setSubmitted(false);
2301
2574
  }
2302
2575
  }, [allFilled]);
2303
2576
  (0, import_react16.useEffect)(() => {
2304
- if (props.autoCheck && allFilled) check();
2305
- }, [allFilled, props.autoCheck, values, passedThreshold]);
2577
+ if (props.autoCheck && allFilled && !passed) check();
2578
+ }, [allFilled, props.autoCheck, values, passedThreshold, passed]);
2306
2579
  const reveal = showSolutions || passed && props.enableSolutionsButton;
2307
2580
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2308
2581
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: parsed.parts.map((part, i) => {
@@ -2361,9 +2634,13 @@ function DragTheWordsInner(props, ref) {
2361
2634
  const [submitted, setSubmitted] = (0, import_react17.useState)(false);
2362
2635
  const completedRef = (0, import_react17.useRef)(false);
2363
2636
  const answeredRef = (0, import_react17.useRef)(false);
2637
+ const checkSnapshotRef = (0, import_react17.useRef)(null);
2638
+ const telemetryReplayedRef = (0, import_react17.useRef)(false);
2364
2639
  const reset = () => {
2365
2640
  completedRef.current = false;
2366
2641
  answeredRef.current = false;
2642
+ checkSnapshotRef.current = null;
2643
+ telemetryReplayedRef.current = false;
2367
2644
  setPassed(false);
2368
2645
  setSubmitted(false);
2369
2646
  setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
@@ -2381,6 +2658,31 @@ function DragTheWordsInner(props, ref) {
2381
2658
  });
2382
2659
  const maxScore = answers.length;
2383
2660
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2661
+ const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2662
+ if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2663
+ telemetryReplayedRef.current = true;
2664
+ const nextPassedThreshold = meetsPassingThreshold(
2665
+ nextScore,
2666
+ nextMaxScore || 1,
2667
+ props.passingScore
2668
+ );
2669
+ assessment.answer({
2670
+ checkId,
2671
+ interactionType: INTERACTION4,
2672
+ question: props.template,
2673
+ response: nextZones,
2674
+ correct: nextPassedThreshold
2675
+ });
2676
+ if (nextPassed || nextPassedThreshold) {
2677
+ assessment.complete({
2678
+ checkId,
2679
+ interactionType: INTERACTION4,
2680
+ score: nextScore,
2681
+ maxScore: nextMaxScore,
2682
+ passingScore: props.passingScore ?? nextMaxScore
2683
+ });
2684
+ }
2685
+ };
2384
2686
  const handle = (0, import_react17.useMemo)(
2385
2687
  () => buildAssessmentHandle({
2386
2688
  checkId,
@@ -2401,22 +2703,35 @@ function DragTheWordsInner(props, ref) {
2401
2703
  getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
2402
2704
  resume: (state) => {
2403
2705
  const rawZones = state.zones;
2404
- if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
2706
+ let nextZones = zones;
2707
+ if (rawZones && typeof rawZones === "object") {
2708
+ nextZones = { ...rawZones };
2709
+ setZones(nextZones);
2710
+ }
2405
2711
  if (Array.isArray(state.pool)) setPool([...state.pool]);
2712
+ let nextPassed = passed;
2713
+ let nextSubmitted = submitted;
2406
2714
  readBooleanStateField(state, "passed", (value) => {
2715
+ nextPassed = value;
2407
2716
  setPassed(value);
2408
2717
  completedRef.current = value;
2409
2718
  answeredRef.current = value;
2410
2719
  });
2411
2720
  readBooleanStateField(state, "submitted", (value) => {
2721
+ nextSubmitted = value;
2412
2722
  setSubmitted(value);
2413
2723
  if (value) answeredRef.current = true;
2414
2724
  });
2415
2725
  const kw = state.keyboardWord;
2416
2726
  if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2727
+ let nextScore = 0;
2728
+ answers.forEach((ans, i) => {
2729
+ if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
2730
+ });
2731
+ replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
2417
2732
  }
2418
2733
  }),
2419
- [allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, submitted, zones]
2734
+ [allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
2420
2735
  );
2421
2736
  useAssessmentHandleRegistration(checkId, handle, ref);
2422
2737
  const placeInZone = (zoneId, word) => {
@@ -2446,7 +2761,10 @@ function DragTheWordsInner(props, ref) {
2446
2761
  return;
2447
2762
  }
2448
2763
  if (!allFilled) return;
2449
- if (answeredRef.current || submitted) return;
2764
+ if (passed) return;
2765
+ const snapshot = JSON.stringify(zones);
2766
+ if (checkSnapshotRef.current === snapshot) return;
2767
+ checkSnapshotRef.current = snapshot;
2450
2768
  answeredRef.current = true;
2451
2769
  setSubmitted(true);
2452
2770
  assessment.answer({
@@ -2471,12 +2789,13 @@ function DragTheWordsInner(props, ref) {
2471
2789
  (0, import_react17.useEffect)(() => {
2472
2790
  if (!allFilled) {
2473
2791
  answeredRef.current = false;
2792
+ checkSnapshotRef.current = null;
2474
2793
  setSubmitted(false);
2475
2794
  }
2476
2795
  }, [allFilled]);
2477
2796
  (0, import_react17.useEffect)(() => {
2478
- if (props.autoCheck && allFilled) check();
2479
- }, [allFilled, props.autoCheck, zones, passedThreshold]);
2797
+ if (props.autoCheck && allFilled && !passed) check();
2798
+ }, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
2480
2799
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2481
2800
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2482
2801
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
@@ -2876,6 +3195,10 @@ function useCompoundPersistence(opts) {
2876
3195
  }, [ctx, opts.index, opts.pageCount]);
2877
3196
  const buildStateRef = (0, import_react21.useRef)(buildState);
2878
3197
  buildStateRef.current = buildState;
3198
+ const transformStateRef = (0, import_react21.useRef)(opts.transformState);
3199
+ transformStateRef.current = opts.transformState;
3200
+ const persistNowRef = (0, import_react21.useRef)(() => {
3201
+ });
2879
3202
  const finalizeHydration = (0, import_react21.useCallback)(
2880
3203
  (childStates) => {
2881
3204
  loadedChildStatesRef.current = {
@@ -2884,6 +3207,7 @@ function useCompoundPersistence(opts) {
2884
3207
  };
2885
3208
  skipSaveUntilHydratedRef.current = false;
2886
3209
  pendingChildResumeRef.current = null;
3210
+ queueMicrotask(() => persistNowRef.current());
2887
3211
  },
2888
3212
  []
2889
3213
  );
@@ -2896,6 +3220,14 @@ function useCompoundPersistence(opts) {
2896
3220
  alreadyResumed: resumedChildKeysRef.current
2897
3221
  });
2898
3222
  if (!applied) {
3223
+ if (handles.size === 0) {
3224
+ const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
3225
+ resumeChildHandles(handles, registeredOnly2, {
3226
+ alreadyResumed: resumedChildKeysRef.current
3227
+ });
3228
+ finalizeHydration(registeredOnly2);
3229
+ return;
3230
+ }
2899
3231
  const handlesAtWait = handles.size;
2900
3232
  queueMicrotask(() => {
2901
3233
  if (pendingChildResumeRef.current !== pending) return;
@@ -2929,8 +3261,14 @@ function useCompoundPersistence(opts) {
2929
3261
  });
2930
3262
  const persistNow = (0, import_react21.useCallback)(() => {
2931
3263
  if (!opts.enabled || !opts.courseId) return;
2932
- saveResume(buildStateRef.current());
3264
+ if (skipSaveUntilHydratedRef.current) return;
3265
+ const built = buildStateRef.current();
3266
+ const state = transformStateRef.current ? transformStateRef.current(built) : built;
3267
+ saveResume(state);
2933
3268
  }, [opts.enabled, opts.courseId, saveResume]);
3269
+ (0, import_react21.useEffect)(() => {
3270
+ persistNowRef.current = persistNow;
3271
+ }, [persistNow]);
2934
3272
  const notifyImperativeResume = (0, import_react21.useCallback)(
2935
3273
  (state) => {
2936
3274
  const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
@@ -2952,12 +3290,12 @@ function useCompoundPersistence(opts) {
2952
3290
  }
2953
3291
  };
2954
3292
  }, [bridgeRef, notifyImperativeResume]);
2955
- (0, import_react21.useEffect)(() => {
2956
- persistNow();
2957
- }, [persistNow, opts.index, opts.pageCount, handlesVersion]);
2958
3293
  (0, import_react21.useEffect)(() => {
2959
3294
  applyPendingChildResume();
2960
3295
  }, [opts.index, handlesVersion, applyPendingChildResume]);
3296
+ (0, import_react21.useEffect)(() => {
3297
+ persistNow();
3298
+ }, [persistNow, opts.index, opts.pageCount, handlesVersion]);
2961
3299
  (0, import_react21.useEffect)(() => {
2962
3300
  if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
2963
3301
  const flushOnExit = () => {
@@ -2982,7 +3320,8 @@ function useCompoundShell(opts) {
2982
3320
  index: opts.index,
2983
3321
  setIndex: opts.setIndex,
2984
3322
  enabled: opts.persistEnabled,
2985
- storage: opts.storage
3323
+ storage: opts.storage,
3324
+ transformState: opts.transformState
2986
3325
  });
2987
3326
  const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
2988
3327
  const visibleIndex = (0, import_core15.clampCompoundPageIndex)(opts.index, opts.pageCount);
@@ -3037,6 +3376,8 @@ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
3037
3376
  "InteractiveBook",
3038
3377
  "Slide",
3039
3378
  "SlideDeck",
3379
+ "TimedCue",
3380
+ "InteractiveVideo",
3040
3381
  "AssessmentSequence"
3041
3382
  ]);
3042
3383
  function warnOrThrow(msg, strict) {
@@ -3265,14 +3606,40 @@ function Image(props) {
3265
3606
  }
3266
3607
  setLessonkitBlockType(Image, "Image");
3267
3608
 
3268
- // src/blocks/Page.tsx
3609
+ // src/blocks/Video.tsx
3269
3610
  var import_react26 = require("react");
3270
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");
3271
3638
  function Page(props) {
3272
3639
  validateCompoundChildren("Page", props.children);
3273
3640
  const { track } = useLessonkit();
3274
3641
  const lessonId = useEnclosingLessonId();
3275
- (0, import_react26.useEffect)(() => {
3642
+ (0, import_react27.useEffect)(() => {
3276
3643
  if (props.hidden || !lessonId || props.parentType) return;
3277
3644
  track(
3278
3645
  "compound_page_viewed",
@@ -3284,7 +3651,7 @@ function Page(props) {
3284
3651
  { lessonId }
3285
3652
  );
3286
3653
  }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
3287
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3654
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3288
3655
  "section",
3289
3656
  {
3290
3657
  "aria-label": props.title ?? "Page",
@@ -3292,8 +3659,8 @@ function Page(props) {
3292
3659
  "data-testid": `page-${props.blockId}`,
3293
3660
  hidden: props.hidden ? true : void 0,
3294
3661
  children: [
3295
- props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { children: props.title }) : null,
3296
- /* @__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 }) })
3297
3664
  ]
3298
3665
  }
3299
3666
  );
@@ -3301,9 +3668,9 @@ function Page(props) {
3301
3668
  setLessonkitBlockType(Page, "Page");
3302
3669
 
3303
3670
  // src/blocks/InteractiveBook.tsx
3304
- var import_react27 = __toESM(require("react"), 1);
3305
- var import_jsx_runtime18 = require("react/jsx-runtime");
3306
- 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)(
3307
3674
  function InteractiveBookInner2(props, ref) {
3308
3675
  const { blockId, pages, index, setIndex, persistEnabled } = props;
3309
3676
  validateCompoundChildren("InteractiveBook", pages);
@@ -3318,11 +3685,11 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3318
3685
  persistEnabled,
3319
3686
  ref
3320
3687
  });
3321
- const pageTitles = (0, import_react27.useMemo)(
3688
+ const pageTitles = (0, import_react28.useMemo)(
3322
3689
  () => pages.map((page) => page.props.title),
3323
3690
  [pages]
3324
3691
  );
3325
- (0, import_react27.useEffect)(() => {
3692
+ (0, import_react28.useEffect)(() => {
3326
3693
  if (!lessonId || pages.length === 0) return;
3327
3694
  track(
3328
3695
  "book_page_viewed",
@@ -3334,31 +3701,31 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3334
3701
  { lessonId }
3335
3702
  );
3336
3703
  }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
3337
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
3338
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }),
3339
- /* @__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: [
3340
3707
  "Page ",
3341
3708
  progress.current,
3342
3709
  " of ",
3343
3710
  progress.total
3344
3711
  ] }),
3345
- 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: [
3346
3713
  "Score: ",
3347
3714
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3348
3715
  " /",
3349
3716
  " ",
3350
3717
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3351
3718
  ] }) : null,
3352
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
3353
- (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, {
3354
3721
  key: page.key ?? page.props.blockId,
3355
3722
  hidden: i !== visibleIndex,
3356
3723
  pageIndex: i,
3357
3724
  parentType: "InteractiveBook"
3358
3725
  })
3359
3726
  ) }),
3360
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("nav", { "aria-label": "Book navigation", children: [
3361
- /* @__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)(
3362
3729
  "button",
3363
3730
  {
3364
3731
  type: "button",
@@ -3368,7 +3735,7 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3368
3735
  children: "Previous"
3369
3736
  }
3370
3737
  ),
3371
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3738
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3372
3739
  "button",
3373
3740
  {
3374
3741
  type: "button",
@@ -3382,13 +3749,13 @@ var InteractiveBookInner = (0, import_react27.forwardRef)(
3382
3749
  ] });
3383
3750
  }
3384
3751
  );
3385
- var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(props, ref) {
3386
- const blockId = (0, import_react27.useMemo)(
3752
+ var InteractiveBook = (0, import_react28.forwardRef)(function InteractiveBook2(props, ref) {
3753
+ const blockId = (0, import_react28.useMemo)(
3387
3754
  () => normalizeComponentId(props.blockId, "blockId"),
3388
3755
  [props.blockId]
3389
3756
  );
3390
- const pages = import_react27.default.Children.toArray(props.children).filter(
3391
- import_react27.default.isValidElement
3757
+ const pages = import_react28.default.Children.toArray(props.children).filter(
3758
+ import_react28.default.isValidElement
3392
3759
  );
3393
3760
  const { config, storage } = useLessonkit();
3394
3761
  const persistEnabled = config.session?.persistCompoundState !== false;
@@ -3399,12 +3766,12 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
3399
3766
  persistEnabled,
3400
3767
  storage
3401
3768
  });
3402
- const [index, setIndex] = (0, import_react27.useState)(initialIndex);
3403
- const setIndexStable = (0, import_react27.useCallback)((i) => setIndex(i), []);
3404
- (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)(() => {
3405
3772
  setIndex(initialIndex);
3406
3773
  }, [config.courseId, blockId, initialIndex]);
3407
- 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)(
3408
3775
  InteractiveBookInner,
3409
3776
  {
3410
3777
  ...props,
@@ -3420,13 +3787,13 @@ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(p
3420
3787
  setLessonkitBlockType(InteractiveBook, "InteractiveBook");
3421
3788
 
3422
3789
  // src/blocks/Slide.tsx
3423
- var import_react28 = require("react");
3424
- var import_jsx_runtime19 = require("react/jsx-runtime");
3790
+ var import_react29 = require("react");
3791
+ var import_jsx_runtime20 = require("react/jsx-runtime");
3425
3792
  function Slide(props) {
3426
3793
  validateCompoundChildren("Slide", props.children);
3427
3794
  const { track } = useLessonkit();
3428
3795
  const lessonId = useEnclosingLessonId();
3429
- (0, import_react28.useEffect)(() => {
3796
+ (0, import_react29.useEffect)(() => {
3430
3797
  if (props.hidden || !lessonId || props.parentType) return;
3431
3798
  track(
3432
3799
  "compound_page_viewed",
@@ -3438,7 +3805,7 @@ function Slide(props) {
3438
3805
  { lessonId }
3439
3806
  );
3440
3807
  }, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
3441
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
3808
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
3442
3809
  "section",
3443
3810
  {
3444
3811
  "aria-label": props.title ?? "Slide",
@@ -3446,8 +3813,8 @@ function Slide(props) {
3446
3813
  "data-testid": `slide-${props.blockId}`,
3447
3814
  hidden: props.hidden ? true : void 0,
3448
3815
  children: [
3449
- props.title ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }) : null,
3450
- /* @__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 }) })
3451
3818
  ]
3452
3819
  }
3453
3820
  );
@@ -3455,10 +3822,10 @@ function Slide(props) {
3455
3822
  setLessonkitBlockType(Slide, "Slide");
3456
3823
 
3457
3824
  // src/blocks/SlideDeck.tsx
3458
- var import_react30 = __toESM(require("react"), 1);
3825
+ var import_react31 = __toESM(require("react"), 1);
3459
3826
 
3460
3827
  // src/compound/useCompoundKeyboardNav.ts
3461
- var import_react29 = require("react");
3828
+ var import_react30 = require("react");
3462
3829
  var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
3463
3830
  function isEditableTarget(target) {
3464
3831
  if (!(target instanceof HTMLElement)) return false;
@@ -3471,7 +3838,7 @@ function isEditableTarget(target) {
3471
3838
  }
3472
3839
  function useCompoundKeyboardNav(opts) {
3473
3840
  const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
3474
- (0, import_react29.useEffect)(() => {
3841
+ (0, import_react30.useEffect)(() => {
3475
3842
  const el = containerRef.current;
3476
3843
  if (!el || pageCount === 0) return;
3477
3844
  const onKeyDown = (event) => {
@@ -3516,13 +3883,13 @@ function useCompoundKeyboardNav(opts) {
3516
3883
  }
3517
3884
 
3518
3885
  // src/blocks/SlideDeck.tsx
3519
- var import_jsx_runtime20 = require("react/jsx-runtime");
3520
- 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) {
3521
3888
  const { blockId, slides, index, setIndex, persistEnabled } = props;
3522
3889
  validateCompoundChildren("SlideDeck", slides);
3523
3890
  const { config, track } = useLessonkit();
3524
3891
  const lessonId = useEnclosingLessonId();
3525
- const containerRef = (0, import_react30.useRef)(null);
3892
+ const containerRef = (0, import_react31.useRef)(null);
3526
3893
  const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3527
3894
  courseId: config.courseId,
3528
3895
  compoundId: blockId,
@@ -3532,7 +3899,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3532
3899
  persistEnabled,
3533
3900
  ref
3534
3901
  });
3535
- const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), [setIndex]);
3902
+ const setIndexStable = (0, import_react31.useCallback)((i) => setIndex(i), [setIndex]);
3536
3903
  useCompoundKeyboardNav({
3537
3904
  containerRef,
3538
3905
  visibleIndex,
@@ -3541,11 +3908,11 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3541
3908
  goPrev,
3542
3909
  setIndex: setIndexStable
3543
3910
  });
3544
- const slideTitles = (0, import_react30.useMemo)(
3911
+ const slideTitles = (0, import_react31.useMemo)(
3545
3912
  () => slides.map((slide) => slide.props.title),
3546
3913
  [slides]
3547
3914
  );
3548
- (0, import_react30.useEffect)(() => {
3915
+ (0, import_react31.useEffect)(() => {
3549
3916
  if (!lessonId || slides.length === 0) return;
3550
3917
  track(
3551
3918
  "slide_viewed",
@@ -3557,7 +3924,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3557
3924
  { lessonId }
3558
3925
  );
3559
3926
  }, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
3560
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
3927
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3561
3928
  "section",
3562
3929
  {
3563
3930
  ref: containerRef,
@@ -3566,30 +3933,30 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3566
3933
  "data-testid": "slide-deck",
3567
3934
  "data-lk-block-id": blockId,
3568
3935
  children: [
3569
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }),
3570
- /* @__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: [
3571
3938
  "Slide ",
3572
3939
  progress.current,
3573
3940
  " of ",
3574
3941
  progress.total
3575
3942
  ] }),
3576
- 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: [
3577
3944
  "Score: ",
3578
3945
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3579
3946
  " /",
3580
3947
  " ",
3581
3948
  Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3582
3949
  ] }) : null,
3583
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
3584
- (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, {
3585
3952
  key: slide.key ?? slide.props.blockId,
3586
3953
  hidden: i !== visibleIndex,
3587
3954
  slideIndex: i,
3588
3955
  parentType: "SlideDeck"
3589
3956
  })
3590
3957
  ) }),
3591
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3592
- /* @__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)(
3593
3960
  "button",
3594
3961
  {
3595
3962
  type: "button",
@@ -3599,7 +3966,7 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3599
3966
  children: "Previous slide"
3600
3967
  }
3601
3968
  ),
3602
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3969
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3603
3970
  "button",
3604
3971
  {
3605
3972
  type: "button",
@@ -3614,13 +3981,13 @@ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(pro
3614
3981
  }
3615
3982
  );
3616
3983
  });
3617
- var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3618
- const blockId = (0, import_react30.useMemo)(
3984
+ var SlideDeck = (0, import_react31.forwardRef)(function SlideDeck2(props, ref) {
3985
+ const blockId = (0, import_react31.useMemo)(
3619
3986
  () => normalizeComponentId(props.blockId, "blockId"),
3620
3987
  [props.blockId]
3621
3988
  );
3622
- const slides = import_react30.default.Children.toArray(props.children).filter(
3623
- import_react30.default.isValidElement
3989
+ const slides = import_react31.default.Children.toArray(props.children).filter(
3990
+ import_react31.default.isValidElement
3624
3991
  );
3625
3992
  const { config, storage } = useLessonkit();
3626
3993
  const persistEnabled = config.session?.persistCompoundState !== false;
@@ -3631,12 +3998,12 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3631
3998
  persistEnabled,
3632
3999
  storage
3633
4000
  });
3634
- const [index, setIndex] = (0, import_react30.useState)(initialIndex);
3635
- const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), []);
3636
- (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)(() => {
3637
4004
  setIndex(initialIndex);
3638
4005
  }, [config.courseId, blockId, initialIndex]);
3639
- 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)(
3640
4007
  SlideDeckInner,
3641
4008
  {
3642
4009
  ...props,
@@ -3651,72 +4018,1675 @@ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3651
4018
  });
3652
4019
  setLessonkitBlockType(SlideDeck, "SlideDeck");
3653
4020
 
3654
- // src/blocks/Accordion.tsx
3655
- var import_react31 = require("react");
3656
- var import_jsx_runtime21 = require("react/jsx-runtime");
3657
- function Accordion(props) {
3658
- if (isDevEnvironment4()) {
3659
- validateAccordionSections(props.sections);
3660
- }
3661
- const [open, setOpen] = (0, import_react31.useState)(/* @__PURE__ */ new Set());
3662
- const { track } = useLessonkit();
3663
- const lessonId = useEnclosingLessonId();
3664
- const baseId = (0, import_react31.useId)();
3665
- const toggle = (sectionId) => {
3666
- setOpen((prev) => {
3667
- const next = new Set(prev);
3668
- const expanded = !next.has(sectionId);
3669
- if (expanded) next.add(sectionId);
3670
- else next.delete(sectionId);
3671
- track(
3672
- "accordion_section_toggled",
3673
- { blockId: props.blockId, sectionId, expanded },
3674
- lessonId ? { lessonId } : void 0
3675
- );
3676
- return next;
3677
- });
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
+ }
3678
4089
  };
3679
- 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) => {
3680
- const expanded = open.has(section.id);
3681
- const panelId = `${baseId}-${section.id}`;
3682
- const triggerId = `${baseId}-trigger-${section.id}`;
3683
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
3684
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3685
- "button",
3686
- {
3687
- id: triggerId,
3688
- type: "button",
3689
- "aria-expanded": expanded,
3690
- "aria-controls": panelId,
3691
- "data-testid": `accordion-trigger-${section.id}`,
3692
- onClick: () => toggle(section.id),
3693
- children: section.title
3694
- }
3695
- ) }),
3696
- expanded ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3697
- ] }, section.id);
3698
- }) });
3699
4090
  }
3700
- setLessonkitBlockType(Accordion, "Accordion");
3701
4091
 
3702
- // src/blocks/DialogCards.tsx
3703
- var import_react32 = require("react");
3704
- var import_jsx_runtime22 = require("react/jsx-runtime");
3705
- function DialogCards(props) {
3706
- const [index, setIndex] = (0, import_react32.useState)(0);
3707
- const [flipped, setFlipped] = (0, import_react32.useState)(false);
3708
- const card = props.cards[index];
3709
- if (!card) return null;
3710
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3711
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { children: [
3712
- "Card ",
3713
- index + 1,
3714
- " of ",
3715
- props.cards.length
3716
- ] }),
3717
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3718
- "button",
3719
- {
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();
4115
+ const lessonId = useEnclosingLessonId();
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) {
4250
+ track(
4251
+ "video_segment_completed",
4252
+ {
4253
+ blockId,
4254
+ segmentIndex: index,
4255
+ atSeconds: cue.props.atSeconds ?? 0,
4256
+ segmentLabel: cue.props.label
4257
+ },
4258
+ { lessonId }
4259
+ );
4260
+ }
4261
+ videoRef.current?.play().catch(() => {
4262
+ });
4263
+ };
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",
4275
+ {
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
4291
+ }
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)(
4304
+ "button",
4305
+ {
4306
+ type: "button",
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
+ {
3720
5690
  type: "button",
3721
5691
  "data-testid": "dialog-card-flip",
3722
5692
  "aria-pressed": flipped,
@@ -3725,8 +5695,8 @@ function DialogCards(props) {
3725
5695
  children: flipped ? card.back : card.front
3726
5696
  }
3727
5697
  ),
3728
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("nav", { "aria-label": "Card navigation", children: [
3729
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5698
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("nav", { "aria-label": "Card navigation", children: [
5699
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3730
5700
  "button",
3731
5701
  {
3732
5702
  type: "button",
@@ -3739,7 +5709,7 @@ function DialogCards(props) {
3739
5709
  children: "Previous"
3740
5710
  }
3741
5711
  ),
3742
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5712
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
3743
5713
  "button",
3744
5714
  {
3745
5715
  type: "button",
@@ -3758,11 +5728,11 @@ function DialogCards(props) {
3758
5728
  setLessonkitBlockType(DialogCards, "DialogCards");
3759
5729
 
3760
5730
  // src/blocks/Flashcards.tsx
3761
- var import_react33 = require("react");
3762
- var import_jsx_runtime23 = require("react/jsx-runtime");
5731
+ var import_react45 = require("react");
5732
+ var import_jsx_runtime35 = require("react/jsx-runtime");
3763
5733
  function Flashcards(props) {
3764
- const [index, setIndex] = (0, import_react33.useState)(0);
3765
- 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");
3766
5736
  const { track } = useLessonkit();
3767
5737
  const lessonId = useEnclosingLessonId();
3768
5738
  const card = props.cards[index];
@@ -3776,10 +5746,10 @@ function Flashcards(props) {
3776
5746
  lessonId ? { lessonId } : void 0
3777
5747
  );
3778
5748
  };
3779
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3780
- /* @__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 }),
3781
- props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3782
- /* @__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)(
3783
5753
  "button",
3784
5754
  {
3785
5755
  type: "button",
@@ -3797,10 +5767,10 @@ function Flashcards(props) {
3797
5767
  setLessonkitBlockType(Flashcards, "Flashcards");
3798
5768
 
3799
5769
  // src/blocks/ImageHotspots.tsx
3800
- var import_react34 = require("react");
3801
- var import_jsx_runtime24 = require("react/jsx-runtime");
5770
+ var import_react46 = require("react");
5771
+ var import_jsx_runtime36 = require("react/jsx-runtime");
3802
5772
  function ImageHotspots(props) {
3803
- const [active, setActive] = (0, import_react34.useState)(null);
5773
+ const [active, setActive] = (0, import_react46.useState)(null);
3804
5774
  const { track } = useLessonkit();
3805
5775
  const lessonId = useEnclosingLessonId();
3806
5776
  const open = (hotspotId) => {
@@ -3811,10 +5781,10 @@ function ImageHotspots(props) {
3811
5781
  lessonId ? { lessonId } : void 0
3812
5782
  );
3813
5783
  };
3814
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3815
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3816
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3817
- 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)(
3818
5788
  "button",
3819
5789
  {
3820
5790
  type: "button",
@@ -3833,19 +5803,19 @@ function ImageHotspots(props) {
3833
5803
  h.id
3834
5804
  ))
3835
5805
  ] }),
3836
- 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: [
3837
5807
  props.hotspots.find((h) => h.id === active)?.content,
3838
- /* @__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" })
3839
5809
  ] }) : null
3840
5810
  ] });
3841
5811
  }
3842
5812
  setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3843
5813
 
3844
5814
  // src/blocks/ImageSlider.tsx
3845
- var import_react35 = require("react");
3846
- var import_jsx_runtime25 = require("react/jsx-runtime");
5815
+ var import_react47 = require("react");
5816
+ var import_jsx_runtime37 = require("react/jsx-runtime");
3847
5817
  function ImageSlider(props) {
3848
- const [index, setIndex] = (0, import_react35.useState)(0);
5818
+ const [index, setIndex] = (0, import_react47.useState)(0);
3849
5819
  const { track } = useLessonkit();
3850
5820
  const lessonId = useEnclosingLessonId();
3851
5821
  const slide = props.slides[index];
@@ -3858,11 +5828,11 @@ function ImageSlider(props) {
3858
5828
  lessonId ? { lessonId } : void 0
3859
5829
  );
3860
5830
  };
3861
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
3862
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
3863
- slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: slide.caption }) : null,
3864
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3865
- /* @__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)(
3866
5836
  "button",
3867
5837
  {
3868
5838
  type: "button",
@@ -3872,12 +5842,12 @@ function ImageSlider(props) {
3872
5842
  children: "Previous"
3873
5843
  }
3874
5844
  ),
3875
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { children: [
5845
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("span", { children: [
3876
5846
  index + 1,
3877
5847
  " / ",
3878
5848
  props.slides.length
3879
5849
  ] }),
3880
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5850
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
3881
5851
  "button",
3882
5852
  {
3883
5853
  type: "button",
@@ -3893,21 +5863,42 @@ function ImageSlider(props) {
3893
5863
  setLessonkitBlockType(ImageSlider, "ImageSlider");
3894
5864
 
3895
5865
  // src/blocks/FindHotspot.tsx
3896
- var import_react36 = require("react");
3897
- var import_jsx_runtime26 = require("react/jsx-runtime");
3898
- var INTERACTION6 = "findHotspot";
5866
+ var import_react48 = require("react");
5867
+ var import_jsx_runtime38 = require("react/jsx-runtime");
5868
+ var INTERACTION11 = "findHotspot";
3899
5869
  function FindHotspotInner(props, ref) {
3900
- const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3901
- const [selected, setSelected] = (0, import_react36.useState)(null);
3902
- const [checked, setChecked] = (0, import_react36.useState)(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);
3903
5874
  const assessment = useAssessmentState(props.enclosingLessonId);
3904
5875
  const targetIdsKey = props.targets.map((t) => t.id).join("\0");
3905
- (0, import_react36.useEffect)(() => {
5876
+ (0, import_react48.useEffect)(() => {
3906
5877
  setSelected(null);
3907
5878
  setChecked(false);
5879
+ telemetryReplayedRef.current = false;
3908
5880
  }, [checkId, props.correctTargetId, targetIdsKey]);
3909
5881
  const correct = selected === props.correctTargetId;
3910
- const handle = (0, import_react36.useMemo)(
5882
+ const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
5883
+ if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
5884
+ telemetryReplayedRef.current = true;
5885
+ assessment.answer({
5886
+ checkId,
5887
+ interactionType: INTERACTION11,
5888
+ response: nextSelected,
5889
+ correct: nextCorrect
5890
+ });
5891
+ if (nextCorrect) {
5892
+ assessment.complete({
5893
+ checkId,
5894
+ interactionType: INTERACTION11,
5895
+ score: 1,
5896
+ maxScore: 1,
5897
+ passingScore: props.passingScore ?? 1
5898
+ });
5899
+ }
5900
+ };
5901
+ const handle = (0, import_react48.useMemo)(
3911
5902
  () => buildAssessmentHandle({
3912
5903
  checkId,
3913
5904
  getScore: () => checked && correct ? 1 : 0,
@@ -3916,11 +5907,12 @@ function FindHotspotInner(props, ref) {
3916
5907
  resetTask: () => {
3917
5908
  setSelected(null);
3918
5909
  setChecked(false);
5910
+ telemetryReplayedRef.current = false;
3919
5911
  },
3920
5912
  showSolutions: () => setSelected(props.correctTargetId),
3921
5913
  getXAPIData: () => ({
3922
5914
  checkId,
3923
- interactionType: INTERACTION6,
5915
+ interactionType: INTERACTION11,
3924
5916
  response: selected ?? void 0,
3925
5917
  correct: checked ? correct : void 0,
3926
5918
  score: checked && correct ? 1 : 0,
@@ -3928,15 +5920,23 @@ function FindHotspotInner(props, ref) {
3928
5920
  }),
3929
5921
  getCurrentState: () => ({ selected, checked }),
3930
5922
  resume: (state) => {
3931
- const nextSelected = readStringField(state, "selected");
3932
- if (typeof nextSelected === "string" || nextSelected === null) {
3933
- const valid = nextSelected === null || props.targets.some((t) => t.id === nextSelected);
3934
- setSelected(valid ? nextSelected : null);
5923
+ let nextSelected = selected;
5924
+ const rawSelected = readStringField(state, "selected");
5925
+ if (typeof rawSelected === "string" || rawSelected === null) {
5926
+ const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
5927
+ nextSelected = valid ? rawSelected : null;
5928
+ setSelected(nextSelected);
3935
5929
  }
3936
- readBooleanStateField(state, "checked", setChecked);
5930
+ let nextChecked = checked;
5931
+ readBooleanStateField(state, "checked", (value) => {
5932
+ nextChecked = value;
5933
+ setChecked(value);
5934
+ });
5935
+ const nextCorrect = nextSelected === props.correctTargetId;
5936
+ replayTelemetry(nextSelected, nextChecked, nextCorrect);
3937
5937
  }
3938
5938
  }),
3939
- [checkId, selected, checked, correct, props.correctTargetId, props.targets]
5939
+ [assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
3940
5940
  );
3941
5941
  useAssessmentHandleRegistration(checkId, handle, ref);
3942
5942
  const selectTarget = (id) => {
@@ -3948,24 +5948,24 @@ function FindHotspotInner(props, ref) {
3948
5948
  setChecked(true);
3949
5949
  assessment.answer({
3950
5950
  checkId,
3951
- interactionType: INTERACTION6,
5951
+ interactionType: INTERACTION11,
3952
5952
  response: selected,
3953
5953
  correct
3954
5954
  });
3955
5955
  if (correct) {
3956
5956
  assessment.complete({
3957
5957
  checkId,
3958
- interactionType: INTERACTION6,
5958
+ interactionType: INTERACTION11,
3959
5959
  score: 1,
3960
5960
  maxScore: 1,
3961
5961
  passingScore: props.passingScore ?? 1
3962
5962
  });
3963
5963
  }
3964
5964
  };
3965
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
3966
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3967
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3968
- 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)(
3969
5969
  "button",
3970
5970
  {
3971
5971
  type: "button",
@@ -3984,24 +5984,24 @@ function FindHotspotInner(props, ref) {
3984
5984
  t.id
3985
5985
  ))
3986
5986
  ] }),
3987
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
3988
- 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
3989
5989
  ] });
3990
5990
  }
3991
- var FindHotspotInnerForwarded = (0, import_react36.forwardRef)(FindHotspotInner);
3992
- var FindHotspot = (0, import_react36.forwardRef)(function FindHotspot2(props, ref) {
3993
- 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 }) });
3994
5994
  });
3995
5995
  setLessonkitBlockType(FindHotspot, "FindHotspot");
3996
5996
 
3997
5997
  // src/blocks/FindMultipleHotspots.tsx
3998
- var import_react37 = require("react");
3999
- var import_jsx_runtime27 = require("react/jsx-runtime");
4000
- var INTERACTION7 = "findMultipleHotspots";
5998
+ var import_react49 = require("react");
5999
+ var import_jsx_runtime39 = require("react/jsx-runtime");
6000
+ var INTERACTION12 = "findMultipleHotspots";
4001
6001
  function FindMultipleHotspotsInner(props, ref) {
4002
- const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4003
- const [selected, setSelected] = (0, import_react37.useState)(/* @__PURE__ */ new Set());
4004
- 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);
4005
6005
  const assessment = useAssessmentState(props.enclosingLessonId);
4006
6006
  const toggle = (id) => {
4007
6007
  setSelected((prev) => {
@@ -4013,7 +6013,7 @@ function FindMultipleHotspotsInner(props, ref) {
4013
6013
  setChecked(false);
4014
6014
  };
4015
6015
  const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
4016
- const handle = (0, import_react37.useMemo)(
6016
+ const handle = (0, import_react49.useMemo)(
4017
6017
  () => buildAssessmentHandle({
4018
6018
  checkId,
4019
6019
  getScore: () => checked && correct ? 1 : 0,
@@ -4026,7 +6026,7 @@ function FindMultipleHotspotsInner(props, ref) {
4026
6026
  showSolutions: () => setSelected(new Set(props.correctTargetIds)),
4027
6027
  getXAPIData: () => ({
4028
6028
  checkId,
4029
- interactionType: INTERACTION7,
6029
+ interactionType: INTERACTION12,
4030
6030
  response: [...selected],
4031
6031
  correct: checked ? correct : void 0,
4032
6032
  score: checked && correct ? 1 : 0,
@@ -4047,24 +6047,24 @@ function FindMultipleHotspotsInner(props, ref) {
4047
6047
  setChecked(true);
4048
6048
  assessment.answer({
4049
6049
  checkId,
4050
- interactionType: INTERACTION7,
6050
+ interactionType: INTERACTION12,
4051
6051
  response: [...selected],
4052
6052
  correct
4053
6053
  });
4054
6054
  if (correct) {
4055
6055
  assessment.complete({
4056
6056
  checkId,
4057
- interactionType: INTERACTION7,
6057
+ interactionType: INTERACTION12,
4058
6058
  score: 1,
4059
6059
  maxScore: 1,
4060
6060
  passingScore: props.passingScore ?? 1
4061
6061
  });
4062
6062
  }
4063
6063
  };
4064
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
4065
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4066
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4067
- 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)(
4068
6068
  "button",
4069
6069
  {
4070
6070
  type: "button",
@@ -4083,23 +6083,23 @@ function FindMultipleHotspotsInner(props, ref) {
4083
6083
  t.id
4084
6084
  ))
4085
6085
  ] }),
4086
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
4087
- 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
4088
6088
  ] });
4089
6089
  }
4090
- var FindMultipleHotspotsInnerForwarded = (0, import_react37.forwardRef)(FindMultipleHotspotsInner);
4091
- var FindMultipleHotspots = (0, import_react37.forwardRef)(
6090
+ var FindMultipleHotspotsInnerForwarded = (0, import_react49.forwardRef)(FindMultipleHotspotsInner);
6091
+ var FindMultipleHotspots = (0, import_react49.forwardRef)(
4092
6092
  function FindMultipleHotspots2(props, ref) {
4093
- 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 }) });
4094
6094
  }
4095
6095
  );
4096
6096
  setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
4097
6097
 
4098
6098
  // src/index.tsx
4099
- var import_core19 = require("@lessonkit/core");
6099
+ var import_core21 = require("@lessonkit/core");
4100
6100
 
4101
6101
  // src/theme/ThemeProvider.tsx
4102
- var import_react38 = __toESM(require("react"), 1);
6102
+ var import_react50 = __toESM(require("react"), 1);
4103
6103
  var import_themes = require("@lessonkit/themes");
4104
6104
 
4105
6105
  // src/theme/applyCssVariables.ts
@@ -4118,11 +6118,11 @@ function applyCssVariables(target, vars, previousKeys) {
4118
6118
  }
4119
6119
 
4120
6120
  // src/theme/ThemeProvider.tsx
4121
- var import_jsx_runtime28 = require("react/jsx-runtime");
4122
- var ThemeContext = (0, import_react38.createContext)(null);
6121
+ var import_jsx_runtime40 = require("react/jsx-runtime");
6122
+ var ThemeContext = (0, import_react50.createContext)(null);
4123
6123
  var useIsoLayoutEffect2 = (
4124
6124
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
4125
- typeof window !== "undefined" ? import_react38.useLayoutEffect : import_react38.default.useEffect
6125
+ typeof window !== "undefined" ? import_react50.useLayoutEffect : import_react50.default.useEffect
4126
6126
  );
4127
6127
  function getSystemMode() {
4128
6128
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -4141,7 +6141,7 @@ function ThemeProvider(props) {
4141
6141
  const preset = props.preset ?? "default";
4142
6142
  const mode = props.mode ?? "light";
4143
6143
  const targetKind = props.target ?? "document";
4144
- const [resolvedMode, setResolvedMode] = (0, import_react38.useState)(
6144
+ const [resolvedMode, setResolvedMode] = (0, import_react50.useState)(
4145
6145
  () => mode === "system" ? getSystemMode() : mode
4146
6146
  );
4147
6147
  useIsoLayoutEffect2(() => {
@@ -4157,20 +6157,20 @@ function ThemeProvider(props) {
4157
6157
  return () => mq.removeEventListener("change", onChange);
4158
6158
  }, [mode]);
4159
6159
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
4160
- const effectiveTheme = (0, import_react38.useMemo)(() => {
6160
+ const effectiveTheme = (0, import_react50.useMemo)(() => {
4161
6161
  const modeBase = resolveModeBase(mode, dataTheme);
4162
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));
4163
6163
  return (0, import_themes.mergeThemes)(base, props.theme ?? {});
4164
6164
  }, [preset, mode, dataTheme, props.theme]);
4165
- const hostRef = (0, import_react38.useRef)(null);
4166
- 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());
4167
6167
  useIsoLayoutEffect2(() => {
4168
6168
  if (targetKind === "document" && typeof document !== "undefined") {
4169
6169
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
4170
6170
  return () => document.documentElement.removeAttribute("data-lk-theme");
4171
6171
  }
4172
6172
  }, [targetKind, dataTheme]);
4173
- const inject = (0, import_react38.useCallback)(() => {
6173
+ const inject = (0, import_react50.useCallback)(() => {
4174
6174
  const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
4175
6175
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
4176
6176
  if (!el) return;
@@ -4187,7 +6187,7 @@ function ThemeProvider(props) {
4187
6187
  appliedKeysRef.current = /* @__PURE__ */ new Set();
4188
6188
  };
4189
6189
  }, [inject, targetKind]);
4190
- const value = (0, import_react38.useMemo)(
6190
+ const value = (0, import_react50.useMemo)(
4191
6191
  () => ({
4192
6192
  theme: effectiveTheme,
4193
6193
  preset,
@@ -4197,12 +6197,12 @@ function ThemeProvider(props) {
4197
6197
  [effectiveTheme, preset, mode, dataTheme]
4198
6198
  );
4199
6199
  if (targetKind === "document") {
4200
- 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 }) });
4201
6201
  }
4202
- 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 }) });
4203
6203
  }
4204
6204
  function useTheme() {
4205
- const ctx = (0, import_react38.useContext)(ThemeContext);
6205
+ const ctx = (0, import_react50.useContext)(ThemeContext);
4206
6206
  if (!ctx) {
4207
6207
  throw new Error("useTheme must be used within a ThemeProvider");
4208
6208
  }
@@ -4210,13 +6210,15 @@ function useTheme() {
4210
6210
  }
4211
6211
 
4212
6212
  // src/catalogV3Entries.ts
4213
- var import_core18 = require("@lessonkit/core");
6213
+ var import_core20 = require("@lessonkit/core");
4214
6214
  var COMPOUND_PARENTS = [
4215
6215
  "Lesson",
4216
6216
  "Page",
4217
6217
  "InteractiveBook",
4218
6218
  "Slide",
4219
6219
  "SlideDeck",
6220
+ "TimedCue",
6221
+ "InteractiveVideo",
4220
6222
  "AssessmentSequence"
4221
6223
  ];
4222
6224
  function extendParents(entry) {
@@ -4275,6 +6277,23 @@ var v3CompoundAndContentEntries = [
4275
6277
  theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
4276
6278
  telemetry: { emits: [] }
4277
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
+ },
4278
6297
  {
4279
6298
  type: "Page",
4280
6299
  category: "container",
@@ -4282,8 +6301,8 @@ var v3CompoundAndContentEntries = [
4282
6301
  h5pMachineName: "H5P.Column",
4283
6302
  h5pAlias: "Column",
4284
6303
  description: "Column layout container (H5P Column / Page).",
4285
- allowedChildTypes: [...import_core18.PAGE_ALLOWED_CHILD_TYPES],
4286
- 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,
4287
6306
  props: [
4288
6307
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4289
6308
  { name: "title", type: "string", required: false, description: "Page title." },
@@ -4303,8 +6322,8 @@ var v3CompoundAndContentEntries = [
4303
6322
  h5pMachineName: "H5P.InteractiveBook",
4304
6323
  h5pAlias: "Interactive Book",
4305
6324
  description: "Multi-page book with chapter navigation.",
4306
- allowedChildTypes: [...import_core18.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
4307
- 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,
4308
6327
  props: [
4309
6328
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4310
6329
  { name: "title", type: "string", required: true, description: "Book title." },
@@ -4328,9 +6347,9 @@ var v3CompoundAndContentEntries = [
4328
6347
  compoundContract: true,
4329
6348
  h5pMachineName: "H5P.CoursePresentation",
4330
6349
  h5pAlias: "Course Presentation slide",
4331
- description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
4332
- allowedChildTypes: [...import_core18.SLIDE_ALLOWED_CHILD_TYPES],
4333
- 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,
4334
6353
  props: [
4335
6354
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4336
6355
  { name: "title", type: "string", required: false, description: "Slide title." },
@@ -4350,8 +6369,8 @@ var v3CompoundAndContentEntries = [
4350
6369
  h5pMachineName: "H5P.CoursePresentation",
4351
6370
  h5pAlias: "Course Presentation",
4352
6371
  description: "Multi-slide presentation with keyboard navigation.",
4353
- allowedChildTypes: [...import_core18.SLIDE_DECK_ALLOWED_CHILD_TYPES],
4354
- 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,
4355
6374
  props: [
4356
6375
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4357
6376
  { name: "title", type: "string", required: true, description: "Deck title." },
@@ -4369,6 +6388,121 @@ var v3CompoundAndContentEntries = [
4369
6388
  theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
4370
6389
  telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
4371
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
+ },
4372
6506
  {
4373
6507
  type: "Accordion",
4374
6508
  category: "content",
@@ -4502,8 +6636,8 @@ function buildV3CatalogFromV2(v2) {
4502
6636
  return {
4503
6637
  ...base,
4504
6638
  compoundContract: true,
4505
- allowedChildTypes: [...import_core18.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
4506
- 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
4507
6641
  };
4508
6642
  }
4509
6643
  return base;
@@ -4850,6 +6984,100 @@ var v2AssessmentEntries = [
4850
6984
  },
4851
6985
  theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
4852
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 }
4853
7081
  }
4854
7082
  ];
4855
7083
  var BLOCK_CATALOG_V2 = [
@@ -4889,6 +7117,7 @@ function getBlockCatalogEntry(type, opts) {
4889
7117
  // Annotate the CommonJS export names for ESM import in node:
4890
7118
  0 && (module.exports = {
4891
7119
  Accordion,
7120
+ ArithmeticQuiz,
4892
7121
  AssessmentSequence,
4893
7122
  BLOCK_CATALOG,
4894
7123
  BLOCK_CATALOG_V2,
@@ -4897,6 +7126,7 @@ function getBlockCatalogEntry(type, opts) {
4897
7126
  DialogCards,
4898
7127
  DragAndDrop,
4899
7128
  DragTheWords,
7129
+ Essay,
4900
7130
  FillInTheBlanks,
4901
7131
  FindHotspot,
4902
7132
  FindMultipleHotspots,
@@ -4904,22 +7134,33 @@ function getBlockCatalogEntry(type, opts) {
4904
7134
  Heading,
4905
7135
  Image,
4906
7136
  ImageHotspots,
7137
+ ImagePairing,
7138
+ ImageSequencing,
4907
7139
  ImageSlider,
7140
+ InformationWall,
4908
7141
  InteractiveBook,
7142
+ InteractiveVideo,
4909
7143
  KnowledgeCheck,
4910
7144
  Lesson,
4911
7145
  LessonkitProvider,
4912
7146
  MarkTheWords,
7147
+ MemoryGame,
4913
7148
  Page,
7149
+ ParallaxSlideshow,
4914
7150
  ProgressTracker,
7151
+ Questionnaire,
4915
7152
  Quiz,
4916
7153
  Reflection,
4917
7154
  Scenario,
4918
7155
  Slide,
4919
7156
  SlideDeck,
7157
+ Summary,
4920
7158
  Text,
4921
7159
  ThemeProvider,
7160
+ TimedCue,
4922
7161
  TrueFalse,
7162
+ Video,
7163
+ assertProductionCourseConfig,
4923
7164
  blockCatalogV2Version,
4924
7165
  blockCatalogV3Version,
4925
7166
  blockCatalogVersion,
@@ -4934,6 +7175,7 @@ function getBlockCatalogEntry(type, opts) {
4934
7175
  getBlockCatalogEntry,
4935
7176
  resetAssessmentWarningsForTests,
4936
7177
  resetQuizWarningsForTests,
7178
+ shouldEnforceProductionGuard,
4937
7179
  useAssessmentState,
4938
7180
  useCompletion,
4939
7181
  useLessonkit,