@lessonkit/react 1.0.2 → 1.2.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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/components.tsx
2
- import { useEffect as useEffect2, useId, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
3
- import { visuallyHiddenStyle } from "@lessonkit/accessibility";
2
+ import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
3
+ import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
4
4
 
5
5
  // src/context.tsx
6
6
  import { createContext } from "react";
@@ -15,7 +15,39 @@ import {
15
15
  useState
16
16
  } from "react";
17
17
  import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
18
+
19
+ // src/runtime/observability.ts
18
20
  import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
21
+ function createXapiQueueFromObservability(observability) {
22
+ const opts = {};
23
+ if (observability?.onXapiQueueDepth) {
24
+ opts.onDepth = observability.onXapiQueueDepth;
25
+ }
26
+ if (observability?.onXapiQueueCap) {
27
+ opts.onCap = observability.onXapiQueueCap;
28
+ }
29
+ return createInMemoryXAPIQueue(opts);
30
+ }
31
+ function wrapTrackingSink(sink, observability) {
32
+ if (!sink || !observability?.onTelemetrySinkError) return sink;
33
+ const onError = observability.onTelemetrySinkError;
34
+ return ((event) => {
35
+ try {
36
+ const result = sink(event);
37
+ if (result != null && typeof result.catch === "function") {
38
+ return result.catch((err) => {
39
+ onError(err, { sinkId: "tracking" });
40
+ });
41
+ }
42
+ return result;
43
+ } catch (err) {
44
+ onError(err, { sinkId: "tracking" });
45
+ return void 0;
46
+ }
47
+ });
48
+ }
49
+
50
+ // src/provider/useLessonkitProviderRuntime.ts
19
51
  import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
20
52
 
21
53
  // src/runtime/emitTelemetry.ts
@@ -36,7 +68,16 @@ import {
36
68
  mapLessonkitTelemetryToBridgeAction,
37
69
  telemetryEventToLessonkit
38
70
  } from "@lessonkit/lxpack/bridge";
39
- function forwardTelemetryToLxpack(event, mode = "auto") {
71
+ var BRIDGE_MISS_EVENT_NAMES = /* @__PURE__ */ new Set([
72
+ "course_completed",
73
+ "lesson_completed",
74
+ "assessment_completed",
75
+ "quiz_completed"
76
+ ]);
77
+ function forwardTelemetryToLxpack(event, mode = "auto", opts) {
78
+ if (mode === "auto" && opts?.onBridgeMiss && BRIDGE_MISS_EVENT_NAMES.has(event.name) && !getLxpackBridge()) {
79
+ opts.onBridgeMiss(event);
80
+ }
40
81
  forwardTelemetryToBridge(event, mode);
41
82
  }
42
83
 
@@ -67,7 +108,9 @@ function createLegacyPipeline(opts, extraSinks = []) {
67
108
  {
68
109
  id: "lxpack-bridge",
69
110
  emit(event) {
70
- forwardTelemetryToLxpack(event, opts.lxpackBridge);
111
+ forwardTelemetryToLxpack(event, opts.lxpackBridge, {
112
+ onBridgeMiss: opts.onLxpackBridgeMiss
113
+ });
71
114
  }
72
115
  },
73
116
  ...extraSinks
@@ -94,7 +137,8 @@ function emitTelemetry(tracking, xapi, event, opts) {
94
137
  const legacy = {
95
138
  tracking,
96
139
  xapi,
97
- lxpackBridge: opts?.lxpackBridge ?? "auto"
140
+ lxpackBridge: opts?.lxpackBridge ?? "auto",
141
+ onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
98
142
  };
99
143
  emitThroughPipeline(event, legacy, opts?.extraSinks);
100
144
  }
@@ -184,7 +228,9 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
184
228
  xapiStatementSent = true;
185
229
  }
186
230
  }
187
- forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
231
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
232
+ onBridgeMiss: opts.onLxpackBridgeMiss
233
+ });
188
234
  const emitCtx = {
189
235
  courseId: opts.event.courseId,
190
236
  sessionId: opts.event.sessionId,
@@ -195,53 +241,25 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
195
241
  }
196
242
 
197
243
  // src/runtime/plugins.ts
198
- import { createPluginRegistry } from "@lessonkit/core";
244
+ import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
199
245
  function createReactPluginHost(plugins) {
200
246
  if (!plugins?.length) return null;
201
247
  return createPluginRegistry(plugins);
202
248
  }
203
249
  function buildPluginContext(opts) {
204
- return {
205
- courseId: opts.courseId,
206
- sessionId: opts.sessionId,
207
- attemptId: opts.attemptId,
208
- user: opts.user
209
- };
250
+ return buildPluginContextFromCore(opts);
210
251
  }
211
252
  function emitTelemetryWithPlugins(opts) {
212
253
  const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
213
254
  if (next === null) return;
214
255
  emitTelemetry(opts.tracking, opts.xapi, next, {
215
256
  lxpackBridge: opts.lxpackBridge ?? "auto",
216
- extraSinks: opts.extraSinks
217
- });
218
- }
219
-
220
- // src/runtime/telemetry.ts
221
- import { createTrackingClient } from "@lessonkit/core";
222
- function createTrackingClientFromConfig(config) {
223
- if (config.tracking?.enabled === false) return createTrackingClient();
224
- if (config.tracking?.createClient) return config.tracking.createClient();
225
- return createTrackingClient({
226
- sink: config.tracking?.sink,
227
- batchSink: config.tracking?.batchSink,
228
- batch: config.tracking?.batch
257
+ extraSinks: opts.extraSinks,
258
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss
229
259
  });
230
260
  }
231
- async function disposeTrackingClient(client) {
232
- try {
233
- await client?.flush?.();
234
- } catch {
235
- }
236
- try {
237
- await client?.dispose?.();
238
- } catch {
239
- }
240
- }
241
261
 
242
- // src/provider/useLessonkitProviderRuntime.ts
243
- var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
244
- var defaultStorage = createSessionStoragePort();
262
+ // src/provider/courseStarted/emit.ts
245
263
  var courseStartedTrackingFlightKey = null;
246
264
  function isTrackingActive(tracking) {
247
265
  return tracking?.enabled !== false;
@@ -296,6 +314,7 @@ async function emitCourseStartedPipelineOnly(opts) {
296
314
  event: opts.event,
297
315
  xapi: opts.xapi,
298
316
  lxpackBridge: opts.lxpackBridge,
317
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
299
318
  extraSinks: opts.extraSinks,
300
319
  skipXapi: opts.skipXapi
301
320
  });
@@ -313,22 +332,15 @@ async function emitCourseStartedPipelineOnly(opts) {
313
332
  async function emitCourseStarted(opts) {
314
333
  const event = buildCourseStartedEvent(opts);
315
334
  if (event === null) return "filtered";
316
- const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
335
+ const tracked = await emitCourseStartedToTracking(
336
+ opts.tracking,
317
337
  opts.storage,
318
338
  opts.sessionId,
319
- opts.courseId
339
+ opts.courseId,
340
+ event,
341
+ opts.shouldCommit
320
342
  );
321
- if (!trackingAlreadyEmitted) {
322
- const tracked = await emitCourseStartedToTracking(
323
- opts.tracking,
324
- opts.storage,
325
- opts.sessionId,
326
- opts.courseId,
327
- event,
328
- opts.shouldCommit
329
- );
330
- if (!tracked) return "failed";
331
- }
343
+ if (!tracked) return "failed";
332
344
  return emitCourseStartedPipelineOnly({
333
345
  ...opts,
334
346
  event,
@@ -340,28 +352,22 @@ async function emitCourseStarted(opts) {
340
352
  async function emitCourseStartedToTrackingOnly(opts) {
341
353
  const event = buildCourseStartedEvent(opts);
342
354
  if (event === null) return "filtered";
343
- const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
355
+ const tracked = await emitCourseStartedToTracking(
356
+ opts.tracking,
344
357
  opts.storage,
345
358
  opts.sessionId,
346
- opts.courseId
359
+ opts.courseId,
360
+ event,
361
+ opts.shouldCommit
347
362
  );
348
- if (!trackingAlreadyEmitted) {
349
- const tracked = await emitCourseStartedToTracking(
350
- opts.tracking,
351
- opts.storage,
352
- opts.sessionId,
353
- opts.courseId,
354
- event,
355
- opts.shouldCommit
356
- );
357
- if (!tracked) return "failed";
358
- }
363
+ if (!tracked) return "failed";
359
364
  try {
360
365
  if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
361
366
  await emitCourseStartedNonTrackingPipeline({
362
367
  event,
363
368
  xapi: null,
364
369
  lxpackBridge: opts.lxpackBridge,
370
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
365
371
  extraSinks: opts.extraSinks,
366
372
  skipXapi: true
367
373
  });
@@ -394,6 +400,9 @@ async function emitPendingCourseStarted(opts) {
394
400
  opts.sessionId,
395
401
  opts.courseId
396
402
  );
403
+ if (sessionStarted && trackingEmitted && pipelineDelivered) {
404
+ return "emitted";
405
+ }
397
406
  if (sessionStarted && trackingEmitted && !pipelineDelivered) {
398
407
  const event = buildCourseStartedEvent(opts);
399
408
  if (event === null) return "filtered";
@@ -412,6 +421,35 @@ function assertTrackingSinkConfig(tracking) {
412
421
  "[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
413
422
  );
414
423
  }
424
+
425
+ // src/runtime/telemetry.ts
426
+ import { createTrackingClient } from "@lessonkit/core";
427
+ function createTrackingClientFromConfig(config) {
428
+ if (config.tracking?.enabled === false) return createTrackingClient();
429
+ if (config.tracking?.createClient) return config.tracking.createClient();
430
+ return createTrackingClient({
431
+ sink: config.tracking?.sink,
432
+ batchSink: config.tracking?.batchSink,
433
+ batch: config.tracking?.batch
434
+ });
435
+ }
436
+ async function disposeTrackingClient(client) {
437
+ try {
438
+ await client?.flush?.();
439
+ } catch {
440
+ }
441
+ try {
442
+ await client?.dispose?.();
443
+ } catch {
444
+ }
445
+ }
446
+
447
+ // src/provider/useLessonkitProviderRuntime.ts
448
+ var useIsoLayoutEffect = (
449
+ /* v8 ignore next -- SSR uses useEffect when window is unavailable */
450
+ typeof window !== "undefined" ? useLayoutEffect : useEffect
451
+ );
452
+ var defaultStorage = createSessionStoragePort();
415
453
  function useLessonkitProviderRuntime(config) {
416
454
  const normalizedCourseId = useMemo(
417
455
  () => assertValidId(config.courseId, "courseId"),
@@ -422,6 +460,14 @@ function useLessonkitProviderRuntime(config) {
422
460
  [config, normalizedCourseId]
423
461
  );
424
462
  const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
463
+ useEffect(() => {
464
+ if (useV2Runtime) return;
465
+ const g = globalThis;
466
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
467
+ console.warn(
468
+ '[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
469
+ );
470
+ }, [useV2Runtime]);
425
471
  const extraSinksRef = useRef(normalizedConfig.sinks);
426
472
  extraSinksRef.current = normalizedConfig.sinks;
427
473
  const headlessRef = useRef(null);
@@ -440,7 +486,16 @@ function useLessonkitProviderRuntime(config) {
440
486
  courseIdRef.current = normalizedCourseId;
441
487
  const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
442
488
  lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
443
- const pluginHost = useMemo(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
489
+ const observabilityRef = useRef(normalizedConfig.observability);
490
+ observabilityRef.current = normalizedConfig.observability;
491
+ const onLxpackBridgeMiss = useCallback((event) => {
492
+ observabilityRef.current?.onLxpackBridgeMiss?.(event);
493
+ }, []);
494
+ const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
495
+ const pluginHost = useMemo(
496
+ () => createReactPluginHost(normalizedConfig.plugins),
497
+ [pluginsFingerprint]
498
+ );
444
499
  const pluginHostRef = useRef(pluginHost);
445
500
  pluginHostRef.current = pluginHost;
446
501
  const progressRef = useRef(createProgressController());
@@ -456,7 +511,8 @@ function useLessonkitProviderRuntime(config) {
456
511
  headlessRef.current = createLessonkitRuntime({
457
512
  courseId: normalizedCourseId,
458
513
  runtimeVersion: "v2",
459
- session: normalizedConfig.session
514
+ session: normalizedConfig.session,
515
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
460
516
  });
461
517
  progressRef.current = headlessRef.current.progress;
462
518
  } else {
@@ -470,7 +526,8 @@ function useLessonkitProviderRuntime(config) {
470
526
  headlessRef.current = createLessonkitRuntime({
471
527
  courseId: normalizedCourseId,
472
528
  runtimeVersion: "v2",
473
- session: normalizedConfig.session
529
+ session: normalizedConfig.session,
530
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
474
531
  });
475
532
  }
476
533
  if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
@@ -494,7 +551,7 @@ function useLessonkitProviderRuntime(config) {
494
551
  }, []);
495
552
  const activeLessonIdRef = useRef(progress.activeLessonId);
496
553
  activeLessonIdRef.current = progress.activeLessonId;
497
- const xapiQueueRef = useRef(createInMemoryXAPIQueue());
554
+ const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
498
555
  const xapiRef = useRef(null);
499
556
  const [xapi, setXapi] = useState(null);
500
557
  const prevXapiCourseIdRef = useRef(normalizedCourseId);
@@ -515,7 +572,7 @@ function useLessonkitProviderRuntime(config) {
515
572
  }
516
573
  void xapiRef.current?.flush();
517
574
  }
518
- xapiQueueRef.current = createInMemoryXAPIQueue();
575
+ xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
519
576
  prevXapiCourseIdRef.current = courseId;
520
577
  xapiCourseStartedSentOnClientRef.current = false;
521
578
  }
@@ -594,10 +651,13 @@ function useLessonkitProviderRuntime(config) {
594
651
  );
595
652
  useIsoLayoutEffect(() => {
596
653
  const prev = trackingRef.current;
597
- const baseSink = normalizedConfig.tracking?.sink;
654
+ const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
598
655
  const userBatchSink = normalizedConfig.tracking?.batchSink;
599
656
  assertTrackingSinkConfig(normalizedConfig.tracking);
600
- const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
657
+ const sink = pluginHostRef.current && baseSink ? (
658
+ /* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
659
+ pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
660
+ ) : baseSink;
601
661
  const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
602
662
  const host = pluginHostRef.current;
603
663
  const ctx = buildCurrentPluginCtx();
@@ -641,6 +701,7 @@ function useLessonkitProviderRuntime(config) {
641
701
  attemptId: attemptIdRef.current,
642
702
  user: userRef.current,
643
703
  lxpackBridge: lxpackBridgeModeRef.current,
704
+ onLxpackBridgeMiss,
644
705
  extraSinks: extraSinksRef.current,
645
706
  skipXapi: xapiCourseStartedSentOnClientRef.current,
646
707
  onXapiStatementSent: () => {
@@ -682,9 +743,10 @@ function useLessonkitProviderRuntime(config) {
682
743
  user: userRef.current
683
744
  }),
684
745
  lxpackBridge: lxpackBridgeModeRef.current,
746
+ onLxpackBridgeMiss,
685
747
  extraSinks: extraSinksRef.current
686
748
  });
687
- }, []);
749
+ }, [onLxpackBridgeMiss]);
688
750
  const emitLifecycleEvent = useCallback(
689
751
  (name, data, lessonId) => {
690
752
  const event = tryBuildTelemetryEvent({
@@ -740,12 +802,13 @@ function useLessonkitProviderRuntime(config) {
740
802
  attemptId: attemptIdRef.current,
741
803
  user: userRef.current,
742
804
  lxpackBridge: lxpackBridgeModeRef.current,
805
+ onLxpackBridgeMiss,
743
806
  extraSinks: extraSinksRef.current
744
807
  });
745
808
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
746
809
  }
747
810
  })();
748
- }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
811
+ }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
749
812
  const emitLessonCompleted = useCallback(
750
813
  (lessonId, durationMs) => {
751
814
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -794,6 +857,22 @@ function useLessonkitProviderRuntime(config) {
794
857
  })();
795
858
  };
796
859
  }, []);
860
+ useEffect(() => {
861
+ if (typeof document === "undefined") return;
862
+ const flushOnExit = () => {
863
+ void xapiRef.current?.flush();
864
+ void trackingRef.current?.flush?.();
865
+ };
866
+ const onVisibilityChange = () => {
867
+ if (document.visibilityState === "hidden") flushOnExit();
868
+ };
869
+ document.addEventListener("visibilitychange", onVisibilityChange);
870
+ window.addEventListener("pagehide", flushOnExit);
871
+ return () => {
872
+ document.removeEventListener("visibilitychange", onVisibilityChange);
873
+ window.removeEventListener("pagehide", flushOnExit);
874
+ };
875
+ }, []);
797
876
  const setActiveLesson = useCallback(
798
877
  (lessonId) => {
799
878
  if (useV2Runtime && headlessRef.current) {
@@ -857,20 +936,34 @@ function useLessonkitProviderRuntime(config) {
857
936
  session: normalizedConfig.session
858
937
  });
859
938
  }
860
- }, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
939
+ }, [
940
+ useV2Runtime,
941
+ normalizedCourseId,
942
+ sessionAttemptId,
943
+ sessionConfiguredId,
944
+ sessionUserKey,
945
+ normalizedConfig.session
946
+ ]);
947
+ useEffect(() => {
948
+ if (!useV2Runtime || !headlessRef.current) return;
949
+ headlessRef.current.updateConfig({
950
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
951
+ });
952
+ }, [useV2Runtime, pluginHost]);
861
953
  useEffect(() => {
862
- if (!pluginHost) return;
954
+ const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
955
+ if (!host) return;
863
956
  const ctx = buildPluginContext({
864
957
  courseId: courseIdRef.current,
865
958
  sessionId: sessionIdRef.current,
866
959
  attemptId: attemptIdRef.current,
867
960
  user: userRef.current
868
961
  });
869
- pluginHost.setupAll(ctx);
962
+ host.setupAll(ctx);
870
963
  return () => {
871
- pluginHost.disposeAll();
964
+ host.disposeAll();
872
965
  };
873
- }, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
966
+ }, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
874
967
  useEffect(() => {
875
968
  const nextConfigured = normalizedConfig.session?.sessionId;
876
969
  const prevConfigured = prevConfiguredSessionIdRef.current;
@@ -934,7 +1027,27 @@ function LessonkitProvider(props) {
934
1027
  }
935
1028
 
936
1029
  // src/hooks.ts
937
- import { useContext, useMemo as useMemo2 } from "react";
1030
+ import { useContext, useMemo as useMemo3 } from "react";
1031
+
1032
+ // src/assessment/useAssessmentState.ts
1033
+ import { useMemo as useMemo2 } from "react";
1034
+ function useAssessmentState(enclosingLessonId) {
1035
+ const { track } = useLessonkit();
1036
+ const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
1037
+ return useMemo2(
1038
+ () => ({
1039
+ answer: (data) => {
1040
+ track("assessment_answered", data, trackOpts);
1041
+ },
1042
+ complete: (data) => {
1043
+ track("assessment_completed", data, trackOpts);
1044
+ }
1045
+ }),
1046
+ [track, enclosingLessonId]
1047
+ );
1048
+ }
1049
+
1050
+ // src/hooks.ts
938
1051
  function useLessonkit() {
939
1052
  const ctx = useContext(LessonkitContext);
940
1053
  if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
@@ -946,16 +1059,16 @@ function useProgress() {
946
1059
  }
947
1060
  function useTracking() {
948
1061
  const { track } = useLessonkit();
949
- return useMemo2(() => ({ track }), [track]);
1062
+ return useMemo3(() => ({ track }), [track]);
950
1063
  }
951
1064
  function useCompletion() {
952
1065
  const { completeLesson, completeCourse } = useLessonkit();
953
- return useMemo2(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
1066
+ return useMemo3(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
954
1067
  }
955
1068
  function useQuizState(enclosingLessonId) {
956
1069
  const { track } = useLessonkit();
957
1070
  const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
958
- return useMemo2(
1071
+ return useMemo3(
959
1072
  () => ({
960
1073
  answer: (opts) => {
961
1074
  track("quiz_answered", opts, trackOpts);
@@ -1013,230 +1126,490 @@ function getLessonMountCount(lessonId) {
1013
1126
  return mountCounts.get(lessonId) ?? 0;
1014
1127
  }
1015
1128
 
1016
- // src/components.tsx
1017
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1018
- var warnedQuizOutsideLesson = false;
1019
- function resetQuizWarningsForTests() {
1020
- warnedQuizOutsideLesson = false;
1021
- }
1022
- function Course(props) {
1023
- const courseId = useMemo3(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1024
- const providerConfig = useMemo3(
1025
- () => ({ ...props.config, courseId }),
1026
- [props.config, courseId]
1027
- );
1028
- return /* @__PURE__ */ jsx2(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
1029
- /* @__PURE__ */ jsx2("h1", { children: props.title }),
1030
- /* @__PURE__ */ jsx2("div", { children: props.children })
1031
- ] }) });
1032
- }
1033
- function Lesson(props) {
1034
- const lessonId = useMemo3(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1035
- const autoComplete = props.autoCompleteOnUnmount !== false;
1036
- const { setActiveLesson, config } = useLessonkit();
1037
- const { completeLesson } = useCompletion();
1038
- const lessonMountGenerationRef = useRef2(0);
1039
- const liveCourseIdRef = useRef2(config.courseId);
1040
- liveCourseIdRef.current = config.courseId;
1041
- useEffect2(() => {
1042
- const unregister = registerLessonMount(lessonId);
1043
- const generation = ++lessonMountGenerationRef.current;
1044
- const mountedCourseId = config.courseId;
1045
- let effectSurvivedTick = false;
1046
- queueMicrotask(() => {
1047
- queueMicrotask(() => {
1048
- effectSurvivedTick = true;
1049
- });
1050
- });
1051
- setActiveLesson(lessonId);
1052
- return () => {
1053
- unregister();
1054
- if (getLessonMountCount(lessonId) > 0) {
1055
- return;
1056
- }
1057
- if (!autoComplete) return;
1058
- queueMicrotask(() => {
1059
- if (!effectSurvivedTick) return;
1060
- if (lessonMountGenerationRef.current !== generation) return;
1061
- if (liveCourseIdRef.current !== mountedCourseId) return;
1062
- completeLesson(lessonId, { courseId: mountedCourseId });
1063
- });
1064
- };
1065
- }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1066
- return /* @__PURE__ */ jsx2(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
1067
- /* @__PURE__ */ jsx2("h2", { children: props.title }),
1068
- /* @__PURE__ */ jsx2("div", { children: props.children })
1069
- ] }) });
1070
- }
1071
- function Scenario(props) {
1072
- const blockId = useMemo3(
1073
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1074
- [props.blockId]
1075
- );
1076
- return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1077
- }
1078
- function Reflection(props) {
1079
- const blockId = useMemo3(
1080
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1081
- [props.blockId]
1082
- );
1083
- const promptId = useId();
1084
- const hintId = useId();
1085
- const [internalValue, setInternalValue] = useState2("");
1086
- const isControlled = props.value !== void 0;
1087
- const value = isControlled ? props.value : internalValue;
1088
- const handleChange = (event) => {
1089
- if (!isControlled) setInternalValue(event.target.value);
1090
- props.onChange?.(event.target.value);
1091
- };
1092
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1093
- props.prompt ? /* @__PURE__ */ jsx2("p", { id: promptId, children: props.prompt }) : null,
1094
- props.hint ? /* @__PURE__ */ jsx2("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
1095
- props.children,
1096
- /* @__PURE__ */ jsx2(
1097
- "textarea",
1098
- {
1099
- value,
1100
- onChange: handleChange,
1101
- "aria-labelledby": props.prompt ? promptId : void 0,
1102
- "aria-describedby": props.hint ? hintId : void 0,
1103
- "aria-label": props.prompt ? void 0 : "Reflection response"
1104
- }
1105
- )
1106
- ] });
1107
- }
1108
- function KnowledgeCheck(props) {
1109
- return /* @__PURE__ */ jsx2(
1110
- Quiz,
1111
- {
1112
- checkId: props.checkId,
1113
- question: props.question,
1114
- choices: props.choices,
1115
- answer: props.answer,
1116
- passingScore: props.passingScore
1117
- }
1118
- );
1129
+ // src/components/Quiz.tsx
1130
+ import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef3, useState as useState3 } from "react";
1131
+ import { visuallyHiddenStyle } from "@lessonkit/accessibility";
1132
+
1133
+ // src/assessment/AssessmentLessonGuard.tsx
1134
+ import { useEffect as useEffect2 } from "react";
1135
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1136
+ var warnedAssessmentOutsideLesson = false;
1137
+ function resetAssessmentWarningsForTests() {
1138
+ warnedAssessmentOutsideLesson = false;
1119
1139
  }
1120
- function Quiz(props) {
1140
+ function AssessmentLessonGuard(props) {
1121
1141
  const enclosingLessonId = useEnclosingLessonId();
1122
1142
  const missingLesson = enclosingLessonId === void 0;
1123
1143
  useEffect2(() => {
1124
1144
  if (!missingLesson || isDevEnvironment4()) return;
1125
- if (!warnedQuizOutsideLesson) {
1126
- warnedQuizOutsideLesson = true;
1145
+ if (!warnedAssessmentOutsideLesson) {
1146
+ warnedAssessmentOutsideLesson = true;
1127
1147
  console.error(
1128
- "[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
1148
+ `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1129
1149
  );
1130
1150
  }
1131
- }, [missingLesson]);
1151
+ }, [missingLesson, props.blockLabel]);
1132
1152
  if (missingLesson && isDevEnvironment4()) {
1133
- throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1153
+ throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1134
1154
  }
1135
1155
  if (missingLesson) {
1136
- return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
1156
+ return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs("p", { children: [
1157
+ props.blockLabel,
1158
+ " must be placed inside a Lesson."
1159
+ ] }) });
1137
1160
  }
1138
- return /* @__PURE__ */ jsx2(QuizInner, { ...props, enclosingLessonId });
1161
+ return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
1139
1162
  }
1140
- function QuizInner(props) {
1141
- const { enclosingLessonId } = props;
1142
- const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1143
- const quiz = useQuizState(enclosingLessonId);
1144
- const { plugins, config, session } = useLessonkit();
1145
- const [selected, setSelected] = useState2(null);
1146
- const [selectionCorrect, setSelectionCorrect] = useState2(null);
1147
- const [quizPassed, setQuizPassed] = useState2(false);
1148
- const completedRef = useRef2(false);
1149
- const questionId = useId();
1150
- const choicesKey = props.choices.join("\0");
1151
- useEffect2(() => {
1152
- completedRef.current = false;
1153
- setQuizPassed(false);
1154
- setSelected(null);
1155
- setSelectionCorrect(null);
1156
- }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
1157
- const isChoiceCorrect = (choice, custom) => {
1158
- if (!custom) return choice === props.answer;
1159
- if (custom.passed !== void 0) return custom.passed;
1160
- if (custom.maxScore != null && custom.maxScore > 0) {
1161
- return custom.score / custom.maxScore >= 1;
1162
- }
1163
- return choice === props.answer;
1163
+
1164
+ // src/assessment/internal/buildAssessmentHandle.ts
1165
+ function buildAssessmentHandle(opts) {
1166
+ return {
1167
+ getScore: opts.getScore,
1168
+ getMaxScore: opts.getMaxScore,
1169
+ getAnswerGiven: opts.getAnswerGiven,
1170
+ resetTask: opts.resetTask,
1171
+ showSolutions: opts.showSolutions,
1172
+ getXAPIData: opts.getXAPIData,
1173
+ ...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
1174
+ ...opts.resume ? { resume: opts.resume } : {}
1164
1175
  };
1165
- const passed = quizPassed;
1166
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1167
- /* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
1168
- /* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
1169
- /* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
1170
- props.choices.map((c, i) => /* @__PURE__ */ jsxs("label", { style: { display: "block" }, children: [
1171
- /* @__PURE__ */ jsx2(
1172
- "input",
1173
- {
1174
- type: "radio",
1175
- name: questionId,
1176
- value: c,
1177
- checked: selected === c,
1178
- disabled: passed,
1179
- "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1180
- onChange: () => {
1181
- if (passed) return;
1182
- setSelected(c);
1183
- const pluginCtx = buildPluginContext({
1184
- courseId: config.courseId,
1185
- sessionId: session.sessionId,
1186
- attemptId: session.attemptId,
1187
- user: session.user
1188
- });
1189
- const custom = plugins?.scoreAssessment(
1190
- {
1191
- checkId,
1192
- lessonId: enclosingLessonId,
1193
- response: c
1194
- },
1195
- pluginCtx
1196
- ) ?? null;
1197
- const correct = isChoiceCorrect(c, custom);
1198
- setSelectionCorrect(correct);
1199
- quiz.answer({
1200
- checkId,
1201
- question: props.question,
1202
- choice: c,
1203
- correct
1204
- });
1205
- if (correct && !completedRef.current) {
1206
- completedRef.current = true;
1207
- setQuizPassed(true);
1208
- const maxScore = custom?.maxScore ?? 1;
1209
- quiz.complete({
1210
- checkId,
1211
- score: custom?.score ?? 1,
1212
- maxScore,
1213
- passingScore: props.passingScore ?? maxScore
1214
- });
1215
- }
1216
- }
1217
- }
1218
- ),
1219
- c
1220
- ] }, `${questionId}-${i}`))
1221
- ] }),
1222
- selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1223
- ] });
1224
1176
  }
1225
- function ProgressTracker(props) {
1226
- const { progress } = useLessonkit();
1227
- const completed = progress.completedLessonIds.size;
1228
- if (props.totalLessons != null) {
1229
- const total = props.totalLessons;
1230
- const displayed = Math.min(completed, total);
1231
- return /* @__PURE__ */ jsx2("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx2(
1232
- "div",
1233
- {
1177
+
1178
+ // src/assessment/internal/resumeState.ts
1179
+ function readBooleanField(state, key) {
1180
+ const value = state[key];
1181
+ if (value === true || value === false || value === null) return value;
1182
+ return void 0;
1183
+ }
1184
+ function readStringField(state, key) {
1185
+ const value = state[key];
1186
+ if (typeof value === "string" || value === null) return value;
1187
+ return void 0;
1188
+ }
1189
+ function readBooleanStateField(state, key, apply) {
1190
+ const value = state[key];
1191
+ if (typeof value === "boolean") apply(value);
1192
+ }
1193
+
1194
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1195
+ import { useImperativeHandle as useImperativeHandle2 } from "react";
1196
+
1197
+ // src/compound/CompoundProvider.tsx
1198
+ import React3, { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useImperativeHandle, useMemo as useMemo4, useRef as useRef2, useState as useState2 } from "react";
1199
+ import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
1200
+
1201
+ // src/compound/aggregateScores.ts
1202
+ function aggregateAssessmentScores(handles) {
1203
+ let score = 0;
1204
+ let maxScore = 0;
1205
+ let allAnswered = true;
1206
+ for (const handle of handles) {
1207
+ score += handle.getScore();
1208
+ maxScore += handle.getMaxScore();
1209
+ if (!handle.getAnswerGiven()) allAnswered = false;
1210
+ }
1211
+ return { score, maxScore, allAnswered };
1212
+ }
1213
+
1214
+ // src/compound/resumeChildHandles.ts
1215
+ function resumeChildHandles(handles, childStates, opts) {
1216
+ if (opts?.waitForHandles && handles.size === 0 && Object.keys(childStates).length > 0) {
1217
+ return false;
1218
+ }
1219
+ for (const [checkId, handle] of handles) {
1220
+ const child = childStates[checkId];
1221
+ if (child && handle.resume) handle.resume(child);
1222
+ }
1223
+ return true;
1224
+ }
1225
+
1226
+ // src/compound/CompoundProvider.tsx
1227
+ import { jsx as jsx3 } from "react/jsx-runtime";
1228
+ var CompoundRegistryContext = createContext3(null);
1229
+ var CompoundHandlesVersionContext = createContext3(0);
1230
+ function CompoundProvider({
1231
+ children,
1232
+ activePageIndex: _activePageIndex,
1233
+ onActivePageIndexChange: _onActivePageIndexChange
1234
+ }) {
1235
+ const registryRef = useRef2(/* @__PURE__ */ new Map());
1236
+ const [handlesVersion, setHandlesVersion] = useState2(0);
1237
+ const register = useCallback2((checkId, handle) => {
1238
+ const prev = registryRef.current.get(checkId);
1239
+ registryRef.current.set(checkId, handle);
1240
+ if (prev !== handle) {
1241
+ setHandlesVersion((v) => v + 1);
1242
+ }
1243
+ return () => {
1244
+ if (registryRef.current.get(checkId) === handle) {
1245
+ registryRef.current.delete(checkId);
1246
+ setHandlesVersion((v) => v + 1);
1247
+ }
1248
+ };
1249
+ }, []);
1250
+ const registryValue = useMemo4(
1251
+ () => ({
1252
+ register,
1253
+ getHandles: () => registryRef.current
1254
+ }),
1255
+ [register]
1256
+ );
1257
+ return /* @__PURE__ */ jsx3(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx3(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) });
1258
+ }
1259
+ function useCompoundRegistry() {
1260
+ const registry = useContext3(CompoundRegistryContext);
1261
+ const handlesVersion = useContext3(CompoundHandlesVersionContext);
1262
+ if (!registry) return null;
1263
+ return { ...registry, handlesVersion };
1264
+ }
1265
+ function useCompoundHandlesVersion() {
1266
+ return useContext3(CompoundHandlesVersionContext);
1267
+ }
1268
+ function useRegisterAssessmentHandle(checkId, handle) {
1269
+ const registry = useContext3(CompoundRegistryContext);
1270
+ React3.useEffect(() => {
1271
+ if (!registry || !handle) return;
1272
+ return registry.register(checkId, handle);
1273
+ }, [registry, checkId, handle]);
1274
+ }
1275
+ function useCompoundHandleRef(ref, opts) {
1276
+ const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
1277
+ const setIndexClamped = useCallback2(
1278
+ (index) => {
1279
+ const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
1280
+ setActivePageIndex(next);
1281
+ },
1282
+ [pageCount, setActivePageIndex]
1283
+ );
1284
+ useImperativeHandle(
1285
+ ref,
1286
+ () => ({
1287
+ getScore: () => aggregateAssessmentScores(getHandles().values()).score,
1288
+ getMaxScore: () => aggregateAssessmentScores(getHandles().values()).maxScore,
1289
+ getAnswerGiven: () => aggregateAssessmentScores(getHandles().values()).allAnswered,
1290
+ resetTask: () => {
1291
+ for (const handle of getHandles().values()) handle.resetTask();
1292
+ },
1293
+ showSolutions: () => {
1294
+ if (!opts.enableSolutionsButton) return;
1295
+ for (const handle of getHandles().values()) handle.showSolutions();
1296
+ },
1297
+ getCurrentState: () => {
1298
+ const childStates = {};
1299
+ for (const [checkId, handle] of getHandles()) {
1300
+ if (handle.getCurrentState) {
1301
+ childStates[checkId] = handle.getCurrentState();
1302
+ }
1303
+ }
1304
+ return createCompoundResumeState({ activePageIndex, childStates });
1305
+ },
1306
+ resume: (state) => {
1307
+ setIndexClamped(state.activePageIndex);
1308
+ resumeChildHandles(getHandles(), state.childStates);
1309
+ }
1310
+ }),
1311
+ [activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
1312
+ );
1313
+ }
1314
+
1315
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1316
+ function useAssessmentHandleRegistration(checkId, handle, ref) {
1317
+ useImperativeHandle2(ref, () => handle, [handle]);
1318
+ useRegisterAssessmentHandle(checkId, handle);
1319
+ }
1320
+
1321
+ // src/assessment/internal/usePluginScoring.ts
1322
+ import { useCallback as useCallback3 } from "react";
1323
+
1324
+ // src/assessment/scoring.ts
1325
+ function resolvePassingThreshold(passingScore, maxScore) {
1326
+ return passingScore ?? maxScore;
1327
+ }
1328
+ function meetsPassingThreshold(score, maxScore, passingScore) {
1329
+ const threshold = resolvePassingThreshold(passingScore, maxScore);
1330
+ return score >= threshold;
1331
+ }
1332
+ function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
1333
+ const maxScore = custom?.maxScore ?? fallbackMax;
1334
+ if (custom?.passed !== void 0) {
1335
+ const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
1336
+ return { score: score2, maxScore, passed: custom.passed };
1337
+ }
1338
+ if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1339
+ const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1340
+ return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
1341
+ }
1342
+ const score = fallbackCorrect ? maxScore : 0;
1343
+ const passed = meetsPassingThreshold(score, maxScore, passingScore);
1344
+ return { score, maxScore, passed };
1345
+ }
1346
+
1347
+ // src/assessment/internal/usePluginScoring.ts
1348
+ function usePluginScoring(checkId, lessonId) {
1349
+ const { plugins, config, session } = useLessonkit();
1350
+ const getPluginScore = useCallback3(
1351
+ (response) => {
1352
+ const pluginCtx = buildPluginContext({
1353
+ courseId: config.courseId,
1354
+ sessionId: session.sessionId,
1355
+ attemptId: session.attemptId,
1356
+ user: session.user
1357
+ });
1358
+ return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
1359
+ },
1360
+ [checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
1361
+ );
1362
+ const scoreResponse = useCallback3(
1363
+ (response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
1364
+ [getPluginScore]
1365
+ );
1366
+ const isChoiceCorrect = useCallback3(
1367
+ (choice, answer, custom, passingScore) => {
1368
+ if (!custom) return choice === answer;
1369
+ if (custom.passed !== void 0) return custom.passed;
1370
+ if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1371
+ return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1372
+ }
1373
+ return choice === answer;
1374
+ },
1375
+ []
1376
+ );
1377
+ return { getPluginScore, scoreResponse, isChoiceCorrect };
1378
+ }
1379
+
1380
+ // src/components/Quiz.tsx
1381
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1382
+ function QuizInner(props, ref) {
1383
+ const { enclosingLessonId } = props;
1384
+ const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1385
+ const quiz = useQuizState(enclosingLessonId);
1386
+ const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
1387
+ const [selected, setSelected] = useState3(null);
1388
+ const [selectionCorrect, setSelectionCorrect] = useState3(null);
1389
+ const [quizPassed, setQuizPassed] = useState3(false);
1390
+ const completedRef = useRef3(false);
1391
+ const questionId = useId();
1392
+ const choicesKey = props.choices.join("\0");
1393
+ useEffect3(() => {
1394
+ completedRef.current = false;
1395
+ setQuizPassed(false);
1396
+ setSelected(null);
1397
+ setSelectionCorrect(null);
1398
+ }, [checkId, props.answer, props.question, choicesKey]);
1399
+ const passed = quizPassed;
1400
+ const handle = useMemo5(
1401
+ () => buildAssessmentHandle({
1402
+ checkId,
1403
+ getScore: () => {
1404
+ const maxScore = 1;
1405
+ if (quizPassed && selected !== null) return maxScore;
1406
+ if (selected === null) return 0;
1407
+ return selectionCorrect ? maxScore : 0;
1408
+ },
1409
+ getMaxScore: () => 1,
1410
+ getAnswerGiven: () => selected !== null,
1411
+ resetTask: () => {
1412
+ completedRef.current = false;
1413
+ setQuizPassed(false);
1414
+ setSelected(null);
1415
+ setSelectionCorrect(null);
1416
+ },
1417
+ showSolutions: () => {
1418
+ },
1419
+ getXAPIData: () => ({
1420
+ checkId,
1421
+ interactionType: "mcq",
1422
+ response: selected ?? void 0,
1423
+ correct: selectionCorrect ?? void 0,
1424
+ score: quizPassed && selected !== null ? 1 : selected === null ? 0 : selectionCorrect ? 1 : 0,
1425
+ maxScore: 1
1426
+ }),
1427
+ getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
1428
+ resume: (state) => {
1429
+ const nextSelected = readStringField(state, "selected");
1430
+ if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
1431
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1432
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1433
+ setSelectionCorrect(nextCorrect);
1434
+ }
1435
+ readBooleanStateField(state, "quizPassed", (value) => {
1436
+ setQuizPassed(value);
1437
+ completedRef.current = value;
1438
+ });
1439
+ }
1440
+ }),
1441
+ [checkId, quizPassed, selected, selectionCorrect]
1442
+ );
1443
+ useAssessmentHandleRegistration(checkId, handle, ref);
1444
+ return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1445
+ /* @__PURE__ */ jsx4("p", { id: questionId, children: props.question }),
1446
+ /* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
1447
+ /* @__PURE__ */ jsx4("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
1448
+ props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
1449
+ /* @__PURE__ */ jsx4(
1450
+ "input",
1451
+ {
1452
+ type: "radio",
1453
+ name: questionId,
1454
+ value: c,
1455
+ checked: selected === c,
1456
+ disabled: passed,
1457
+ "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1458
+ onChange: () => {
1459
+ if (passed) return;
1460
+ setSelected(c);
1461
+ const custom = getPluginScore(c);
1462
+ const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
1463
+ setSelectionCorrect(correct);
1464
+ quiz.answer({
1465
+ checkId,
1466
+ question: props.question,
1467
+ choice: c,
1468
+ correct
1469
+ });
1470
+ if (correct && !completedRef.current) {
1471
+ completedRef.current = true;
1472
+ setQuizPassed(true);
1473
+ const maxScore = custom?.maxScore ?? 1;
1474
+ quiz.complete({
1475
+ checkId,
1476
+ score: custom?.score ?? maxScore,
1477
+ maxScore,
1478
+ passingScore: props.passingScore ?? maxScore
1479
+ });
1480
+ }
1481
+ }
1482
+ }
1483
+ ),
1484
+ c
1485
+ ] }, `${questionId}-${i}`))
1486
+ ] }),
1487
+ selected && selectionCorrect !== null ? /* @__PURE__ */ jsx4("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1488
+ ] });
1489
+ }
1490
+ var QuizInnerForwarded = forwardRef(QuizInner);
1491
+ var Quiz = forwardRef(function Quiz2(props, ref) {
1492
+ return /* @__PURE__ */ jsx4(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx4(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1493
+ });
1494
+ function KnowledgeCheck(props) {
1495
+ return /* @__PURE__ */ jsx4(
1496
+ Quiz,
1497
+ {
1498
+ checkId: props.checkId,
1499
+ question: props.question,
1500
+ choices: props.choices,
1501
+ answer: props.answer,
1502
+ passingScore: props.passingScore
1503
+ }
1504
+ );
1505
+ }
1506
+ function resetQuizWarningsForTests() {
1507
+ resetAssessmentWarningsForTests();
1508
+ }
1509
+
1510
+ // src/components.tsx
1511
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1512
+ function Course(props) {
1513
+ const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1514
+ const providerConfig = useMemo6(
1515
+ () => ({ ...props.config, courseId }),
1516
+ [props.config, courseId]
1517
+ );
1518
+ return /* @__PURE__ */ jsx5(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
1519
+ /* @__PURE__ */ jsx5("h1", { children: props.title }),
1520
+ /* @__PURE__ */ jsx5("div", { children: props.children })
1521
+ ] }) });
1522
+ }
1523
+ function Lesson(props) {
1524
+ const lessonId = useMemo6(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1525
+ const autoComplete = props.autoCompleteOnUnmount !== false;
1526
+ const { setActiveLesson, config } = useLessonkit();
1527
+ const { completeLesson } = useCompletion();
1528
+ const lessonMountGenerationRef = useRef4(0);
1529
+ const liveCourseIdRef = useRef4(config.courseId);
1530
+ liveCourseIdRef.current = config.courseId;
1531
+ useEffect4(() => {
1532
+ const unregister = registerLessonMount(lessonId);
1533
+ const generation = ++lessonMountGenerationRef.current;
1534
+ const mountedCourseId = config.courseId;
1535
+ let effectSurvivedTick = false;
1536
+ queueMicrotask(() => {
1537
+ queueMicrotask(() => {
1538
+ effectSurvivedTick = true;
1539
+ });
1540
+ });
1541
+ setActiveLesson(lessonId);
1542
+ return () => {
1543
+ unregister();
1544
+ if (getLessonMountCount(lessonId) > 0) {
1545
+ return;
1546
+ }
1547
+ if (!autoComplete) return;
1548
+ queueMicrotask(() => {
1549
+ if (!effectSurvivedTick) return;
1550
+ if (lessonMountGenerationRef.current !== generation) return;
1551
+ if (liveCourseIdRef.current !== mountedCourseId) return;
1552
+ completeLesson(lessonId, { courseId: mountedCourseId });
1553
+ });
1554
+ };
1555
+ }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1556
+ return /* @__PURE__ */ jsx5(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
1557
+ /* @__PURE__ */ jsx5("h2", { children: props.title }),
1558
+ /* @__PURE__ */ jsx5("div", { children: props.children })
1559
+ ] }) });
1560
+ }
1561
+ function Scenario(props) {
1562
+ const blockId = useMemo6(
1563
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1564
+ [props.blockId]
1565
+ );
1566
+ return /* @__PURE__ */ jsx5("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1567
+ }
1568
+ function Reflection(props) {
1569
+ const blockId = useMemo6(
1570
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1571
+ [props.blockId]
1572
+ );
1573
+ const promptId = useId2();
1574
+ const hintId = useId2();
1575
+ const [internalValue, setInternalValue] = useState4("");
1576
+ const isControlled = props.value !== void 0;
1577
+ const value = isControlled ? props.value : internalValue;
1578
+ const handleChange = (event) => {
1579
+ if (!isControlled) setInternalValue(event.target.value);
1580
+ props.onChange?.(event.target.value);
1581
+ };
1582
+ return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1583
+ props.prompt ? /* @__PURE__ */ jsx5("p", { id: promptId, children: props.prompt }) : null,
1584
+ props.hint ? /* @__PURE__ */ jsx5("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
1585
+ props.children,
1586
+ /* @__PURE__ */ jsx5(
1587
+ "textarea",
1588
+ {
1589
+ value,
1590
+ onChange: handleChange,
1591
+ "aria-labelledby": props.prompt ? promptId : void 0,
1592
+ "aria-describedby": props.hint ? hintId : void 0,
1593
+ "aria-label": props.prompt ? void 0 : "Reflection response"
1594
+ }
1595
+ )
1596
+ ] });
1597
+ }
1598
+ function ProgressTracker(props) {
1599
+ const { progress } = useLessonkit();
1600
+ const completed = progress.completedLessonIds.size;
1601
+ if (props.totalLessons != null) {
1602
+ const total = props.totalLessons;
1603
+ const displayed = Math.min(completed, total);
1604
+ return /* @__PURE__ */ jsx5("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx5(
1605
+ "div",
1606
+ {
1234
1607
  role: "progressbar",
1235
1608
  "aria-valuemin": 0,
1236
1609
  "aria-valuemax": total,
1237
1610
  "aria-valuenow": displayed,
1238
1611
  "aria-label": "Lessons completed",
1239
- children: /* @__PURE__ */ jsxs("p", { children: [
1612
+ children: /* @__PURE__ */ jsxs3("p", { children: [
1240
1613
  "Lessons completed: ",
1241
1614
  displayed,
1242
1615
  " of ",
@@ -1245,11 +1618,1806 @@ function ProgressTracker(props) {
1245
1618
  }
1246
1619
  ) });
1247
1620
  }
1248
- return /* @__PURE__ */ jsx2("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs("p", { children: [
1249
- "Lessons completed: ",
1250
- completed
1251
- ] }) });
1621
+ return /* @__PURE__ */ jsx5("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
1622
+ "Lessons completed: ",
1623
+ completed
1624
+ ] }) });
1625
+ }
1626
+
1627
+ // src/blocks/TrueFalse.tsx
1628
+ import React7, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef5, useState as useState5 } from "react";
1629
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1630
+ var INTERACTION = "trueFalse";
1631
+ function TrueFalseInner(props, ref) {
1632
+ const { enclosingLessonId } = props;
1633
+ const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1634
+ const assessment = useAssessmentState(enclosingLessonId);
1635
+ const { config } = useLessonkit();
1636
+ const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
1637
+ const [selected, setSelected] = useState5(null);
1638
+ const [selectionCorrect, setSelectionCorrect] = useState5(null);
1639
+ const [showSolutions, setShowSolutions] = useState5(false);
1640
+ const [passed, setPassed] = useState5(false);
1641
+ const completedRef = useRef5(false);
1642
+ const questionId = React7.useId();
1643
+ const reset = () => {
1644
+ completedRef.current = false;
1645
+ setPassed(false);
1646
+ setSelected(null);
1647
+ setSelectionCorrect(null);
1648
+ setShowSolutions(false);
1649
+ };
1650
+ useEffect5(() => {
1651
+ reset();
1652
+ }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1653
+ const handle = useMemo7(
1654
+ () => buildAssessmentHandle({
1655
+ checkId,
1656
+ getScore: () => {
1657
+ const maxScore = 1;
1658
+ return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
1659
+ },
1660
+ getMaxScore: () => 1,
1661
+ getAnswerGiven: () => selected !== null,
1662
+ resetTask: reset,
1663
+ showSolutions: () => setShowSolutions(true),
1664
+ getXAPIData: () => ({
1665
+ checkId,
1666
+ interactionType: INTERACTION,
1667
+ response: selected ?? void 0,
1668
+ correct: selected === props.answer,
1669
+ score: passed ? 1 : selected === null ? 0 : selected === props.answer ? 1 : 0,
1670
+ maxScore: 1
1671
+ }),
1672
+ getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
1673
+ resume: (state) => {
1674
+ const nextSelected = readBooleanField(state, "selected");
1675
+ if (nextSelected === true || nextSelected === false || nextSelected === null) {
1676
+ setSelected(nextSelected);
1677
+ }
1678
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1679
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1680
+ setSelectionCorrect(nextCorrect);
1681
+ }
1682
+ readBooleanStateField(state, "passed", (value) => {
1683
+ setPassed(value);
1684
+ completedRef.current = value;
1685
+ });
1686
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
1687
+ }
1688
+ }),
1689
+ [checkId, passed, props.answer, selected, selectionCorrect, showSolutions]
1690
+ );
1691
+ useAssessmentHandleRegistration(checkId, handle, ref);
1692
+ const submit = (value) => {
1693
+ if (passed && !props.enableRetry) return;
1694
+ setSelected(value);
1695
+ const correct = value === props.answer;
1696
+ const scored = scoreResponse(value, correct, 1, props.passingScore);
1697
+ setSelectionCorrect(scored.passed);
1698
+ assessment.answer({
1699
+ checkId,
1700
+ interactionType: INTERACTION,
1701
+ question: props.question,
1702
+ response: value,
1703
+ correct: scored.passed
1704
+ });
1705
+ if (scored.passed && !completedRef.current) {
1706
+ completedRef.current = true;
1707
+ setPassed(true);
1708
+ assessment.complete({
1709
+ checkId,
1710
+ interactionType: INTERACTION,
1711
+ score: scored.score,
1712
+ maxScore: scored.maxScore,
1713
+ passingScore: props.passingScore ?? scored.maxScore
1714
+ });
1715
+ }
1716
+ };
1717
+ const reveal = showSolutions || passed && props.enableSolutionsButton;
1718
+ return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1719
+ /* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
1720
+ /* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
1721
+ /* @__PURE__ */ jsx6("legend", { className: "lk-visually-hidden", children: "True or False" }),
1722
+ /* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
1723
+ /* @__PURE__ */ jsx6(
1724
+ "input",
1725
+ {
1726
+ type: "radio",
1727
+ name: `${questionId}-tf`,
1728
+ checked: selected === true,
1729
+ disabled: passed && !props.enableRetry,
1730
+ onChange: () => submit(true)
1731
+ }
1732
+ ),
1733
+ "True"
1734
+ ] }),
1735
+ /* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
1736
+ /* @__PURE__ */ jsx6(
1737
+ "input",
1738
+ {
1739
+ type: "radio",
1740
+ name: `${questionId}-tf`,
1741
+ checked: selected === false,
1742
+ disabled: passed && !props.enableRetry,
1743
+ onChange: () => submit(false)
1744
+ }
1745
+ ),
1746
+ "False"
1747
+ ] })
1748
+ ] }),
1749
+ reveal ? /* @__PURE__ */ jsxs4("p", { children: [
1750
+ "Correct answer: ",
1751
+ /* @__PURE__ */ jsx6("strong", { children: props.answer ? "True" : "False" })
1752
+ ] }) : null,
1753
+ selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1754
+ props.enableRetry && passed ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1755
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1756
+ ] });
1757
+ }
1758
+ var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
1759
+ var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
1760
+ return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1761
+ });
1762
+
1763
+ // src/blocks/MarkTheWords.tsx
1764
+ import React8, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef6, useState as useState6 } from "react";
1765
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1766
+ var INTERACTION2 = "markTheWords";
1767
+ function tokenize(text) {
1768
+ return text.split(/(\s+)/).filter((t) => t.length > 0);
1769
+ }
1770
+ function MarkTheWordsInner(props, ref) {
1771
+ const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1772
+ const assessment = useAssessmentState(props.enclosingLessonId);
1773
+ const tokens = useMemo8(() => tokenize(props.text), [props.text]);
1774
+ const correctSet = useMemo8(
1775
+ () => new Set(props.correctWords.map((w) => w.toLowerCase())),
1776
+ [props.correctWords]
1777
+ );
1778
+ const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
1779
+ const [passed, setPassed] = useState6(false);
1780
+ const [showSolutions, setShowSolutions] = useState6(false);
1781
+ const completedRef = useRef6(false);
1782
+ const reset = () => {
1783
+ completedRef.current = false;
1784
+ setPassed(false);
1785
+ setMarked(/* @__PURE__ */ new Set());
1786
+ setShowSolutions(false);
1787
+ };
1788
+ useEffect6(() => {
1789
+ reset();
1790
+ }, [checkId, props.text, props.correctWords.join("\0")]);
1791
+ const selectableIndices = useMemo8(() => {
1792
+ const indices = [];
1793
+ tokens.forEach((t, i) => {
1794
+ if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
1795
+ });
1796
+ return indices;
1797
+ }, [tokens, correctSet]);
1798
+ const hasTargets = selectableIndices.length > 0;
1799
+ const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
1800
+ const maxScore = selectableIndices.length;
1801
+ const score = allMarked ? maxScore : marked.size;
1802
+ const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1803
+ const handle = useMemo8(
1804
+ () => buildAssessmentHandle({
1805
+ checkId,
1806
+ getScore: () => score,
1807
+ getMaxScore: () => maxScore || 1,
1808
+ getAnswerGiven: () => marked.size > 0,
1809
+ resetTask: reset,
1810
+ showSolutions: () => setShowSolutions(true),
1811
+ getXAPIData: () => ({
1812
+ checkId,
1813
+ interactionType: INTERACTION2,
1814
+ response: [...marked].map((i) => tokens[i]),
1815
+ correct: passedThreshold,
1816
+ score,
1817
+ maxScore: maxScore || 1
1818
+ }),
1819
+ getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
1820
+ resume: (state) => {
1821
+ const raw = state.marked;
1822
+ if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
1823
+ readBooleanStateField(state, "passed", (value) => {
1824
+ setPassed(value);
1825
+ completedRef.current = value;
1826
+ });
1827
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
1828
+ }
1829
+ }),
1830
+ [checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
1831
+ );
1832
+ useAssessmentHandleRegistration(checkId, handle, ref);
1833
+ const toggle = (index) => {
1834
+ if (passed && !props.enableRetry) return;
1835
+ setMarked((prev) => {
1836
+ const next = new Set(prev);
1837
+ if (next.has(index)) next.delete(index);
1838
+ else next.add(index);
1839
+ return next;
1840
+ });
1841
+ };
1842
+ useEffect6(() => {
1843
+ if (!hasTargets) {
1844
+ if (isDevEnvironment4()) {
1845
+ console.warn(
1846
+ "[lessonkit] MarkTheWords: no tokens match correctWords",
1847
+ props.correctWords
1848
+ );
1849
+ }
1850
+ return;
1851
+ }
1852
+ if (!passedThreshold || completedRef.current) return;
1853
+ completedRef.current = true;
1854
+ setPassed(true);
1855
+ assessment.answer({
1856
+ checkId,
1857
+ interactionType: INTERACTION2,
1858
+ question: props.text,
1859
+ response: [...marked].map((i) => tokens[i]),
1860
+ correct: true
1861
+ });
1862
+ assessment.complete({
1863
+ checkId,
1864
+ interactionType: INTERACTION2,
1865
+ score,
1866
+ maxScore,
1867
+ passingScore: props.passingScore ?? maxScore
1868
+ });
1869
+ }, [
1870
+ assessment,
1871
+ checkId,
1872
+ hasTargets,
1873
+ marked,
1874
+ maxScore,
1875
+ passedThreshold,
1876
+ props.passingScore,
1877
+ props.correctWords,
1878
+ props.text,
1879
+ score,
1880
+ tokens
1881
+ ]);
1882
+ return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1883
+ !hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
1884
+ "No words in this sentence match ",
1885
+ /* @__PURE__ */ jsx7("code", { children: "correctWords" }),
1886
+ ". Check spelling and capitalization in the source text."
1887
+ ] }) : null,
1888
+ /* @__PURE__ */ jsx7("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
1889
+ /* @__PURE__ */ jsx7("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1890
+ const isWord = !/^\s+$/.test(token);
1891
+ const isTarget = isWord && correctSet.has(token.toLowerCase());
1892
+ if (!isTarget) return /* @__PURE__ */ jsx7(React8.Fragment, { children: token }, i);
1893
+ const selected = marked.has(i);
1894
+ const solution = showSolutions || passed && props.enableSolutionsButton;
1895
+ return /* @__PURE__ */ jsx7(
1896
+ "button",
1897
+ {
1898
+ type: "button",
1899
+ "data-testid": `mark-word-${i}`,
1900
+ "aria-pressed": selected,
1901
+ disabled: passed && !props.enableRetry,
1902
+ onClick: () => toggle(i),
1903
+ style: {
1904
+ margin: "0 0.1em",
1905
+ textDecoration: solution ? "underline" : void 0,
1906
+ fontWeight: selected || solution ? "bold" : void 0
1907
+ },
1908
+ children: token
1909
+ },
1910
+ i
1911
+ );
1912
+ }) }),
1913
+ allMarked ? /* @__PURE__ */ jsx7("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
1914
+ props.enableRetry && passed ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1915
+ props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1916
+ ] });
1917
+ }
1918
+ var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
1919
+ var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
1920
+ return /* @__PURE__ */ jsx7(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx7(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1921
+ });
1922
+
1923
+ // src/blocks/FillInTheBlanks.tsx
1924
+ import React9, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef7, useState as useState7 } from "react";
1925
+
1926
+ // src/assessment/internal/parseStarDelimitedTemplate.ts
1927
+ function parseStarDelimitedTemplate(template, idPrefix) {
1928
+ const parts = [];
1929
+ const values = [];
1930
+ const re = /\*([^*]+)\*/g;
1931
+ let last = 0;
1932
+ let match;
1933
+ let n = 0;
1934
+ while ((match = re.exec(template)) !== null) {
1935
+ parts.push(template.slice(last, match.index));
1936
+ values.push(match[1].trim());
1937
+ parts.push(`${idPrefix}-${n++}`);
1938
+ last = match.index + match[0].length;
1939
+ }
1940
+ parts.push(template.slice(last));
1941
+ return { parts, values };
1942
+ }
1943
+
1944
+ // src/blocks/FillInTheBlanks.tsx
1945
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1946
+ var INTERACTION3 = "fillInBlanks";
1947
+ function parseTemplate(template) {
1948
+ const { parts, values } = parseStarDelimitedTemplate(template, "blank");
1949
+ return {
1950
+ parts,
1951
+ blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
1952
+ };
1953
+ }
1954
+ function FillInTheBlanksInner(props, ref) {
1955
+ const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1956
+ const assessment = useAssessmentState(props.enclosingLessonId);
1957
+ const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
1958
+ const blanks = props.blanks ?? parsed.blanks;
1959
+ const [values, setValues] = useState7(
1960
+ () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
1961
+ );
1962
+ const [passed, setPassed] = useState7(false);
1963
+ const [showSolutions, setShowSolutions] = useState7(false);
1964
+ const completedRef = useRef7(false);
1965
+ const answeredRef = useRef7(false);
1966
+ const reset = () => {
1967
+ completedRef.current = false;
1968
+ answeredRef.current = false;
1969
+ setPassed(false);
1970
+ setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
1971
+ setShowSolutions(false);
1972
+ };
1973
+ useEffect7(() => {
1974
+ reset();
1975
+ }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
1976
+ const hasBlanks = blanks.length > 0;
1977
+ const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
1978
+ let score = 0;
1979
+ blanks.forEach((b) => {
1980
+ if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
1981
+ });
1982
+ const maxScore = blanks.length;
1983
+ const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1984
+ const handle = useMemo9(
1985
+ () => buildAssessmentHandle({
1986
+ checkId,
1987
+ getScore: () => score,
1988
+ getMaxScore: () => maxScore || 1,
1989
+ getAnswerGiven: () => allFilled,
1990
+ resetTask: reset,
1991
+ showSolutions: () => setShowSolutions(true),
1992
+ getXAPIData: () => ({
1993
+ checkId,
1994
+ interactionType: INTERACTION3,
1995
+ response: values,
1996
+ correct: passedThreshold,
1997
+ score,
1998
+ maxScore: maxScore || 1
1999
+ }),
2000
+ getCurrentState: () => ({ values, passed, showSolutions }),
2001
+ resume: (state) => {
2002
+ const raw = state.values;
2003
+ if (raw && typeof raw === "object") setValues({ ...raw });
2004
+ readBooleanStateField(state, "passed", (value) => {
2005
+ setPassed(value);
2006
+ completedRef.current = value;
2007
+ answeredRef.current = value;
2008
+ });
2009
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
2010
+ }
2011
+ }),
2012
+ [allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
2013
+ );
2014
+ useAssessmentHandleRegistration(checkId, handle, ref);
2015
+ const check = () => {
2016
+ if (!hasBlanks) {
2017
+ if (isDevEnvironment4()) {
2018
+ console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
2019
+ }
2020
+ return;
2021
+ }
2022
+ if (!allFilled) return;
2023
+ if (!answeredRef.current) {
2024
+ answeredRef.current = true;
2025
+ assessment.answer({
2026
+ checkId,
2027
+ interactionType: INTERACTION3,
2028
+ question: props.template,
2029
+ response: values,
2030
+ correct: passedThreshold
2031
+ });
2032
+ }
2033
+ if (passedThreshold && !completedRef.current) {
2034
+ completedRef.current = true;
2035
+ setPassed(true);
2036
+ assessment.complete({
2037
+ checkId,
2038
+ interactionType: INTERACTION3,
2039
+ score,
2040
+ maxScore,
2041
+ passingScore: props.passingScore ?? maxScore
2042
+ });
2043
+ }
2044
+ };
2045
+ useEffect7(() => {
2046
+ if (!allFilled) answeredRef.current = false;
2047
+ }, [allFilled]);
2048
+ useEffect7(() => {
2049
+ if (props.autoCheck && allFilled) check();
2050
+ }, [allFilled, props.autoCheck, values, passedThreshold]);
2051
+ const reveal = showSolutions || passed && props.enableSolutionsButton;
2052
+ return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2053
+ /* @__PURE__ */ jsx8("p", { children: parsed.parts.map((part, i) => {
2054
+ const blank = blanks.find((b) => b.id === part);
2055
+ if (!blank) return /* @__PURE__ */ jsx8(React9.Fragment, { children: part }, i);
2056
+ return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
2057
+ /* @__PURE__ */ jsx8("span", { className: "lk-visually-hidden", children: blank.answer }),
2058
+ /* @__PURE__ */ jsx8(
2059
+ "input",
2060
+ {
2061
+ type: "text",
2062
+ "data-testid": `blank-${blank.id}`,
2063
+ "aria-label": `Blank ${blank.id}`,
2064
+ value: reveal ? blank.answer : values[blank.id] ?? "",
2065
+ readOnly: reveal,
2066
+ disabled: passed && !props.enableRetry,
2067
+ onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
2068
+ onBlur: () => props.autoCheck && check(),
2069
+ size: Math.max(8, blank.answer.length + 2)
2070
+ }
2071
+ )
2072
+ ] }, blank.id);
2073
+ }) }),
2074
+ !props.autoCheck ? /* @__PURE__ */ jsx8("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2075
+ !hasBlanks ? /* @__PURE__ */ jsx8("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2076
+ allFilled ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2077
+ props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2078
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2079
+ ] });
2080
+ }
2081
+ var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
2082
+ var FillInTheBlanks = forwardRef4(
2083
+ function FillInTheBlanks2(props, ref) {
2084
+ return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2085
+ }
2086
+ );
2087
+
2088
+ // src/blocks/DragTheWords.tsx
2089
+ import React10, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef8, useState as useState8 } from "react";
2090
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2091
+ var INTERACTION4 = "dragTheWords";
2092
+ function parseZones(template) {
2093
+ const { parts, values } = parseStarDelimitedTemplate(template, "zone");
2094
+ return { parts, answers: values };
2095
+ }
2096
+ function DragTheWordsInner(props, ref) {
2097
+ const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2098
+ const assessment = useAssessmentState(props.enclosingLessonId);
2099
+ const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
2100
+ const [zones, setZones] = useState8(
2101
+ () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
2102
+ );
2103
+ const [pool, setPool] = useState8(() => [...props.words]);
2104
+ const [keyboardWord, setKeyboardWord] = useState8(null);
2105
+ const [passed, setPassed] = useState8(false);
2106
+ const completedRef = useRef8(false);
2107
+ const answeredRef = useRef8(false);
2108
+ const reset = () => {
2109
+ completedRef.current = false;
2110
+ answeredRef.current = false;
2111
+ setPassed(false);
2112
+ setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
2113
+ setPool([...props.words]);
2114
+ setKeyboardWord(null);
2115
+ };
2116
+ useEffect8(() => {
2117
+ reset();
2118
+ }, [checkId, props.template, props.words.join("\0")]);
2119
+ const hasZones = answers.length > 0;
2120
+ const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
2121
+ let score = 0;
2122
+ answers.forEach((ans, i) => {
2123
+ if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
2124
+ });
2125
+ const maxScore = answers.length;
2126
+ const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2127
+ const handle = useMemo10(
2128
+ () => buildAssessmentHandle({
2129
+ checkId,
2130
+ getScore: () => score,
2131
+ getMaxScore: () => maxScore || 1,
2132
+ getAnswerGiven: () => allFilled,
2133
+ resetTask: reset,
2134
+ showSolutions: () => {
2135
+ },
2136
+ getXAPIData: () => ({
2137
+ checkId,
2138
+ interactionType: INTERACTION4,
2139
+ response: zones,
2140
+ correct: passedThreshold,
2141
+ score,
2142
+ maxScore: maxScore || 1
2143
+ }),
2144
+ getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
2145
+ resume: (state) => {
2146
+ const rawZones = state.zones;
2147
+ if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
2148
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2149
+ readBooleanStateField(state, "passed", (value) => {
2150
+ setPassed(value);
2151
+ completedRef.current = value;
2152
+ answeredRef.current = value;
2153
+ });
2154
+ const kw = state.keyboardWord;
2155
+ if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2156
+ }
2157
+ }),
2158
+ [allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
2159
+ );
2160
+ useAssessmentHandleRegistration(checkId, handle, ref);
2161
+ const placeInZone = (zoneId, word) => {
2162
+ if (passed && !props.enableRetry) return;
2163
+ const prev = zones[zoneId];
2164
+ setZones((z) => ({ ...z, [zoneId]: word }));
2165
+ setPool((p) => {
2166
+ const next = p.filter((w) => w !== word);
2167
+ if (prev) next.push(prev);
2168
+ return next;
2169
+ });
2170
+ setKeyboardWord(null);
2171
+ };
2172
+ const onDragStart = (word) => (e) => {
2173
+ e.dataTransfer.setData("text/plain", word);
2174
+ };
2175
+ const onDrop = (zoneId) => (e) => {
2176
+ e.preventDefault();
2177
+ const word = e.dataTransfer.getData("text/plain");
2178
+ if (word) placeInZone(zoneId, word);
2179
+ };
2180
+ const check = () => {
2181
+ if (!hasZones) {
2182
+ if (isDevEnvironment4()) {
2183
+ console.warn("[lessonkit] DragTheWords has no drop zones in template");
2184
+ }
2185
+ return;
2186
+ }
2187
+ if (!allFilled) return;
2188
+ if (!answeredRef.current) {
2189
+ answeredRef.current = true;
2190
+ assessment.answer({
2191
+ checkId,
2192
+ interactionType: INTERACTION4,
2193
+ question: props.template,
2194
+ response: zones,
2195
+ correct: passedThreshold
2196
+ });
2197
+ }
2198
+ if (passedThreshold && !completedRef.current) {
2199
+ completedRef.current = true;
2200
+ setPassed(true);
2201
+ assessment.complete({
2202
+ checkId,
2203
+ interactionType: INTERACTION4,
2204
+ score,
2205
+ maxScore,
2206
+ passingScore: props.passingScore ?? maxScore
2207
+ });
2208
+ }
2209
+ };
2210
+ useEffect8(() => {
2211
+ if (!allFilled) answeredRef.current = false;
2212
+ }, [allFilled]);
2213
+ useEffect8(() => {
2214
+ if (props.autoCheck && allFilled) check();
2215
+ }, [allFilled, props.autoCheck, zones, passedThreshold]);
2216
+ return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2217
+ /* @__PURE__ */ jsx9("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2218
+ /* @__PURE__ */ jsx9("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx9(
2219
+ "button",
2220
+ {
2221
+ type: "button",
2222
+ draggable: true,
2223
+ "data-testid": `word-${word}`,
2224
+ "aria-pressed": keyboardWord === word,
2225
+ onDragStart: onDragStart(word),
2226
+ onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
2227
+ style: { margin: "0.25rem" },
2228
+ children: word
2229
+ },
2230
+ word
2231
+ )) }),
2232
+ /* @__PURE__ */ jsx9("p", { children: parts.map((part, i) => {
2233
+ if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx9(React10.Fragment, { children: part }, i);
2234
+ return /* @__PURE__ */ jsx9(
2235
+ "span",
2236
+ {
2237
+ role: "button",
2238
+ tabIndex: 0,
2239
+ "data-testid": part,
2240
+ onDragOver: (e) => e.preventDefault(),
2241
+ onDrop: onDrop(part),
2242
+ onClick: () => keyboardWord && placeInZone(part, keyboardWord),
2243
+ onKeyDown: (e) => {
2244
+ if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
2245
+ },
2246
+ style: {
2247
+ display: "inline-block",
2248
+ minWidth: "6em",
2249
+ border: "1px dashed currentColor",
2250
+ padding: "0.2em 0.5em",
2251
+ margin: "0 0.2em"
2252
+ },
2253
+ children: zones[part] || "___"
2254
+ },
2255
+ part
2256
+ );
2257
+ }) }),
2258
+ /* @__PURE__ */ jsx9("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2259
+ !hasZones ? /* @__PURE__ */ jsx9("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2260
+ allFilled ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2261
+ ] });
2262
+ }
2263
+ var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
2264
+ var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
2265
+ return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2266
+ });
2267
+
2268
+ // src/blocks/DragAndDrop.tsx
2269
+ import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef9, useState as useState9 } from "react";
2270
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2271
+ var INTERACTION5 = "dragAndDrop";
2272
+ function DragAndDropInner(props, ref) {
2273
+ const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2274
+ const assessment = useAssessmentState(props.enclosingLessonId);
2275
+ const [assignments, setAssignments] = useState9(
2276
+ () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2277
+ );
2278
+ const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
2279
+ const [keyboardItem, setKeyboardItem] = useState9(null);
2280
+ const [passed, setPassed] = useState9(false);
2281
+ const completedRef = useRef9(false);
2282
+ const reset = () => {
2283
+ completedRef.current = false;
2284
+ setPassed(false);
2285
+ setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2286
+ setPool(props.items.map((i) => i.id));
2287
+ setKeyboardItem(null);
2288
+ };
2289
+ useEffect9(() => {
2290
+ reset();
2291
+ }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2292
+ const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2293
+ const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
2294
+ const handle = useMemo11(() => {
2295
+ const maxScore = props.targets.length || 1;
2296
+ let score = 0;
2297
+ props.targets.forEach((t) => {
2298
+ if (assignments[t.id] === t.accepts) score += 1;
2299
+ });
2300
+ return buildAssessmentHandle({
2301
+ checkId,
2302
+ getScore: () => score,
2303
+ getMaxScore: () => maxScore,
2304
+ getAnswerGiven: () => allFilled,
2305
+ resetTask: reset,
2306
+ showSolutions: () => {
2307
+ },
2308
+ getXAPIData: () => ({
2309
+ checkId,
2310
+ interactionType: INTERACTION5,
2311
+ response: assignments,
2312
+ correct: allCorrect,
2313
+ score,
2314
+ maxScore
2315
+ }),
2316
+ getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
2317
+ resume: (state) => {
2318
+ const rawAssignments = state.assignments;
2319
+ if (rawAssignments && typeof rawAssignments === "object") {
2320
+ setAssignments({ ...rawAssignments });
2321
+ }
2322
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2323
+ readBooleanStateField(state, "passed", (value) => {
2324
+ setPassed(value);
2325
+ completedRef.current = value;
2326
+ });
2327
+ const item = state.keyboardItem;
2328
+ if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
2329
+ }
2330
+ });
2331
+ }, [allCorrect, allFilled, assignments, checkId, keyboardItem, passed, pool, props.targets]);
2332
+ useAssessmentHandleRegistration(checkId, handle, ref);
2333
+ const place = (targetId, itemId) => {
2334
+ if (passed && !props.enableRetry) return;
2335
+ const prev = assignments[targetId];
2336
+ setAssignments((a) => ({ ...a, [targetId]: itemId }));
2337
+ setPool((p) => {
2338
+ const next = p.filter((id) => id !== itemId);
2339
+ if (prev) next.push(prev);
2340
+ return next;
2341
+ });
2342
+ setKeyboardItem(null);
2343
+ };
2344
+ const check = () => {
2345
+ if (!allFilled) return;
2346
+ assessment.answer({
2347
+ checkId,
2348
+ interactionType: INTERACTION5,
2349
+ response: assignments,
2350
+ correct: allCorrect
2351
+ });
2352
+ if (allCorrect && !completedRef.current) {
2353
+ completedRef.current = true;
2354
+ setPassed(true);
2355
+ assessment.complete({
2356
+ checkId,
2357
+ interactionType: INTERACTION5,
2358
+ score: props.targets.length,
2359
+ maxScore: props.targets.length,
2360
+ passingScore: props.passingScore ?? props.targets.length
2361
+ });
2362
+ }
2363
+ };
2364
+ return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2365
+ /* @__PURE__ */ jsx10("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2366
+ /* @__PURE__ */ jsx10("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
2367
+ const item = props.items.find((i) => i.id === id);
2368
+ return /* @__PURE__ */ jsx10(
2369
+ "button",
2370
+ {
2371
+ type: "button",
2372
+ draggable: true,
2373
+ "data-testid": `drag-item-${id}`,
2374
+ "aria-pressed": keyboardItem === id,
2375
+ onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2376
+ onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2377
+ style: { margin: "0.25rem" },
2378
+ children: item.label
2379
+ },
2380
+ id
2381
+ );
2382
+ }) }),
2383
+ /* @__PURE__ */ jsx10("ul", { children: props.targets.map((target) => {
2384
+ const assigned = assignments[target.id];
2385
+ const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2386
+ return /* @__PURE__ */ jsxs8("li", { children: [
2387
+ /* @__PURE__ */ jsx10("strong", { children: target.label }),
2388
+ " ",
2389
+ /* @__PURE__ */ jsx10(
2390
+ "span",
2391
+ {
2392
+ role: "button",
2393
+ tabIndex: 0,
2394
+ "data-testid": `drop-${target.id}`,
2395
+ onDragOver: (e) => e.preventDefault(),
2396
+ onDrop: (e) => {
2397
+ e.preventDefault();
2398
+ const id = e.dataTransfer.getData("text/plain");
2399
+ if (id) place(target.id, id);
2400
+ },
2401
+ onClick: () => keyboardItem && place(target.id, keyboardItem),
2402
+ onKeyDown: (e) => {
2403
+ if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2404
+ },
2405
+ style: {
2406
+ display: "inline-block",
2407
+ minWidth: "8em",
2408
+ border: "1px dashed currentColor",
2409
+ padding: "0.25em"
2410
+ },
2411
+ children: label
2412
+ }
2413
+ )
2414
+ ] }, target.id);
2415
+ }) }),
2416
+ /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2417
+ allFilled ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
2418
+ ] });
2419
+ }
2420
+ var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
2421
+ var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
2422
+ return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2423
+ });
2424
+
2425
+ // src/blocks/AssessmentSequence.tsx
2426
+ import React14, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo13, useState as useState10 } from "react";
2427
+
2428
+ // src/compound/useCompoundShell.ts
2429
+ import { useMemo as useMemo12 } from "react";
2430
+ import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
2431
+
2432
+ // src/compound/useCompoundNavigation.ts
2433
+ import { useCallback as useCallback4 } from "react";
2434
+ function useCompoundNavigation(pageCount, index, setIndex) {
2435
+ const goNext = useCallback4(() => {
2436
+ if (pageCount < 1) return;
2437
+ setIndex((i) => Math.min(i + 1, pageCount - 1));
2438
+ }, [pageCount, setIndex]);
2439
+ const goPrev = useCallback4(() => {
2440
+ setIndex((i) => Math.max(i - 1, 0));
2441
+ }, [setIndex]);
2442
+ const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
2443
+ return {
2444
+ index: clampedIndex,
2445
+ setIndex,
2446
+ goNext,
2447
+ goPrev,
2448
+ progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
2449
+ };
2450
+ }
2451
+
2452
+ // src/compound/useCompoundPersistence.ts
2453
+ import { useCallback as useCallback6, useEffect as useEffect11, useRef as useRef11 } from "react";
2454
+ import {
2455
+ clampCompoundPageIndex as clampCompoundPageIndex2,
2456
+ createCompoundResumeState as createCompoundResumeState2,
2457
+ createSessionStoragePort as createSessionStoragePort3,
2458
+ loadCompoundState as loadCompoundState2
2459
+ } from "@lessonkit/core";
2460
+
2461
+ // src/compound/useCompoundResume.ts
2462
+ import { useCallback as useCallback5, useEffect as useEffect10, useRef as useRef10 } from "react";
2463
+ import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
2464
+ import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
2465
+ function useCompoundResume(opts) {
2466
+ const storageRef = useRef10(opts.storage ?? createSessionStoragePort2());
2467
+ const resumedRef = useRef10(false);
2468
+ useEffect10(() => {
2469
+ if (!opts.enabled || !opts.courseId || resumedRef.current) return;
2470
+ const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
2471
+ if (saved) {
2472
+ resumedRef.current = true;
2473
+ opts.onResume?.(saved);
2474
+ }
2475
+ }, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
2476
+ return useCallback5(
2477
+ (state) => {
2478
+ if (!opts.enabled || !opts.courseId) return;
2479
+ saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
2480
+ },
2481
+ [opts.enabled, opts.courseId, opts.compoundId]
2482
+ );
2483
+ }
2484
+
2485
+ // src/compound/useCompoundPersistence.ts
2486
+ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort3()) {
2487
+ if (!enabled || !courseId || pageCount < 1) return 0;
2488
+ const saved = loadCompoundState2(storage, courseId, compoundId);
2489
+ if (!saved) return 0;
2490
+ return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
2491
+ }
2492
+ function useCompoundPersistence(opts) {
2493
+ const storage = opts.storage ?? createSessionStoragePort3();
2494
+ const ctx = useCompoundRegistry();
2495
+ const handlesVersion = useCompoundHandlesVersion();
2496
+ const pendingChildResumeRef = useRef11(null);
2497
+ const loadedChildStatesRef = useRef11({});
2498
+ const skipSaveUntilHydratedRef = useRef11(false);
2499
+ const buildState = useCallback6(() => {
2500
+ const childStates = {
2501
+ ...loadedChildStatesRef.current
2502
+ };
2503
+ if (ctx) {
2504
+ for (const [checkId, handle] of ctx.getHandles()) {
2505
+ if (handle.getCurrentState) {
2506
+ childStates[checkId] = handle.getCurrentState();
2507
+ delete loadedChildStatesRef.current[checkId];
2508
+ }
2509
+ }
2510
+ }
2511
+ return createCompoundResumeState2({
2512
+ activePageIndex: clampCompoundPageIndex2(opts.index, opts.pageCount),
2513
+ childStates
2514
+ });
2515
+ }, [ctx, opts.index, opts.pageCount]);
2516
+ const applyPendingChildResume = useCallback6(() => {
2517
+ const pending = pendingChildResumeRef.current;
2518
+ if (!pending || !ctx) return;
2519
+ const applied = resumeChildHandles(ctx.getHandles(), pending.childStates, { waitForHandles: true });
2520
+ if (!applied) return;
2521
+ pendingChildResumeRef.current = null;
2522
+ skipSaveUntilHydratedRef.current = false;
2523
+ }, [ctx]);
2524
+ const saveResume = useCompoundResume({
2525
+ courseId: opts.courseId,
2526
+ compoundId: opts.compoundId,
2527
+ enabled: opts.enabled,
2528
+ storage,
2529
+ onResume: (state) => {
2530
+ const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
2531
+ loadedChildStatesRef.current = { ...state.childStates };
2532
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2533
+ opts.setIndex(clamped);
2534
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped };
2535
+ queueMicrotask(() => applyPendingChildResume());
2536
+ }
2537
+ });
2538
+ useEffect11(() => {
2539
+ if (!opts.enabled || !opts.courseId) return;
2540
+ if (skipSaveUntilHydratedRef.current) return;
2541
+ saveResume(buildState());
2542
+ }, [
2543
+ opts.enabled,
2544
+ opts.courseId,
2545
+ opts.index,
2546
+ opts.pageCount,
2547
+ handlesVersion,
2548
+ saveResume,
2549
+ buildState
2550
+ ]);
2551
+ useEffect11(() => {
2552
+ applyPendingChildResume();
2553
+ }, [opts.index, handlesVersion, applyPendingChildResume]);
2554
+ }
2555
+
2556
+ // src/compound/useCompoundShell.ts
2557
+ function useCompoundShell(opts) {
2558
+ const ctx = useCompoundRegistry();
2559
+ useCompoundPersistence({
2560
+ courseId: opts.courseId,
2561
+ compoundId: opts.compoundId,
2562
+ pageCount: opts.pageCount,
2563
+ index: opts.index,
2564
+ setIndex: opts.setIndex,
2565
+ enabled: opts.persistEnabled,
2566
+ storage: opts.storage
2567
+ });
2568
+ const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
2569
+ const visibleIndex = clampCompoundPageIndex3(opts.index, opts.pageCount);
2570
+ useCompoundHandleRef(opts.ref, {
2571
+ activePageIndex: visibleIndex,
2572
+ setActivePageIndex: opts.setIndex,
2573
+ getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
2574
+ pageCount: opts.pageCount,
2575
+ enableSolutionsButton: opts.enableSolutionsButton
2576
+ });
2577
+ return { visibleIndex, goNext, goPrev, progress, ctx };
2578
+ }
2579
+ function useCompoundInitialIndex(opts) {
2580
+ return useMemo12(
2581
+ () => readCompoundInitialIndex(
2582
+ opts.courseId,
2583
+ opts.compoundId,
2584
+ opts.pageCount,
2585
+ opts.persistEnabled,
2586
+ opts.storage
2587
+ ),
2588
+ [opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
2589
+ );
2590
+ }
2591
+
2592
+ // src/compound/validateChildren.ts
2593
+ import React13 from "react";
2594
+ import {
2595
+ ACCORDION_FORBIDDEN_CHILD_TYPES,
2596
+ COMPOUND_MAX_NESTING_DEPTH,
2597
+ isChildTypeAllowed
2598
+ } from "@lessonkit/core";
2599
+
2600
+ // src/compound/blockType.ts
2601
+ var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
2602
+ function setLessonkitBlockType(component, blockType) {
2603
+ component[LESSONKIT_BLOCK_TYPE] = blockType;
2604
+ if (!component.displayName) {
2605
+ component.displayName = blockType;
2606
+ }
2607
+ return component;
2608
+ }
2609
+ function getLessonkitBlockType(component) {
2610
+ if (!component || typeof component !== "object" && typeof component !== "function") {
2611
+ return void 0;
2612
+ }
2613
+ const typed = component;
2614
+ return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
2615
+ }
2616
+
2617
+ // src/compound/validateChildren.ts
2618
+ var warnedPairs = /* @__PURE__ */ new Set();
2619
+ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
2620
+ "Page",
2621
+ "InteractiveBook",
2622
+ "AssessmentSequence"
2623
+ ]);
2624
+ function warnOrThrow(msg, strict) {
2625
+ if (strict) throw new Error(msg);
2626
+ if (!warnedPairs.has(msg)) {
2627
+ warnedPairs.add(msg);
2628
+ console.warn(msg);
2629
+ }
2630
+ }
2631
+ function validateNode(parent, node, depth, strict) {
2632
+ React13.Children.forEach(node, (child) => {
2633
+ if (!React13.isValidElement(child)) return;
2634
+ const blockType = getLessonkitBlockType(child.type);
2635
+ if (!blockType) {
2636
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
2637
+ validateNode(parent, child.props.children, depth, strict);
2638
+ }
2639
+ return;
2640
+ }
2641
+ if (!isChildTypeAllowed(parent, blockType)) {
2642
+ const key = `${parent}:${blockType}`;
2643
+ if (!warnedPairs.has(key)) {
2644
+ warnedPairs.add(key);
2645
+ const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
2646
+ if (strict) throw new Error(msg);
2647
+ console.warn(msg);
2648
+ }
2649
+ }
2650
+ if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
2651
+ const maxDepth = COMPOUND_MAX_NESTING_DEPTH[parent];
2652
+ if (depth >= maxDepth) {
2653
+ warnOrThrow(
2654
+ `[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
2655
+ strict
2656
+ );
2657
+ }
2658
+ const nestedParent = blockType;
2659
+ validateNode(nestedParent, child.props.children, depth + 1, strict);
2660
+ } else if (blockType === "Accordion") {
2661
+ const sections = child.props.sections;
2662
+ if (sections) validateAccordionSections(sections, strict);
2663
+ } else if (child.props && typeof child.props === "object" && "children" in child.props) {
2664
+ validateSubtreeForForbidden(
2665
+ child.props.children,
2666
+ ACCORDION_FORBIDDEN_CHILD_TYPES,
2667
+ strict
2668
+ );
2669
+ }
2670
+ });
2671
+ }
2672
+ function validateSubtreeForForbidden(node, forbidden, strict) {
2673
+ React13.Children.forEach(node, (child) => {
2674
+ if (!React13.isValidElement(child)) return;
2675
+ const blockType = getLessonkitBlockType(child.type);
2676
+ if (blockType && forbidden.includes(blockType)) {
2677
+ warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
2678
+ }
2679
+ if (blockType === "Accordion") {
2680
+ const sections = child.props.sections;
2681
+ if (sections) validateAccordionSections(sections, strict);
2682
+ return;
2683
+ }
2684
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
2685
+ validateSubtreeForForbidden(
2686
+ child.props.children,
2687
+ forbidden,
2688
+ strict
2689
+ );
2690
+ }
2691
+ });
2692
+ }
2693
+ function validateAccordionSections(sections, strict) {
2694
+ if (!isDevEnvironment4() && !strict) return;
2695
+ for (const section of sections) {
2696
+ validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
2697
+ }
2698
+ }
2699
+ function validateCompoundChildren(parent, children, strict) {
2700
+ if (!isDevEnvironment4() && !strict) return;
2701
+ validateNode(parent, children, 0, strict);
2702
+ }
2703
+
2704
+ // src/compound/warnPersistence.ts
2705
+ var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
2706
+ function warnSharedCompoundStorageKey(opts) {
2707
+ if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
2708
+ console.warn(
2709
+ `[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
2710
+ );
2711
+ }
2712
+
2713
+ // src/blocks/AssessmentSequence.tsx
2714
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2715
+ var AssessmentSequenceInner = forwardRef7(
2716
+ function AssessmentSequenceInner2(props, ref) {
2717
+ const { compoundId, childArray, index, setIndex, persistEnabled } = props;
2718
+ const sequential = props.sequential !== false;
2719
+ const { config } = useLessonkit();
2720
+ const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
2721
+ courseId: config.courseId,
2722
+ compoundId,
2723
+ pageCount: childArray.length,
2724
+ index,
2725
+ setIndex,
2726
+ persistEnabled,
2727
+ ref,
2728
+ enableSolutionsButton: props.enableSolutionsButton
2729
+ });
2730
+ validateCompoundChildren("AssessmentSequence", props.children);
2731
+ if (!sequential) {
2732
+ return /* @__PURE__ */ jsx11("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
2733
+ }
2734
+ return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
2735
+ /* @__PURE__ */ jsxs9("p", { children: [
2736
+ "Question ",
2737
+ progress.current,
2738
+ " of ",
2739
+ progress.total
2740
+ ] }),
2741
+ /* @__PURE__ */ jsx11("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx11("div", { hidden: i !== visibleIndex, children: child }, child.key ?? i)) }),
2742
+ /* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
2743
+ /* @__PURE__ */ jsx11(
2744
+ "button",
2745
+ {
2746
+ type: "button",
2747
+ "data-testid": "sequence-prev",
2748
+ disabled: visibleIndex === 0 || childArray.length === 0,
2749
+ onClick: goPrev,
2750
+ children: "Previous"
2751
+ }
2752
+ ),
2753
+ /* @__PURE__ */ jsx11(
2754
+ "button",
2755
+ {
2756
+ type: "button",
2757
+ "data-testid": "sequence-next",
2758
+ disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
2759
+ onClick: goNext,
2760
+ children: "Next"
2761
+ }
2762
+ )
2763
+ ] })
2764
+ ] });
2765
+ }
2766
+ );
2767
+ var AssessmentSequence = forwardRef7(
2768
+ function AssessmentSequence2(props, ref) {
2769
+ const compoundId = useMemo13(
2770
+ () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
2771
+ [props.blockId]
2772
+ );
2773
+ const childArray = React14.Children.toArray(props.children).filter(
2774
+ React14.isValidElement
2775
+ );
2776
+ const { config } = useLessonkit();
2777
+ const persistEnabled = config.session?.persistCompoundState !== false;
2778
+ useEffect12(() => {
2779
+ warnSharedCompoundStorageKey({
2780
+ persistEnabled,
2781
+ hasExplicitBlockId: Boolean(props.blockId),
2782
+ componentName: "AssessmentSequence"
2783
+ });
2784
+ }, [persistEnabled, props.blockId]);
2785
+ const initialIndex = useCompoundInitialIndex({
2786
+ courseId: config.courseId,
2787
+ compoundId,
2788
+ pageCount: childArray.length,
2789
+ persistEnabled
2790
+ });
2791
+ const [index, setIndex] = useState10(initialIndex);
2792
+ const setIndexStable = useCallback7((i) => setIndex(i), []);
2793
+ return /* @__PURE__ */ jsx11(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx11(
2794
+ AssessmentSequenceInner,
2795
+ {
2796
+ ...props,
2797
+ ref,
2798
+ compoundId,
2799
+ childArray,
2800
+ index,
2801
+ setIndex,
2802
+ persistEnabled
2803
+ }
2804
+ ) });
2805
+ }
2806
+ );
2807
+ setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
2808
+
2809
+ // src/blocks/Text.tsx
2810
+ import "react";
2811
+ import { jsx as jsx12 } from "react/jsx-runtime";
2812
+ function Text(props) {
2813
+ return /* @__PURE__ */ jsx12("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
2814
+ }
2815
+ setLessonkitBlockType(Text, "Text");
2816
+
2817
+ // src/blocks/Heading.tsx
2818
+ import { jsx as jsx13 } from "react/jsx-runtime";
2819
+ function Heading(props) {
2820
+ const Tag = `h${props.level}`;
2821
+ return /* @__PURE__ */ jsx13(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
2822
+ }
2823
+ setLessonkitBlockType(Heading, "Heading");
2824
+
2825
+ // src/blocks/Image.tsx
2826
+ import { jsx as jsx14 } from "react/jsx-runtime";
2827
+ function Image(props) {
2828
+ return /* @__PURE__ */ jsx14(
2829
+ "img",
2830
+ {
2831
+ src: props.src,
2832
+ alt: props.alt,
2833
+ "data-lk-block-id": props.blockId,
2834
+ "data-testid": props.blockId ? `image-${props.blockId}` : "image",
2835
+ style: { maxWidth: "100%", height: "auto" }
2836
+ }
2837
+ );
2838
+ }
2839
+ setLessonkitBlockType(Image, "Image");
2840
+
2841
+ // src/blocks/Page.tsx
2842
+ import { useEffect as useEffect13 } from "react";
2843
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2844
+ function Page(props) {
2845
+ validateCompoundChildren("Page", props.children);
2846
+ const { track } = useLessonkit();
2847
+ const lessonId = useEnclosingLessonId();
2848
+ useEffect13(() => {
2849
+ if (props.hidden || !lessonId) return;
2850
+ track(
2851
+ "compound_page_viewed",
2852
+ {
2853
+ blockId: props.blockId,
2854
+ pageIndex: props.pageIndex ?? 0,
2855
+ parentType: props.parentType
2856
+ },
2857
+ { lessonId }
2858
+ );
2859
+ }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
2860
+ return /* @__PURE__ */ jsxs10(
2861
+ "section",
2862
+ {
2863
+ "aria-label": props.title ?? "Page",
2864
+ "data-lk-block-id": props.blockId,
2865
+ "data-testid": `page-${props.blockId}`,
2866
+ hidden: props.hidden ? true : void 0,
2867
+ children: [
2868
+ props.title ? /* @__PURE__ */ jsx15("h3", { children: props.title }) : null,
2869
+ /* @__PURE__ */ jsx15("div", { children: props.children })
2870
+ ]
2871
+ }
2872
+ );
2873
+ }
2874
+ setLessonkitBlockType(Page, "Page");
2875
+
2876
+ // src/blocks/InteractiveBook.tsx
2877
+ import React17, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
2878
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
2879
+ var InteractiveBookInner = forwardRef8(
2880
+ function InteractiveBookInner2(props, ref) {
2881
+ const { blockId, pages, index, setIndex, persistEnabled } = props;
2882
+ validateCompoundChildren("InteractiveBook", pages);
2883
+ const { config, track } = useLessonkit();
2884
+ const lessonId = useEnclosingLessonId();
2885
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
2886
+ courseId: config.courseId,
2887
+ compoundId: blockId,
2888
+ pageCount: pages.length,
2889
+ index,
2890
+ setIndex,
2891
+ persistEnabled,
2892
+ ref
2893
+ });
2894
+ const pageTitles = useMemo14(
2895
+ () => pages.map((page) => page.props.title),
2896
+ [pages]
2897
+ );
2898
+ useEffect14(() => {
2899
+ if (!lessonId || pages.length === 0) return;
2900
+ track(
2901
+ "book_page_viewed",
2902
+ {
2903
+ blockId,
2904
+ pageIndex: visibleIndex,
2905
+ pageTitle: pageTitles[visibleIndex]
2906
+ },
2907
+ { lessonId }
2908
+ );
2909
+ }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
2910
+ return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
2911
+ /* @__PURE__ */ jsx16("h3", { children: props.title }),
2912
+ /* @__PURE__ */ jsxs11("p", { children: [
2913
+ "Page ",
2914
+ progress.current,
2915
+ " of ",
2916
+ progress.total
2917
+ ] }),
2918
+ props.showBookScore && ctx ? /* @__PURE__ */ jsxs11("p", { "data-testid": "book-score", children: [
2919
+ "Score: ",
2920
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
2921
+ " /",
2922
+ " ",
2923
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
2924
+ ] }) : null,
2925
+ /* @__PURE__ */ jsx16("div", { "data-testid": "interactive-book-page", children: pages.map(
2926
+ (page, i) => React17.cloneElement(page, {
2927
+ key: page.key ?? page.props.blockId,
2928
+ hidden: i !== visibleIndex,
2929
+ pageIndex: i,
2930
+ parentType: "InteractiveBook"
2931
+ })
2932
+ ) }),
2933
+ /* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
2934
+ /* @__PURE__ */ jsx16(
2935
+ "button",
2936
+ {
2937
+ type: "button",
2938
+ "data-testid": "book-prev",
2939
+ disabled: visibleIndex === 0 || pages.length === 0,
2940
+ onClick: goPrev,
2941
+ children: "Previous"
2942
+ }
2943
+ ),
2944
+ /* @__PURE__ */ jsx16(
2945
+ "button",
2946
+ {
2947
+ type: "button",
2948
+ "data-testid": "book-next",
2949
+ disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
2950
+ onClick: goNext,
2951
+ children: "Next"
2952
+ }
2953
+ )
2954
+ ] })
2955
+ ] });
2956
+ }
2957
+ );
2958
+ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
2959
+ const blockId = useMemo14(
2960
+ () => normalizeComponentId(props.blockId, "blockId"),
2961
+ [props.blockId]
2962
+ );
2963
+ const pages = React17.Children.toArray(props.children).filter(
2964
+ React17.isValidElement
2965
+ );
2966
+ const { config } = useLessonkit();
2967
+ const persistEnabled = config.session?.persistCompoundState !== false;
2968
+ const initialIndex = useCompoundInitialIndex({
2969
+ courseId: config.courseId,
2970
+ compoundId: blockId,
2971
+ pageCount: pages.length,
2972
+ persistEnabled
2973
+ });
2974
+ const [index, setIndex] = useState11(initialIndex);
2975
+ const setIndexStable = useCallback8((i) => setIndex(i), []);
2976
+ return /* @__PURE__ */ jsx16(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx16(
2977
+ InteractiveBookInner,
2978
+ {
2979
+ ...props,
2980
+ ref,
2981
+ blockId,
2982
+ pages,
2983
+ index,
2984
+ setIndex,
2985
+ persistEnabled
2986
+ }
2987
+ ) });
2988
+ });
2989
+ setLessonkitBlockType(InteractiveBook, "InteractiveBook");
2990
+
2991
+ // src/blocks/Accordion.tsx
2992
+ import { useId as useId3, useState as useState12 } from "react";
2993
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
2994
+ function Accordion(props) {
2995
+ if (isDevEnvironment4()) {
2996
+ validateAccordionSections(props.sections);
2997
+ }
2998
+ const [open, setOpen] = useState12(/* @__PURE__ */ new Set());
2999
+ const { track } = useLessonkit();
3000
+ const lessonId = useEnclosingLessonId();
3001
+ const baseId = useId3();
3002
+ const toggle = (sectionId) => {
3003
+ setOpen((prev) => {
3004
+ const next = new Set(prev);
3005
+ const expanded = !next.has(sectionId);
3006
+ if (expanded) next.add(sectionId);
3007
+ else next.delete(sectionId);
3008
+ track(
3009
+ "accordion_section_toggled",
3010
+ { blockId: props.blockId, sectionId, expanded },
3011
+ lessonId ? { lessonId } : void 0
3012
+ );
3013
+ return next;
3014
+ });
3015
+ };
3016
+ return /* @__PURE__ */ jsx17("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3017
+ const expanded = open.has(section.id);
3018
+ const panelId = `${baseId}-${section.id}`;
3019
+ const triggerId = `${baseId}-trigger-${section.id}`;
3020
+ return /* @__PURE__ */ jsxs12("div", { "data-testid": `accordion-section-${section.id}`, children: [
3021
+ /* @__PURE__ */ jsx17("h4", { children: /* @__PURE__ */ jsx17(
3022
+ "button",
3023
+ {
3024
+ id: triggerId,
3025
+ type: "button",
3026
+ "aria-expanded": expanded,
3027
+ "aria-controls": panelId,
3028
+ "data-testid": `accordion-trigger-${section.id}`,
3029
+ onClick: () => toggle(section.id),
3030
+ children: section.title
3031
+ }
3032
+ ) }),
3033
+ expanded ? /* @__PURE__ */ jsx17("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3034
+ ] }, section.id);
3035
+ }) });
3036
+ }
3037
+ setLessonkitBlockType(Accordion, "Accordion");
3038
+
3039
+ // src/blocks/DialogCards.tsx
3040
+ import { useState as useState13 } from "react";
3041
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3042
+ function DialogCards(props) {
3043
+ const [index, setIndex] = useState13(0);
3044
+ const [flipped, setFlipped] = useState13(false);
3045
+ const card = props.cards[index];
3046
+ if (!card) return null;
3047
+ return /* @__PURE__ */ jsxs13("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3048
+ /* @__PURE__ */ jsxs13("p", { children: [
3049
+ "Card ",
3050
+ index + 1,
3051
+ " of ",
3052
+ props.cards.length
3053
+ ] }),
3054
+ /* @__PURE__ */ jsx18(
3055
+ "button",
3056
+ {
3057
+ type: "button",
3058
+ "data-testid": "dialog-card-flip",
3059
+ "aria-pressed": flipped,
3060
+ onClick: () => setFlipped((f) => !f),
3061
+ style: { minHeight: "6rem", width: "100%" },
3062
+ children: flipped ? card.back : card.front
3063
+ }
3064
+ ),
3065
+ /* @__PURE__ */ jsxs13("nav", { "aria-label": "Card navigation", children: [
3066
+ /* @__PURE__ */ jsx18(
3067
+ "button",
3068
+ {
3069
+ type: "button",
3070
+ "data-testid": "dialog-prev",
3071
+ disabled: index === 0,
3072
+ onClick: () => {
3073
+ setIndex((i) => i - 1);
3074
+ setFlipped(false);
3075
+ },
3076
+ children: "Previous"
3077
+ }
3078
+ ),
3079
+ /* @__PURE__ */ jsx18(
3080
+ "button",
3081
+ {
3082
+ type: "button",
3083
+ "data-testid": "dialog-next",
3084
+ disabled: index >= props.cards.length - 1,
3085
+ onClick: () => {
3086
+ setIndex((i) => i + 1);
3087
+ setFlipped(false);
3088
+ },
3089
+ children: "Next"
3090
+ }
3091
+ )
3092
+ ] })
3093
+ ] });
3094
+ }
3095
+ setLessonkitBlockType(DialogCards, "DialogCards");
3096
+
3097
+ // src/blocks/Flashcards.tsx
3098
+ import { useState as useState14 } from "react";
3099
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3100
+ function Flashcards(props) {
3101
+ const [index, setIndex] = useState14(0);
3102
+ const [face, setFace] = useState14("front");
3103
+ const { track } = useLessonkit();
3104
+ const lessonId = useEnclosingLessonId();
3105
+ const card = props.cards[index];
3106
+ if (!card) return null;
3107
+ const flip = () => {
3108
+ const next = face === "front" ? "back" : "front";
3109
+ setFace(next);
3110
+ track(
3111
+ "flashcard_flipped",
3112
+ { blockId: props.blockId, cardIndex: index, face: next },
3113
+ lessonId ? { lessonId } : void 0
3114
+ );
3115
+ };
3116
+ return /* @__PURE__ */ jsxs14("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3117
+ /* @__PURE__ */ jsx19("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3118
+ props.selfScore ? /* @__PURE__ */ jsx19("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3119
+ /* @__PURE__ */ jsx19(
3120
+ "button",
3121
+ {
3122
+ type: "button",
3123
+ "data-testid": "flashcard-next",
3124
+ disabled: index >= props.cards.length - 1,
3125
+ onClick: () => {
3126
+ setIndex((i) => i + 1);
3127
+ setFace("front");
3128
+ },
3129
+ children: "Next card"
3130
+ }
3131
+ )
3132
+ ] });
3133
+ }
3134
+ setLessonkitBlockType(Flashcards, "Flashcards");
3135
+
3136
+ // src/blocks/ImageHotspots.tsx
3137
+ import { useState as useState15 } from "react";
3138
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
3139
+ function ImageHotspots(props) {
3140
+ const [active, setActive] = useState15(null);
3141
+ const { track } = useLessonkit();
3142
+ const lessonId = useEnclosingLessonId();
3143
+ const open = (hotspotId) => {
3144
+ setActive(hotspotId);
3145
+ track(
3146
+ "hotspot_opened",
3147
+ { blockId: props.blockId, hotspotId },
3148
+ lessonId ? { lessonId } : void 0
3149
+ );
3150
+ };
3151
+ return /* @__PURE__ */ jsxs15("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3152
+ /* @__PURE__ */ jsxs15("div", { style: { position: "relative", display: "inline-block" }, children: [
3153
+ /* @__PURE__ */ jsx20("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3154
+ props.hotspots.map((h) => /* @__PURE__ */ jsx20(
3155
+ "button",
3156
+ {
3157
+ type: "button",
3158
+ "aria-expanded": active === h.id,
3159
+ "aria-label": h.label,
3160
+ "data-testid": `hotspot-${h.id}`,
3161
+ style: {
3162
+ position: "absolute",
3163
+ left: `${h.x}%`,
3164
+ top: `${h.y}%`,
3165
+ transform: "translate(-50%, -50%)"
3166
+ },
3167
+ onClick: () => open(h.id),
3168
+ children: "+"
3169
+ },
3170
+ h.id
3171
+ ))
3172
+ ] }),
3173
+ active ? /* @__PURE__ */ jsxs15("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3174
+ props.hotspots.find((h) => h.id === active)?.content,
3175
+ /* @__PURE__ */ jsx20("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3176
+ ] }) : null
3177
+ ] });
3178
+ }
3179
+ setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3180
+
3181
+ // src/blocks/ImageSlider.tsx
3182
+ import { useState as useState16 } from "react";
3183
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
3184
+ function ImageSlider(props) {
3185
+ const [index, setIndex] = useState16(0);
3186
+ const { track } = useLessonkit();
3187
+ const lessonId = useEnclosingLessonId();
3188
+ const slide = props.slides[index];
3189
+ if (!slide) return null;
3190
+ const goTo = (next) => {
3191
+ setIndex(next);
3192
+ track(
3193
+ "image_slider_changed",
3194
+ { blockId: props.blockId, slideIndex: next },
3195
+ lessonId ? { lessonId } : void 0
3196
+ );
3197
+ };
3198
+ return /* @__PURE__ */ jsxs16("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
3199
+ /* @__PURE__ */ jsx21("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
3200
+ slide.caption ? /* @__PURE__ */ jsx21("p", { children: slide.caption }) : null,
3201
+ /* @__PURE__ */ jsxs16("nav", { "aria-label": "Slide navigation", children: [
3202
+ /* @__PURE__ */ jsx21(
3203
+ "button",
3204
+ {
3205
+ type: "button",
3206
+ "data-testid": "slider-prev",
3207
+ disabled: index === 0,
3208
+ onClick: () => goTo(index - 1),
3209
+ children: "Previous"
3210
+ }
3211
+ ),
3212
+ /* @__PURE__ */ jsxs16("span", { children: [
3213
+ index + 1,
3214
+ " / ",
3215
+ props.slides.length
3216
+ ] }),
3217
+ /* @__PURE__ */ jsx21(
3218
+ "button",
3219
+ {
3220
+ type: "button",
3221
+ "data-testid": "slider-next",
3222
+ disabled: index >= props.slides.length - 1,
3223
+ onClick: () => goTo(index + 1),
3224
+ children: "Next"
3225
+ }
3226
+ )
3227
+ ] })
3228
+ ] });
3229
+ }
3230
+ setLessonkitBlockType(ImageSlider, "ImageSlider");
3231
+
3232
+ // src/blocks/FindHotspot.tsx
3233
+ import { forwardRef as forwardRef9, useMemo as useMemo15, useState as useState17 } from "react";
3234
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
3235
+ var INTERACTION6 = "findHotspot";
3236
+ function FindHotspotInner(props, ref) {
3237
+ const checkId = useMemo15(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3238
+ const [selected, setSelected] = useState17(null);
3239
+ const [checked, setChecked] = useState17(false);
3240
+ const assessment = useAssessmentState(props.enclosingLessonId);
3241
+ const correct = selected === props.correctTargetId;
3242
+ const handle = useMemo15(
3243
+ () => buildAssessmentHandle({
3244
+ checkId,
3245
+ getScore: () => checked && correct ? 1 : 0,
3246
+ getMaxScore: () => 1,
3247
+ getAnswerGiven: () => selected !== null,
3248
+ resetTask: () => {
3249
+ setSelected(null);
3250
+ setChecked(false);
3251
+ },
3252
+ showSolutions: () => setSelected(props.correctTargetId),
3253
+ getXAPIData: () => ({
3254
+ checkId,
3255
+ interactionType: INTERACTION6,
3256
+ response: selected ?? void 0,
3257
+ correct: checked ? correct : void 0,
3258
+ score: checked && correct ? 1 : 0,
3259
+ maxScore: 1
3260
+ }),
3261
+ getCurrentState: () => ({ selected, checked }),
3262
+ resume: (state) => {
3263
+ const nextSelected = readStringField(state, "selected");
3264
+ if (typeof nextSelected === "string") setSelected(nextSelected);
3265
+ readBooleanStateField(state, "checked", setChecked);
3266
+ }
3267
+ }),
3268
+ [checkId, selected, checked, correct, props.correctTargetId]
3269
+ );
3270
+ useAssessmentHandleRegistration(checkId, handle, ref);
3271
+ const submit = () => {
3272
+ if (!selected) return;
3273
+ setChecked(true);
3274
+ assessment.answer({
3275
+ checkId,
3276
+ interactionType: INTERACTION6,
3277
+ response: selected,
3278
+ correct
3279
+ });
3280
+ if (correct) {
3281
+ assessment.complete({
3282
+ checkId,
3283
+ interactionType: INTERACTION6,
3284
+ score: 1,
3285
+ maxScore: 1,
3286
+ passingScore: props.passingScore
3287
+ });
3288
+ }
3289
+ };
3290
+ return /* @__PURE__ */ jsxs17("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
3291
+ /* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
3292
+ /* @__PURE__ */ jsx22("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3293
+ props.targets.map((t) => /* @__PURE__ */ jsx22(
3294
+ "button",
3295
+ {
3296
+ type: "button",
3297
+ "aria-label": t.label,
3298
+ "aria-pressed": selected === t.id,
3299
+ "data-testid": `target-${t.id}`,
3300
+ style: {
3301
+ position: "absolute",
3302
+ left: `${t.x}%`,
3303
+ top: `${t.y}%`,
3304
+ transform: "translate(-50%, -50%)"
3305
+ },
3306
+ onClick: () => setSelected(t.id),
3307
+ children: t.label
3308
+ },
3309
+ t.id
3310
+ ))
3311
+ ] }),
3312
+ /* @__PURE__ */ jsx22("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
3313
+ checked ? /* @__PURE__ */ jsx22("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
3314
+ ] });
1252
3315
  }
3316
+ var FindHotspotInnerForwarded = forwardRef9(FindHotspotInner);
3317
+ var FindHotspot = forwardRef9(function FindHotspot2(props, ref) {
3318
+ return /* @__PURE__ */ jsx22(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx22(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
3319
+ });
3320
+ setLessonkitBlockType(FindHotspot, "FindHotspot");
3321
+
3322
+ // src/blocks/FindMultipleHotspots.tsx
3323
+ import { forwardRef as forwardRef10, useMemo as useMemo16, useState as useState18 } from "react";
3324
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
3325
+ var INTERACTION7 = "findMultipleHotspots";
3326
+ function FindMultipleHotspotsInner(props, ref) {
3327
+ const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3328
+ const [selected, setSelected] = useState18(/* @__PURE__ */ new Set());
3329
+ const [checked, setChecked] = useState18(false);
3330
+ const assessment = useAssessmentState(props.enclosingLessonId);
3331
+ const toggle = (id) => {
3332
+ setSelected((prev) => {
3333
+ const next = new Set(prev);
3334
+ if (next.has(id)) next.delete(id);
3335
+ else next.add(id);
3336
+ return next;
3337
+ });
3338
+ };
3339
+ const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
3340
+ const handle = useMemo16(
3341
+ () => buildAssessmentHandle({
3342
+ checkId,
3343
+ getScore: () => checked && correct ? 1 : 0,
3344
+ getMaxScore: () => 1,
3345
+ getAnswerGiven: () => selected.size > 0,
3346
+ resetTask: () => {
3347
+ setSelected(/* @__PURE__ */ new Set());
3348
+ setChecked(false);
3349
+ },
3350
+ showSolutions: () => setSelected(new Set(props.correctTargetIds)),
3351
+ getXAPIData: () => ({
3352
+ checkId,
3353
+ interactionType: INTERACTION7,
3354
+ response: [...selected],
3355
+ correct: checked ? correct : void 0,
3356
+ score: checked && correct ? 1 : 0,
3357
+ maxScore: 1
3358
+ }),
3359
+ getCurrentState: () => ({ selected: [...selected], checked }),
3360
+ resume: (state) => {
3361
+ const raw = state.selected;
3362
+ if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
3363
+ readBooleanStateField(state, "checked", setChecked);
3364
+ }
3365
+ }),
3366
+ [checkId, selected, checked, correct, props.correctTargetIds]
3367
+ );
3368
+ useAssessmentHandleRegistration(checkId, handle, ref);
3369
+ const submit = () => {
3370
+ if (selected.size === 0) return;
3371
+ setChecked(true);
3372
+ assessment.answer({
3373
+ checkId,
3374
+ interactionType: INTERACTION7,
3375
+ response: [...selected],
3376
+ correct
3377
+ });
3378
+ if (correct) {
3379
+ assessment.complete({
3380
+ checkId,
3381
+ interactionType: INTERACTION7,
3382
+ score: 1,
3383
+ maxScore: 1,
3384
+ passingScore: props.passingScore
3385
+ });
3386
+ }
3387
+ };
3388
+ return /* @__PURE__ */ jsxs18("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
3389
+ /* @__PURE__ */ jsxs18("div", { style: { position: "relative", display: "inline-block" }, children: [
3390
+ /* @__PURE__ */ jsx23("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3391
+ props.targets.map((t) => /* @__PURE__ */ jsx23(
3392
+ "button",
3393
+ {
3394
+ type: "button",
3395
+ "aria-label": t.label,
3396
+ "aria-pressed": selected.has(t.id),
3397
+ "data-testid": `target-${t.id}`,
3398
+ style: {
3399
+ position: "absolute",
3400
+ left: `${t.x}%`,
3401
+ top: `${t.y}%`,
3402
+ transform: "translate(-50%, -50%)"
3403
+ },
3404
+ onClick: () => toggle(t.id),
3405
+ children: t.label
3406
+ },
3407
+ t.id
3408
+ ))
3409
+ ] }),
3410
+ /* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
3411
+ checked ? /* @__PURE__ */ jsx23("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
3412
+ ] });
3413
+ }
3414
+ var FindMultipleHotspotsInnerForwarded = forwardRef10(FindMultipleHotspotsInner);
3415
+ var FindMultipleHotspots = forwardRef10(
3416
+ function FindMultipleHotspots2(props, ref) {
3417
+ return /* @__PURE__ */ jsx23(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx23(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
3418
+ }
3419
+ );
3420
+ setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
1253
3421
 
1254
3422
  // src/index.tsx
1255
3423
  import {
@@ -1263,14 +3431,14 @@ import {
1263
3431
  } from "@lessonkit/core";
1264
3432
 
1265
3433
  // src/theme/ThemeProvider.tsx
1266
- import React3, {
1267
- createContext as createContext3,
1268
- useCallback as useCallback2,
1269
- useContext as useContext3,
3434
+ import React25, {
3435
+ createContext as createContext4,
3436
+ useCallback as useCallback9,
3437
+ useContext as useContext4,
1270
3438
  useLayoutEffect as useLayoutEffect2,
1271
- useMemo as useMemo4,
1272
- useRef as useRef3,
1273
- useState as useState3
3439
+ useMemo as useMemo17,
3440
+ useRef as useRef12,
3441
+ useState as useState19
1274
3442
  } from "react";
1275
3443
  import {
1276
3444
  brandThemeOverrides,
@@ -1297,9 +3465,12 @@ function applyCssVariables(target, vars, previousKeys) {
1297
3465
  }
1298
3466
 
1299
3467
  // src/theme/ThemeProvider.tsx
1300
- import { jsx as jsx3 } from "react/jsx-runtime";
1301
- var ThemeContext = createContext3(null);
1302
- var useIsoLayoutEffect2 = typeof window !== "undefined" ? useLayoutEffect2 : React3.useEffect;
3468
+ import { jsx as jsx24 } from "react/jsx-runtime";
3469
+ var ThemeContext = createContext4(null);
3470
+ var useIsoLayoutEffect2 = (
3471
+ /* v8 ignore next -- SSR uses useEffect when window is unavailable */
3472
+ typeof window !== "undefined" ? useLayoutEffect2 : React25.useEffect
3473
+ );
1303
3474
  function getSystemMode() {
1304
3475
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1305
3476
  return "light";
@@ -1317,7 +3488,7 @@ function ThemeProvider(props) {
1317
3488
  const preset = props.preset ?? "default";
1318
3489
  const mode = props.mode ?? "light";
1319
3490
  const targetKind = props.target ?? "document";
1320
- const [resolvedMode, setResolvedMode] = useState3(
3491
+ const [resolvedMode, setResolvedMode] = useState19(
1321
3492
  () => mode === "system" ? getSystemMode() : mode
1322
3493
  );
1323
3494
  useIsoLayoutEffect2(() => {
@@ -1333,20 +3504,20 @@ function ThemeProvider(props) {
1333
3504
  return () => mq.removeEventListener("change", onChange);
1334
3505
  }, [mode]);
1335
3506
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
1336
- const effectiveTheme = useMemo4(() => {
3507
+ const effectiveTheme = useMemo17(() => {
1337
3508
  const modeBase = resolveModeBase(mode, dataTheme);
1338
3509
  const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
1339
3510
  return mergeThemes(base, props.theme ?? {});
1340
3511
  }, [preset, mode, dataTheme, props.theme]);
1341
- const hostRef = useRef3(null);
1342
- const appliedKeysRef = useRef3(/* @__PURE__ */ new Set());
3512
+ const hostRef = useRef12(null);
3513
+ const appliedKeysRef = useRef12(/* @__PURE__ */ new Set());
1343
3514
  useIsoLayoutEffect2(() => {
1344
3515
  if (targetKind === "document" && typeof document !== "undefined") {
1345
3516
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
1346
3517
  return () => document.documentElement.removeAttribute("data-lk-theme");
1347
3518
  }
1348
3519
  }, [targetKind, dataTheme]);
1349
- const inject = useCallback2(() => {
3520
+ const inject = useCallback9(() => {
1350
3521
  const vars = themeToCssVariables(effectiveTheme);
1351
3522
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
1352
3523
  if (!el) return;
@@ -1363,7 +3534,7 @@ function ThemeProvider(props) {
1363
3534
  appliedKeysRef.current = /* @__PURE__ */ new Set();
1364
3535
  };
1365
3536
  }, [inject, targetKind]);
1366
- const value = useMemo4(
3537
+ const value = useMemo17(
1367
3538
  () => ({
1368
3539
  theme: effectiveTheme,
1369
3540
  preset,
@@ -1373,20 +3544,275 @@ function ThemeProvider(props) {
1373
3544
  [effectiveTheme, preset, mode, dataTheme]
1374
3545
  );
1375
3546
  if (targetKind === "document") {
1376
- return /* @__PURE__ */ jsx3(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx3("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
3547
+ return /* @__PURE__ */ jsx24(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx24("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
1377
3548
  }
1378
- return /* @__PURE__ */ jsx3(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx3("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
3549
+ return /* @__PURE__ */ jsx24(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx24("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
1379
3550
  }
1380
3551
  function useTheme() {
1381
- const ctx = useContext3(ThemeContext);
3552
+ const ctx = useContext4(ThemeContext);
1382
3553
  if (!ctx) {
1383
3554
  throw new Error("useTheme must be used within a ThemeProvider");
1384
3555
  }
1385
3556
  return ctx;
1386
3557
  }
1387
3558
 
3559
+ // src/catalogV3Entries.ts
3560
+ import {
3561
+ ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
3562
+ INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
3563
+ PAGE_ALLOWED_CHILD_TYPES,
3564
+ COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
3565
+ } from "@lessonkit/core";
3566
+ var COMPOUND_PARENTS = ["Lesson", "Page", "InteractiveBook", "AssessmentSequence"];
3567
+ function extendParents(entry) {
3568
+ if (!entry.parentConstraints?.length) return entry;
3569
+ const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
3570
+ return { ...entry, parentConstraints: [...merged] };
3571
+ }
3572
+ var assessmentBehaviourProps = [
3573
+ { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
3574
+ { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
3575
+ { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
3576
+ { name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
3577
+ ];
3578
+ var v3CompoundAndContentEntries = [
3579
+ {
3580
+ type: "Text",
3581
+ category: "content",
3582
+ description: "Paragraph text content.",
3583
+ props: [
3584
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
3585
+ { name: "children", type: "ReactNode", required: true, description: "Text body." }
3586
+ ],
3587
+ requiredIds: [],
3588
+ parentConstraints: [...COMPOUND_PARENTS],
3589
+ a11y: { element: "p", ariaLabel: "Text", keyboard: "N/A", notes: "Semantic paragraph." },
3590
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
3591
+ telemetry: { emits: [] }
3592
+ },
3593
+ {
3594
+ type: "Heading",
3595
+ category: "content",
3596
+ description: "Heading levels 1\u20133.",
3597
+ props: [
3598
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
3599
+ { name: "level", type: "1 | 2 | 3", required: true, description: "Heading level." },
3600
+ { name: "children", type: "ReactNode", required: true, description: "Heading text." }
3601
+ ],
3602
+ requiredIds: [],
3603
+ parentConstraints: [...COMPOUND_PARENTS],
3604
+ a11y: { element: "h1-h3", ariaLabel: "Heading", keyboard: "N/A", notes: "Use one level per outline." },
3605
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
3606
+ telemetry: { emits: [] }
3607
+ },
3608
+ {
3609
+ type: "Image",
3610
+ category: "content",
3611
+ description: "Image with required alt text.",
3612
+ props: [
3613
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
3614
+ { name: "src", type: "string", required: true, description: "Image URL." },
3615
+ { name: "alt", type: "string", required: true, description: "Alt text." }
3616
+ ],
3617
+ requiredIds: [],
3618
+ parentConstraints: [...COMPOUND_PARENTS],
3619
+ a11y: { element: "img", ariaLabel: "Image", keyboard: "N/A", notes: "Requires alt." },
3620
+ theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
3621
+ telemetry: { emits: [] }
3622
+ },
3623
+ {
3624
+ type: "Page",
3625
+ category: "container",
3626
+ compoundContract: true,
3627
+ h5pMachineName: "H5P.Column",
3628
+ h5pAlias: "Column",
3629
+ description: "Column layout container (H5P Column / Page).",
3630
+ allowedChildTypes: [...PAGE_ALLOWED_CHILD_TYPES],
3631
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Page,
3632
+ props: [
3633
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3634
+ { name: "title", type: "string", required: false, description: "Page title." },
3635
+ { name: "children", type: "ReactNode", required: true, description: "Page content." }
3636
+ ],
3637
+ requiredIds: [],
3638
+ optionalIds: ["blockId"],
3639
+ parentConstraints: ["Lesson", "InteractiveBook"],
3640
+ a11y: { element: "section", ariaLabel: "Page", keyboard: "N/A", notes: "H5P Column equivalent." },
3641
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
3642
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
3643
+ },
3644
+ {
3645
+ type: "InteractiveBook",
3646
+ category: "container",
3647
+ compoundContract: true,
3648
+ h5pMachineName: "H5P.InteractiveBook",
3649
+ h5pAlias: "Interactive Book",
3650
+ description: "Multi-page book with chapter navigation.",
3651
+ allowedChildTypes: [...INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
3652
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.InteractiveBook,
3653
+ props: [
3654
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3655
+ { name: "title", type: "string", required: true, description: "Book title." },
3656
+ { name: "showBookScore", type: "boolean", required: false, description: "Show aggregate score." },
3657
+ { name: "children", type: "Page[]", required: true, description: "Page chapters." }
3658
+ ],
3659
+ requiredIds: ["blockId"],
3660
+ parentConstraints: ["Lesson"],
3661
+ a11y: {
3662
+ element: "section",
3663
+ ariaLabel: "Interactive book",
3664
+ keyboard: "Previous/Next chapter navigation.",
3665
+ notes: "H5P Interactive Book equivalent."
3666
+ },
3667
+ theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
3668
+ telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
3669
+ },
3670
+ {
3671
+ type: "Accordion",
3672
+ category: "content",
3673
+ h5pMachineName: "H5P.Accordion",
3674
+ h5pAlias: "Accordion",
3675
+ description: "Expandable sections.",
3676
+ props: [
3677
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3678
+ { name: "sections", type: "AccordionSection[]", required: true, description: "Sections." }
3679
+ ],
3680
+ requiredIds: ["blockId"],
3681
+ parentConstraints: [...COMPOUND_PARENTS],
3682
+ a11y: { element: "section", ariaLabel: "Accordion", keyboard: "Button toggles sections.", notes: "No nested accordions." },
3683
+ theming: { surface: "global-inherit", stylingNotes: "Disclosure pattern." },
3684
+ telemetry: { emits: ["accordion_section_toggled"] }
3685
+ },
3686
+ {
3687
+ type: "DialogCards",
3688
+ category: "content",
3689
+ h5pMachineName: "H5P.Dialogcards",
3690
+ h5pAlias: "Dialog Cards",
3691
+ description: "Flip cards with front/back text.",
3692
+ props: [
3693
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3694
+ { name: "cards", type: "DialogCard[]", required: true, description: "Cards." }
3695
+ ],
3696
+ requiredIds: ["blockId"],
3697
+ parentConstraints: [...COMPOUND_PARENTS],
3698
+ a11y: { element: "section", ariaLabel: "Dialog cards", keyboard: "Flip and navigate cards.", notes: "Reduced motion safe." },
3699
+ theming: { surface: "global-inherit", stylingNotes: "Card flip." },
3700
+ telemetry: { emits: [] }
3701
+ },
3702
+ {
3703
+ type: "Flashcards",
3704
+ category: "content",
3705
+ h5pMachineName: "H5P.Flashcards",
3706
+ h5pAlias: "Flashcards",
3707
+ description: "Study flashcards with optional self-score.",
3708
+ props: [
3709
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3710
+ { name: "cards", type: "Flashcard[]", required: true, description: "Cards." },
3711
+ { name: "selfScore", type: "boolean", required: false, description: "Self-score mode." }
3712
+ ],
3713
+ requiredIds: ["blockId"],
3714
+ parentConstraints: [...COMPOUND_PARENTS],
3715
+ a11y: { element: "section", ariaLabel: "Flashcards", keyboard: "Flip and next.", notes: "Not LMS-scored by default." },
3716
+ theming: { surface: "global-inherit", stylingNotes: "Study mode." },
3717
+ telemetry: { emits: ["flashcard_flipped"] }
3718
+ },
3719
+ {
3720
+ type: "ImageHotspots",
3721
+ category: "content",
3722
+ h5pMachineName: "H5P.ImageHotspots",
3723
+ h5pAlias: "Image Hotspots",
3724
+ description: "Image with clickable hotspot popovers.",
3725
+ props: [
3726
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3727
+ { name: "src", type: "string", required: true, description: "Image URL." },
3728
+ { name: "alt", type: "string", required: true, description: "Alt text." },
3729
+ { name: "hotspots", type: "HotspotSpec[]", required: true, description: "Hotspots." }
3730
+ ],
3731
+ requiredIds: ["blockId"],
3732
+ parentConstraints: [...COMPOUND_PARENTS],
3733
+ a11y: { element: "section", ariaLabel: "Image hotspots", keyboard: "Buttons on image.", notes: "Popover dialog." },
3734
+ theming: { surface: "global-inherit", stylingNotes: "Positioned hotspots." },
3735
+ telemetry: { emits: ["hotspot_opened"] }
3736
+ },
3737
+ {
3738
+ type: "ImageSlider",
3739
+ category: "content",
3740
+ h5pMachineName: "H5P.ImageSlider",
3741
+ h5pAlias: "Image Slider",
3742
+ description: "Carousel of images.",
3743
+ props: [
3744
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3745
+ { name: "slides", type: "ImageSlide[]", required: true, description: "Slides." }
3746
+ ],
3747
+ requiredIds: ["blockId"],
3748
+ parentConstraints: [...COMPOUND_PARENTS],
3749
+ a11y: { element: "section", ariaLabel: "Image slider", keyboard: "Previous/next slide.", notes: "Carousel." },
3750
+ theming: { surface: "global-inherit", stylingNotes: "Slider." },
3751
+ telemetry: { emits: ["image_slider_changed"] }
3752
+ },
3753
+ {
3754
+ type: "FindHotspot",
3755
+ category: "assessment",
3756
+ assessmentContract: true,
3757
+ h5pMachineName: "H5P.ImageHotspotQuestion",
3758
+ h5pAlias: "Find the Hotspot",
3759
+ description: "Select the correct region on an image.",
3760
+ props: [
3761
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
3762
+ { name: "src", type: "string", required: true, description: "Image URL." },
3763
+ { name: "alt", type: "string", required: true, description: "Alt text." },
3764
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
3765
+ { name: "correctTargetId", type: "string", required: true, description: "Correct target id." },
3766
+ ...assessmentBehaviourProps
3767
+ ],
3768
+ requiredIds: ["checkId"],
3769
+ parentConstraints: [...COMPOUND_PARENTS],
3770
+ a11y: { element: "section", ariaLabel: "Find the hotspot", keyboard: "Select target buttons.", notes: "Scored." },
3771
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
3772
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
3773
+ },
3774
+ {
3775
+ type: "FindMultipleHotspots",
3776
+ category: "assessment",
3777
+ assessmentContract: true,
3778
+ h5pMachineName: "H5P.ImageMultipleHotspotQuestion",
3779
+ h5pAlias: "Find Multiple Hotspots",
3780
+ description: "Select all correct regions on an image.",
3781
+ props: [
3782
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
3783
+ { name: "src", type: "string", required: true, description: "Image URL." },
3784
+ { name: "alt", type: "string", required: true, description: "Alt text." },
3785
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
3786
+ { name: "correctTargetIds", type: "string[]", required: true, description: "Correct target ids." },
3787
+ ...assessmentBehaviourProps
3788
+ ],
3789
+ requiredIds: ["checkId"],
3790
+ parentConstraints: [...COMPOUND_PARENTS],
3791
+ a11y: { element: "section", ariaLabel: "Find multiple hotspots", keyboard: "Toggle targets.", notes: "Scored." },
3792
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
3793
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
3794
+ }
3795
+ ];
3796
+ function buildV3CatalogFromV2(v2) {
3797
+ const patched = v2.map((entry) => {
3798
+ const base = extendParents(entry);
3799
+ if (entry.type === "AssessmentSequence") {
3800
+ return {
3801
+ ...base,
3802
+ compoundContract: true,
3803
+ allowedChildTypes: [...ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
3804
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.AssessmentSequence
3805
+ };
3806
+ }
3807
+ return base;
3808
+ });
3809
+ return [...patched, ...v3CompoundAndContentEntries];
3810
+ }
3811
+
1388
3812
  // src/blockCatalog.ts
1389
3813
  var blockCatalogVersion = 1;
3814
+ var blockCatalogV2Version = 2;
3815
+ var blockCatalogV3Version = 3;
1390
3816
  var BLOCK_CATALOG = [
1391
3817
  {
1392
3818
  type: "Course",
@@ -1573,13 +3999,170 @@ var BLOCK_CATALOG = [
1573
3999
  }
1574
4000
  }
1575
4001
  ];
1576
- function buildBlockCatalog() {
1577
- return BLOCK_CATALOG.map((entry) => ({
4002
+ var assessmentBehaviourProps2 = [
4003
+ { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
4004
+ { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
4005
+ { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
4006
+ { name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
4007
+ ];
4008
+ var v2AssessmentEntries = [
4009
+ {
4010
+ type: "TrueFalse",
4011
+ category: "assessment",
4012
+ assessmentContract: true,
4013
+ h5pMachineName: "H5P.TrueFalse",
4014
+ h5pAlias: "True/False",
4015
+ description: "Binary true/false question with assessment contract.",
4016
+ props: [
4017
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4018
+ { name: "question", type: "string", required: true, description: "Question text." },
4019
+ { name: "answer", type: "boolean", required: true, description: "Correct answer." },
4020
+ ...assessmentBehaviourProps2
4021
+ ],
4022
+ requiredIds: ["checkId"],
4023
+ parentConstraints: ["Lesson", "AssessmentSequence"],
4024
+ a11y: {
4025
+ element: "section",
4026
+ ariaLabel: "True or False",
4027
+ keyboard: "Radio group with True/False options.",
4028
+ liveRegions: "role='status' for feedback.",
4029
+ notes: "H5P True/False equivalent."
4030
+ },
4031
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4032
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4033
+ },
4034
+ {
4035
+ type: "FillInTheBlanks",
4036
+ category: "assessment",
4037
+ assessmentContract: true,
4038
+ h5pMachineName: "H5P.Blanks",
4039
+ h5pAlias: "Fill in the Blanks",
4040
+ description: "Fill-in-the-blank text with *answer* markers in template.",
4041
+ props: [
4042
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4043
+ { name: "template", type: "string", required: true, description: "Text with *blank* markers." },
4044
+ { name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
4045
+ ...assessmentBehaviourProps2
4046
+ ],
4047
+ requiredIds: ["checkId"],
4048
+ parentConstraints: ["Lesson", "AssessmentSequence"],
4049
+ a11y: {
4050
+ element: "section",
4051
+ ariaLabel: "Fill in the Blanks",
4052
+ keyboard: "Tab between text inputs.",
4053
+ notes: "H5P Fill in the Blanks equivalent."
4054
+ },
4055
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4056
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4057
+ },
4058
+ {
4059
+ type: "DragAndDrop",
4060
+ category: "assessment",
4061
+ assessmentContract: true,
4062
+ h5pMachineName: "H5P.DragQuestion",
4063
+ h5pAlias: "Drag and Drop",
4064
+ description: "Drag items onto labeled targets.",
4065
+ props: [
4066
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4067
+ { name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
4068
+ { name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
4069
+ ...assessmentBehaviourProps2
4070
+ ],
4071
+ requiredIds: ["checkId"],
4072
+ parentConstraints: ["Lesson", "AssessmentSequence"],
4073
+ a11y: {
4074
+ element: "section",
4075
+ ariaLabel: "Drag and Drop",
4076
+ keyboard: "Select item then activate target; drag also supported.",
4077
+ notes: "H5P Drag and Drop equivalent."
4078
+ },
4079
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4080
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4081
+ },
4082
+ {
4083
+ type: "DragTheWords",
4084
+ category: "assessment",
4085
+ assessmentContract: true,
4086
+ h5pMachineName: "H5P.DragText",
4087
+ h5pAlias: "Drag the Words",
4088
+ description: "Drag words into inline blanks.",
4089
+ props: [
4090
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4091
+ { name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
4092
+ { name: "words", type: "string[]", required: true, description: "Draggable word bank." },
4093
+ ...assessmentBehaviourProps2
4094
+ ],
4095
+ requiredIds: ["checkId"],
4096
+ parentConstraints: ["Lesson", "AssessmentSequence"],
4097
+ a11y: {
4098
+ element: "section",
4099
+ ariaLabel: "Drag the Words",
4100
+ keyboard: "Select word then activate zone.",
4101
+ notes: "H5P Drag the Words equivalent."
4102
+ },
4103
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4104
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4105
+ },
4106
+ {
4107
+ type: "MarkTheWords",
4108
+ category: "assessment",
4109
+ assessmentContract: true,
4110
+ h5pMachineName: "H5P.MarkTheWords",
4111
+ h5pAlias: "Mark the Words",
4112
+ description: "Select correct words in a sentence.",
4113
+ props: [
4114
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4115
+ { name: "text", type: "string", required: true, description: "Source text." },
4116
+ { name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
4117
+ ...assessmentBehaviourProps2
4118
+ ],
4119
+ requiredIds: ["checkId"],
4120
+ parentConstraints: ["Lesson", "AssessmentSequence"],
4121
+ a11y: {
4122
+ element: "section",
4123
+ ariaLabel: "Mark the Words",
4124
+ keyboard: "Toggle words with buttons.",
4125
+ notes: "H5P Mark the Words equivalent."
4126
+ },
4127
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4128
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4129
+ },
4130
+ {
4131
+ type: "AssessmentSequence",
4132
+ category: "container",
4133
+ h5pMachineName: "H5P.QuestionSet",
4134
+ h5pAlias: "Question Set",
4135
+ description: "Ordered sequence of contract-compliant assessments.",
4136
+ props: [
4137
+ { name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
4138
+ { name: "sequential", type: "boolean", required: false, description: "One question at a time." },
4139
+ ...assessmentBehaviourProps2.filter((p) => p.name !== "passingScore")
4140
+ ],
4141
+ requiredIds: [],
4142
+ parentConstraints: ["Lesson"],
4143
+ a11y: {
4144
+ element: "section",
4145
+ ariaLabel: "Assessment sequence",
4146
+ keyboard: "Previous/Next navigation between steps.",
4147
+ notes: "H5P Question Set equivalent."
4148
+ },
4149
+ theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
4150
+ telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
4151
+ }
4152
+ ];
4153
+ var BLOCK_CATALOG_V2 = [
4154
+ ...BLOCK_CATALOG,
4155
+ ...v2AssessmentEntries
4156
+ ];
4157
+ var BLOCK_CATALOG_V3 = buildV3CatalogFromV2(BLOCK_CATALOG_V2);
4158
+ function cloneCatalogEntry(entry) {
4159
+ return {
1578
4160
  ...entry,
1579
4161
  props: entry.props.map((p) => ({ ...p })),
1580
4162
  aliases: entry.aliases ? [...entry.aliases] : void 0,
1581
4163
  optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
1582
4164
  parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
4165
+ allowedChildTypes: entry.allowedChildTypes ? [...entry.allowedChildTypes] : void 0,
1583
4166
  a11y: { ...entry.a11y },
1584
4167
  theming: {
1585
4168
  ...entry.theming,
@@ -1589,22 +4172,51 @@ function buildBlockCatalog() {
1589
4172
  ...entry.telemetry,
1590
4173
  emits: [...entry.telemetry.emits]
1591
4174
  }
1592
- }));
4175
+ };
4176
+ }
4177
+ function buildBlockCatalog(opts) {
4178
+ const version = opts?.version ?? 3;
4179
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4180
+ return source.map((entry) => cloneCatalogEntry(entry));
1593
4181
  }
1594
- function getBlockCatalogEntry(type) {
1595
- return BLOCK_CATALOG.find((entry) => entry.type === type || entry.aliases?.includes(type));
4182
+ function getBlockCatalogEntry(type, opts) {
4183
+ const version = opts?.version ?? 3;
4184
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4185
+ return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
1596
4186
  }
1597
4187
  export {
4188
+ Accordion,
4189
+ AssessmentSequence,
1598
4190
  BLOCK_CATALOG,
4191
+ BLOCK_CATALOG_V2,
4192
+ BLOCK_CATALOG_V3,
1599
4193
  Course,
4194
+ DialogCards,
4195
+ DragAndDrop,
4196
+ DragTheWords,
4197
+ FillInTheBlanks,
4198
+ FindHotspot,
4199
+ FindMultipleHotspots,
4200
+ Flashcards,
4201
+ Heading,
4202
+ Image,
4203
+ ImageHotspots,
4204
+ ImageSlider,
4205
+ InteractiveBook,
1600
4206
  KnowledgeCheck,
1601
4207
  Lesson,
1602
4208
  LessonkitProvider,
4209
+ MarkTheWords,
4210
+ Page,
1603
4211
  ProgressTracker,
1604
4212
  Quiz,
1605
4213
  Reflection,
1606
4214
  Scenario,
4215
+ Text,
1607
4216
  ThemeProvider,
4217
+ TrueFalse,
4218
+ blockCatalogV2Version,
4219
+ blockCatalogV3Version,
1608
4220
  blockCatalogVersion,
1609
4221
  buildBlockCatalog,
1610
4222
  buildTelemetryEvent2 as buildTelemetryEvent,
@@ -1615,7 +4227,9 @@ export {
1615
4227
  defineLifecyclePlugin,
1616
4228
  defineTelemetryPlugin,
1617
4229
  getBlockCatalogEntry,
4230
+ resetAssessmentWarningsForTests,
1618
4231
  resetQuizWarningsForTests,
4232
+ useAssessmentState,
1619
4233
  useCompletion,
1620
4234
  useLessonkit,
1621
4235
  useProgress,