@lessonkit/react 1.1.0 → 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,29 +1,6 @@
1
1
  // src/components.tsx
2
- import { useEffect as useEffect2, useId, useMemo as useMemo4, useRef as useRef2, useState as useState2 } from "react";
3
- import { visuallyHiddenStyle } from "@lessonkit/accessibility";
4
-
5
- // src/assessment/scoring.ts
6
- function resolvePassingThreshold(passingScore, maxScore) {
7
- return passingScore ?? maxScore;
8
- }
9
- function meetsPassingThreshold(score, maxScore, passingScore) {
10
- const threshold = resolvePassingThreshold(passingScore, maxScore);
11
- return score >= threshold;
12
- }
13
- function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
14
- const maxScore = custom?.maxScore ?? fallbackMax;
15
- if (custom?.passed !== void 0) {
16
- const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
17
- return { score: score2, maxScore, passed: custom.passed };
18
- }
19
- if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
20
- const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
21
- return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
22
- }
23
- const score = fallbackCorrect ? maxScore : 0;
24
- const passed = meetsPassingThreshold(score, maxScore, passingScore);
25
- return { score, maxScore, passed };
26
- }
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";
27
4
 
28
5
  // src/context.tsx
29
6
  import { createContext } from "react";
@@ -38,7 +15,39 @@ import {
38
15
  useState
39
16
  } from "react";
40
17
  import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
18
+
19
+ // src/runtime/observability.ts
41
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
42
51
  import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
43
52
 
44
53
  // src/runtime/emitTelemetry.ts
@@ -59,7 +68,16 @@ import {
59
68
  mapLessonkitTelemetryToBridgeAction,
60
69
  telemetryEventToLessonkit
61
70
  } from "@lessonkit/lxpack/bridge";
62
- 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
+ }
63
81
  forwardTelemetryToBridge(event, mode);
64
82
  }
65
83
 
@@ -90,7 +108,9 @@ function createLegacyPipeline(opts, extraSinks = []) {
90
108
  {
91
109
  id: "lxpack-bridge",
92
110
  emit(event) {
93
- forwardTelemetryToLxpack(event, opts.lxpackBridge);
111
+ forwardTelemetryToLxpack(event, opts.lxpackBridge, {
112
+ onBridgeMiss: opts.onLxpackBridgeMiss
113
+ });
94
114
  }
95
115
  },
96
116
  ...extraSinks
@@ -117,7 +137,8 @@ function emitTelemetry(tracking, xapi, event, opts) {
117
137
  const legacy = {
118
138
  tracking,
119
139
  xapi,
120
- lxpackBridge: opts?.lxpackBridge ?? "auto"
140
+ lxpackBridge: opts?.lxpackBridge ?? "auto",
141
+ onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
121
142
  };
122
143
  emitThroughPipeline(event, legacy, opts?.extraSinks);
123
144
  }
@@ -207,7 +228,9 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
207
228
  xapiStatementSent = true;
208
229
  }
209
230
  }
210
- forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
231
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
232
+ onBridgeMiss: opts.onLxpackBridgeMiss
233
+ });
211
234
  const emitCtx = {
212
235
  courseId: opts.event.courseId,
213
236
  sessionId: opts.event.sessionId,
@@ -218,56 +241,25 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
218
241
  }
219
242
 
220
243
  // src/runtime/plugins.ts
221
- import { createPluginRegistry } from "@lessonkit/core";
244
+ import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
222
245
  function createReactPluginHost(plugins) {
223
246
  if (!plugins?.length) return null;
224
247
  return createPluginRegistry(plugins);
225
248
  }
226
249
  function buildPluginContext(opts) {
227
- return {
228
- courseId: opts.courseId,
229
- sessionId: opts.sessionId,
230
- attemptId: opts.attemptId,
231
- user: opts.user
232
- };
250
+ return buildPluginContextFromCore(opts);
233
251
  }
234
252
  function emitTelemetryWithPlugins(opts) {
235
253
  const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
236
254
  if (next === null) return;
237
255
  emitTelemetry(opts.tracking, opts.xapi, next, {
238
256
  lxpackBridge: opts.lxpackBridge ?? "auto",
239
- extraSinks: opts.extraSinks
240
- });
241
- }
242
-
243
- // src/runtime/telemetry.ts
244
- import { createTrackingClient } from "@lessonkit/core";
245
- function createTrackingClientFromConfig(config) {
246
- if (config.tracking?.enabled === false) return createTrackingClient();
247
- if (config.tracking?.createClient) return config.tracking.createClient();
248
- return createTrackingClient({
249
- sink: config.tracking?.sink,
250
- batchSink: config.tracking?.batchSink,
251
- batch: config.tracking?.batch
257
+ extraSinks: opts.extraSinks,
258
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss
252
259
  });
253
260
  }
254
- async function disposeTrackingClient(client) {
255
- try {
256
- await client?.flush?.();
257
- } catch {
258
- }
259
- try {
260
- await client?.dispose?.();
261
- } catch {
262
- }
263
- }
264
261
 
265
- // src/provider/useLessonkitProviderRuntime.ts
266
- var useIsoLayoutEffect = (
267
- /* v8 ignore next -- SSR uses useEffect when window is unavailable */
268
- typeof window !== "undefined" ? useLayoutEffect : useEffect
269
- );
270
- var defaultStorage = createSessionStoragePort();
262
+ // src/provider/courseStarted/emit.ts
271
263
  var courseStartedTrackingFlightKey = null;
272
264
  function isTrackingActive(tracking) {
273
265
  return tracking?.enabled !== false;
@@ -322,6 +314,7 @@ async function emitCourseStartedPipelineOnly(opts) {
322
314
  event: opts.event,
323
315
  xapi: opts.xapi,
324
316
  lxpackBridge: opts.lxpackBridge,
317
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
325
318
  extraSinks: opts.extraSinks,
326
319
  skipXapi: opts.skipXapi
327
320
  });
@@ -374,6 +367,7 @@ async function emitCourseStartedToTrackingOnly(opts) {
374
367
  event,
375
368
  xapi: null,
376
369
  lxpackBridge: opts.lxpackBridge,
370
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
377
371
  extraSinks: opts.extraSinks,
378
372
  skipXapi: true
379
373
  });
@@ -427,6 +421,35 @@ function assertTrackingSinkConfig(tracking) {
427
421
  "[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
428
422
  );
429
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();
430
453
  function useLessonkitProviderRuntime(config) {
431
454
  const normalizedCourseId = useMemo(
432
455
  () => assertValidId(config.courseId, "courseId"),
@@ -437,6 +460,14 @@ function useLessonkitProviderRuntime(config) {
437
460
  [config, normalizedCourseId]
438
461
  );
439
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]);
440
471
  const extraSinksRef = useRef(normalizedConfig.sinks);
441
472
  extraSinksRef.current = normalizedConfig.sinks;
442
473
  const headlessRef = useRef(null);
@@ -455,7 +486,16 @@ function useLessonkitProviderRuntime(config) {
455
486
  courseIdRef.current = normalizedCourseId;
456
487
  const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
457
488
  lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
458
- 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
+ );
459
499
  const pluginHostRef = useRef(pluginHost);
460
500
  pluginHostRef.current = pluginHost;
461
501
  const progressRef = useRef(createProgressController());
@@ -471,7 +511,8 @@ function useLessonkitProviderRuntime(config) {
471
511
  headlessRef.current = createLessonkitRuntime({
472
512
  courseId: normalizedCourseId,
473
513
  runtimeVersion: "v2",
474
- session: normalizedConfig.session
514
+ session: normalizedConfig.session,
515
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
475
516
  });
476
517
  progressRef.current = headlessRef.current.progress;
477
518
  } else {
@@ -485,7 +526,8 @@ function useLessonkitProviderRuntime(config) {
485
526
  headlessRef.current = createLessonkitRuntime({
486
527
  courseId: normalizedCourseId,
487
528
  runtimeVersion: "v2",
488
- session: normalizedConfig.session
529
+ session: normalizedConfig.session,
530
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
489
531
  });
490
532
  }
491
533
  if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
@@ -509,7 +551,7 @@ function useLessonkitProviderRuntime(config) {
509
551
  }, []);
510
552
  const activeLessonIdRef = useRef(progress.activeLessonId);
511
553
  activeLessonIdRef.current = progress.activeLessonId;
512
- const xapiQueueRef = useRef(createInMemoryXAPIQueue());
554
+ const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
513
555
  const xapiRef = useRef(null);
514
556
  const [xapi, setXapi] = useState(null);
515
557
  const prevXapiCourseIdRef = useRef(normalizedCourseId);
@@ -530,7 +572,7 @@ function useLessonkitProviderRuntime(config) {
530
572
  }
531
573
  void xapiRef.current?.flush();
532
574
  }
533
- xapiQueueRef.current = createInMemoryXAPIQueue();
575
+ xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
534
576
  prevXapiCourseIdRef.current = courseId;
535
577
  xapiCourseStartedSentOnClientRef.current = false;
536
578
  }
@@ -609,7 +651,7 @@ function useLessonkitProviderRuntime(config) {
609
651
  );
610
652
  useIsoLayoutEffect(() => {
611
653
  const prev = trackingRef.current;
612
- const baseSink = normalizedConfig.tracking?.sink;
654
+ const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
613
655
  const userBatchSink = normalizedConfig.tracking?.batchSink;
614
656
  assertTrackingSinkConfig(normalizedConfig.tracking);
615
657
  const sink = pluginHostRef.current && baseSink ? (
@@ -659,6 +701,7 @@ function useLessonkitProviderRuntime(config) {
659
701
  attemptId: attemptIdRef.current,
660
702
  user: userRef.current,
661
703
  lxpackBridge: lxpackBridgeModeRef.current,
704
+ onLxpackBridgeMiss,
662
705
  extraSinks: extraSinksRef.current,
663
706
  skipXapi: xapiCourseStartedSentOnClientRef.current,
664
707
  onXapiStatementSent: () => {
@@ -700,9 +743,10 @@ function useLessonkitProviderRuntime(config) {
700
743
  user: userRef.current
701
744
  }),
702
745
  lxpackBridge: lxpackBridgeModeRef.current,
746
+ onLxpackBridgeMiss,
703
747
  extraSinks: extraSinksRef.current
704
748
  });
705
- }, []);
749
+ }, [onLxpackBridgeMiss]);
706
750
  const emitLifecycleEvent = useCallback(
707
751
  (name, data, lessonId) => {
708
752
  const event = tryBuildTelemetryEvent({
@@ -758,12 +802,13 @@ function useLessonkitProviderRuntime(config) {
758
802
  attemptId: attemptIdRef.current,
759
803
  user: userRef.current,
760
804
  lxpackBridge: lxpackBridgeModeRef.current,
805
+ onLxpackBridgeMiss,
761
806
  extraSinks: extraSinksRef.current
762
807
  });
763
808
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
764
809
  }
765
810
  })();
766
- }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
811
+ }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
767
812
  const emitLessonCompleted = useCallback(
768
813
  (lessonId, durationMs) => {
769
814
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -812,6 +857,22 @@ function useLessonkitProviderRuntime(config) {
812
857
  })();
813
858
  };
814
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
+ }, []);
815
876
  const setActiveLesson = useCallback(
816
877
  (lessonId) => {
817
878
  if (useV2Runtime && headlessRef.current) {
@@ -875,20 +936,34 @@ function useLessonkitProviderRuntime(config) {
875
936
  session: normalizedConfig.session
876
937
  });
877
938
  }
878
- }, [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]);
879
953
  useEffect(() => {
880
- if (!pluginHost) return;
954
+ const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
955
+ if (!host) return;
881
956
  const ctx = buildPluginContext({
882
957
  courseId: courseIdRef.current,
883
958
  sessionId: sessionIdRef.current,
884
959
  attemptId: attemptIdRef.current,
885
960
  user: userRef.current
886
961
  });
887
- pluginHost.setupAll(ctx);
962
+ host.setupAll(ctx);
888
963
  return () => {
889
- pluginHost.disposeAll();
964
+ host.disposeAll();
890
965
  };
891
- }, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
966
+ }, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
892
967
  useEffect(() => {
893
968
  const nextConfigured = normalizedConfig.session?.sessionId;
894
969
  const prevConfigured = prevConfiguredSessionIdRef.current;
@@ -1051,465 +1126,669 @@ function getLessonMountCount(lessonId) {
1051
1126
  return mountCounts.get(lessonId) ?? 0;
1052
1127
  }
1053
1128
 
1054
- // src/components.tsx
1055
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1056
- var warnedQuizOutsideLesson = false;
1057
- function resetQuizWarningsForTests() {
1058
- warnedQuizOutsideLesson = false;
1059
- }
1060
- function Course(props) {
1061
- const courseId = useMemo4(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1062
- const providerConfig = useMemo4(
1063
- () => ({ ...props.config, courseId }),
1064
- [props.config, courseId]
1065
- );
1066
- return /* @__PURE__ */ jsx2(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
1067
- /* @__PURE__ */ jsx2("h1", { children: props.title }),
1068
- /* @__PURE__ */ jsx2("div", { children: props.children })
1069
- ] }) });
1070
- }
1071
- function Lesson(props) {
1072
- const lessonId = useMemo4(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1073
- const autoComplete = props.autoCompleteOnUnmount !== false;
1074
- const { setActiveLesson, config } = useLessonkit();
1075
- const { completeLesson } = useCompletion();
1076
- const lessonMountGenerationRef = useRef2(0);
1077
- const liveCourseIdRef = useRef2(config.courseId);
1078
- liveCourseIdRef.current = config.courseId;
1079
- useEffect2(() => {
1080
- const unregister = registerLessonMount(lessonId);
1081
- const generation = ++lessonMountGenerationRef.current;
1082
- const mountedCourseId = config.courseId;
1083
- let effectSurvivedTick = false;
1084
- queueMicrotask(() => {
1085
- queueMicrotask(() => {
1086
- effectSurvivedTick = true;
1087
- });
1088
- });
1089
- setActiveLesson(lessonId);
1090
- return () => {
1091
- unregister();
1092
- if (getLessonMountCount(lessonId) > 0) {
1093
- return;
1094
- }
1095
- if (!autoComplete) return;
1096
- queueMicrotask(() => {
1097
- if (!effectSurvivedTick) return;
1098
- if (lessonMountGenerationRef.current !== generation) return;
1099
- if (liveCourseIdRef.current !== mountedCourseId) return;
1100
- completeLesson(lessonId, { courseId: mountedCourseId });
1101
- });
1102
- };
1103
- }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1104
- return /* @__PURE__ */ jsx2(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
1105
- /* @__PURE__ */ jsx2("h2", { children: props.title }),
1106
- /* @__PURE__ */ jsx2("div", { children: props.children })
1107
- ] }) });
1108
- }
1109
- function Scenario(props) {
1110
- const blockId = useMemo4(
1111
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1112
- [props.blockId]
1113
- );
1114
- return /* @__PURE__ */ jsx2("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1115
- }
1116
- function Reflection(props) {
1117
- const blockId = useMemo4(
1118
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1119
- [props.blockId]
1120
- );
1121
- const promptId = useId();
1122
- const hintId = useId();
1123
- const [internalValue, setInternalValue] = useState2("");
1124
- const isControlled = props.value !== void 0;
1125
- const value = isControlled ? props.value : internalValue;
1126
- const handleChange = (event) => {
1127
- if (!isControlled) setInternalValue(event.target.value);
1128
- props.onChange?.(event.target.value);
1129
- };
1130
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1131
- props.prompt ? /* @__PURE__ */ jsx2("p", { id: promptId, children: props.prompt }) : null,
1132
- props.hint ? /* @__PURE__ */ jsx2("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
1133
- props.children,
1134
- /* @__PURE__ */ jsx2(
1135
- "textarea",
1136
- {
1137
- value,
1138
- onChange: handleChange,
1139
- "aria-labelledby": props.prompt ? promptId : void 0,
1140
- "aria-describedby": props.hint ? hintId : void 0,
1141
- "aria-label": props.prompt ? void 0 : "Reflection response"
1142
- }
1143
- )
1144
- ] });
1145
- }
1146
- function KnowledgeCheck(props) {
1147
- return /* @__PURE__ */ jsx2(
1148
- Quiz,
1149
- {
1150
- checkId: props.checkId,
1151
- question: props.question,
1152
- choices: props.choices,
1153
- answer: props.answer,
1154
- passingScore: props.passingScore
1155
- }
1156
- );
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;
1157
1139
  }
1158
- function Quiz(props) {
1140
+ function AssessmentLessonGuard(props) {
1159
1141
  const enclosingLessonId = useEnclosingLessonId();
1160
1142
  const missingLesson = enclosingLessonId === void 0;
1161
1143
  useEffect2(() => {
1162
1144
  if (!missingLesson || isDevEnvironment4()) return;
1163
- if (!warnedQuizOutsideLesson) {
1164
- warnedQuizOutsideLesson = true;
1145
+ if (!warnedAssessmentOutsideLesson) {
1146
+ warnedAssessmentOutsideLesson = true;
1165
1147
  console.error(
1166
- "[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.`
1167
1149
  );
1168
1150
  }
1169
- }, [missingLesson]);
1151
+ }, [missingLesson, props.blockLabel]);
1170
1152
  if (missingLesson && isDevEnvironment4()) {
1171
- throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1153
+ throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1172
1154
  }
1173
1155
  if (missingLesson) {
1174
- 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
+ ] }) });
1175
1160
  }
1176
- return /* @__PURE__ */ jsx2(QuizInner, { ...props, enclosingLessonId });
1161
+ return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
1177
1162
  }
1178
- function QuizInner(props) {
1179
- const { enclosingLessonId } = props;
1180
- const checkId = useMemo4(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1181
- const quiz = useQuizState(enclosingLessonId);
1182
- const { plugins, config, session } = useLessonkit();
1183
- const [selected, setSelected] = useState2(null);
1184
- const [selectionCorrect, setSelectionCorrect] = useState2(null);
1185
- const [quizPassed, setQuizPassed] = useState2(false);
1186
- const completedRef = useRef2(false);
1187
- const questionId = useId();
1188
- const choicesKey = props.choices.join("\0");
1189
- useEffect2(() => {
1190
- completedRef.current = false;
1191
- setQuizPassed(false);
1192
- setSelected(null);
1193
- setSelectionCorrect(null);
1194
- }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
1195
- const isChoiceCorrect = (choice, custom) => {
1196
- if (!custom) return choice === props.answer;
1197
- if (custom.passed !== void 0) return custom.passed;
1198
- if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1199
- return meetsPassingThreshold(custom.score, custom.maxScore, props.passingScore);
1200
- }
1201
- 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 } : {}
1202
1175
  };
1203
- const passed = quizPassed;
1204
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1205
- /* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
1206
- /* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
1207
- /* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
1208
- props.choices.map((c, i) => /* @__PURE__ */ jsxs("label", { style: { display: "block" }, children: [
1209
- /* @__PURE__ */ jsx2(
1210
- "input",
1211
- {
1212
- type: "radio",
1213
- name: questionId,
1214
- value: c,
1215
- checked: selected === c,
1216
- disabled: passed,
1217
- "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1218
- onChange: () => {
1219
- if (passed) return;
1220
- setSelected(c);
1221
- const pluginCtx = buildPluginContext({
1222
- courseId: config.courseId,
1223
- sessionId: session.sessionId,
1224
- attemptId: session.attemptId,
1225
- user: session.user
1226
- });
1227
- const custom = plugins?.scoreAssessment(
1228
- {
1229
- checkId,
1230
- lessonId: enclosingLessonId,
1231
- response: c
1232
- },
1233
- pluginCtx
1234
- ) ?? null;
1235
- const correct = isChoiceCorrect(c, custom);
1236
- setSelectionCorrect(correct);
1237
- quiz.answer({
1238
- checkId,
1239
- question: props.question,
1240
- choice: c,
1241
- correct
1242
- });
1243
- if (correct && !completedRef.current) {
1244
- completedRef.current = true;
1245
- setQuizPassed(true);
1246
- const maxScore = custom?.maxScore ?? 1;
1247
- quiz.complete({
1248
- checkId,
1249
- score: custom?.score ?? maxScore,
1250
- maxScore,
1251
- passingScore: props.passingScore ?? maxScore
1252
- });
1253
- }
1254
- }
1255
- }
1256
- ),
1257
- c
1258
- ] }, `${questionId}-${i}`))
1259
- ] }),
1260
- selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1261
- ] });
1262
1176
  }
1263
- function ProgressTracker(props) {
1264
- const { progress } = useLessonkit();
1265
- const completed = progress.completedLessonIds.size;
1266
- if (props.totalLessons != null) {
1267
- const total = props.totalLessons;
1268
- const displayed = Math.min(completed, total);
1269
- return /* @__PURE__ */ jsx2("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx2(
1270
- "div",
1271
- {
1272
- role: "progressbar",
1273
- "aria-valuemin": 0,
1274
- "aria-valuemax": total,
1275
- "aria-valuenow": displayed,
1276
- "aria-label": "Lessons completed",
1277
- children: /* @__PURE__ */ jsxs("p", { children: [
1278
- "Lessons completed: ",
1279
- displayed,
1280
- " of ",
1281
- total
1282
- ] })
1283
- }
1284
- ) });
1285
- }
1286
- return /* @__PURE__ */ jsx2("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs("p", { children: [
1287
- "Lessons completed: ",
1288
- completed
1289
- ] }) });
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);
1290
1192
  }
1291
1193
 
1292
- // src/blocks/TrueFalse.tsx
1293
- import React5, { forwardRef, useEffect as useEffect4, useImperativeHandle, useMemo as useMemo6, useRef as useRef4, useState as useState3 } from "react";
1194
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1195
+ import { useImperativeHandle as useImperativeHandle2 } from "react";
1294
1196
 
1295
- // src/assessment/AssessmentLessonGuard.tsx
1296
- import { useEffect as useEffect3 } from "react";
1297
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1298
- var warnedAssessmentOutsideLesson = false;
1299
- function resetAssessmentWarningsForTests() {
1300
- warnedAssessmentOutsideLesson = false;
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 };
1301
1212
  }
1302
- function AssessmentLessonGuard(props) {
1303
- const enclosingLessonId = useEnclosingLessonId();
1304
- const missingLesson = enclosingLessonId === void 0;
1305
- useEffect3(() => {
1306
- if (!missingLesson || isDevEnvironment4()) return;
1307
- if (!warnedAssessmentOutsideLesson) {
1308
- warnedAssessmentOutsideLesson = true;
1309
- console.error(
1310
- `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1311
- );
1312
- }
1313
- }, [missingLesson, props.blockLabel]);
1314
- if (missingLesson && isDevEnvironment4()) {
1315
- throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
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;
1316
1218
  }
1317
- if (missingLesson) {
1318
- return /* @__PURE__ */ jsx3("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs2("p", { children: [
1319
- props.blockLabel,
1320
- " must be placed inside a Lesson."
1321
- ] }) });
1219
+ for (const [checkId, handle] of handles) {
1220
+ const child = childStates[checkId];
1221
+ if (child && handle.resume) handle.resume(child);
1322
1222
  }
1323
- return /* @__PURE__ */ jsx3(Fragment, { children: props.children(enclosingLessonId) });
1223
+ return true;
1324
1224
  }
1325
1225
 
1326
- // src/assessment/AssessmentSequenceContext.tsx
1327
- import React4, { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useMemo as useMemo5, useRef as useRef3 } from "react";
1328
- import { jsx as jsx4 } from "react/jsx-runtime";
1329
- var AssessmentSequenceContext = createContext3(null);
1330
- function AssessmentSequenceProvider({ children }) {
1331
- const registryRef = useRef3(/* @__PURE__ */ new Map());
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);
1332
1237
  const register = useCallback2((checkId, handle) => {
1238
+ const prev = registryRef.current.get(checkId);
1333
1239
  registryRef.current.set(checkId, handle);
1240
+ if (prev !== handle) {
1241
+ setHandlesVersion((v) => v + 1);
1242
+ }
1334
1243
  return () => {
1335
- registryRef.current.delete(checkId);
1244
+ if (registryRef.current.get(checkId) === handle) {
1245
+ registryRef.current.delete(checkId);
1246
+ setHandlesVersion((v) => v + 1);
1247
+ }
1336
1248
  };
1337
1249
  }, []);
1338
- const value = useMemo5(
1250
+ const registryValue = useMemo4(
1339
1251
  () => ({
1340
1252
  register,
1341
1253
  getHandles: () => registryRef.current
1342
1254
  }),
1343
1255
  [register]
1344
1256
  );
1345
- return /* @__PURE__ */ jsx4(AssessmentSequenceContext.Provider, { value, children });
1257
+ return /* @__PURE__ */ jsx3(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx3(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) });
1346
1258
  }
1347
- function useAssessmentSequenceRegistry() {
1348
- return useContext3(AssessmentSequenceContext);
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);
1349
1267
  }
1350
1268
  function useRegisterAssessmentHandle(checkId, handle) {
1351
- const ctx = useAssessmentSequenceRegistry();
1352
- React4.useEffect(() => {
1353
- if (!ctx || !handle) return;
1354
- return ctx.register(checkId, handle);
1355
- }, [ctx, 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
+ );
1356
1313
  }
1357
1314
 
1358
- // src/blocks/TrueFalse.tsx
1359
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1360
- var INTERACTION = "trueFalse";
1361
- function TrueFalseInner(props, ref) {
1362
- const { enclosingLessonId } = props;
1363
- const checkId = useMemo6(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1364
- const assessment = useAssessmentState(enclosingLessonId);
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) {
1365
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);
1366
1387
  const [selected, setSelected] = useState3(null);
1367
1388
  const [selectionCorrect, setSelectionCorrect] = useState3(null);
1368
- const [showSolutions, setShowSolutions] = useState3(false);
1369
- const [passed, setPassed] = useState3(false);
1370
- const completedRef = useRef4(false);
1371
- const questionId = React5.useId();
1372
- const reset = () => {
1389
+ const [quizPassed, setQuizPassed] = useState3(false);
1390
+ const completedRef = useRef3(false);
1391
+ const questionId = useId();
1392
+ const choicesKey = props.choices.join("\0");
1393
+ useEffect3(() => {
1373
1394
  completedRef.current = false;
1374
- setPassed(false);
1395
+ setQuizPassed(false);
1375
1396
  setSelected(null);
1376
1397
  setSelectionCorrect(null);
1377
- setShowSolutions(false);
1378
- };
1379
- useEffect4(() => {
1380
- reset();
1381
- }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1382
- const handle = useMemo6(() => {
1383
- const maxScore = 1;
1384
- const score = passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
1385
- return {
1386
- getScore: () => score,
1387
- getMaxScore: () => maxScore,
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,
1388
1410
  getAnswerGiven: () => selected !== null,
1389
- resetTask: reset,
1390
- showSolutions: () => setShowSolutions(true),
1411
+ resetTask: () => {
1412
+ completedRef.current = false;
1413
+ setQuizPassed(false);
1414
+ setSelected(null);
1415
+ setSelectionCorrect(null);
1416
+ },
1417
+ showSolutions: () => {
1418
+ },
1391
1419
  getXAPIData: () => ({
1392
1420
  checkId,
1393
- interactionType: INTERACTION,
1421
+ interactionType: "mcq",
1394
1422
  response: selected ?? void 0,
1395
- correct: selected === props.answer,
1396
- score,
1397
- maxScore
1398
- })
1399
- };
1400
- }, [checkId, passed, props.answer, selected]);
1401
- useImperativeHandle(ref, () => handle, [handle]);
1402
- useRegisterAssessmentHandle(checkId, handle);
1403
- const submit = (value) => {
1404
- if (passed && !props.enableRetry) return;
1405
- setSelected(value);
1406
- const pluginCtx = buildPluginContext({
1407
- courseId: config.courseId,
1408
- sessionId: session.sessionId,
1409
- attemptId: session.attemptId,
1410
- user: session.user
1411
- });
1412
- const custom = plugins?.scoreAssessment(
1413
- { checkId, lessonId: enclosingLessonId, response: value },
1414
- pluginCtx
1415
- ) ?? null;
1416
- const correct = value === props.answer;
1417
- const scored = scoreFromCustom(custom, correct, 1, props.passingScore);
1418
- setSelectionCorrect(scored.passed);
1419
- assessment.answer({
1420
- checkId,
1421
- interactionType: INTERACTION,
1422
- question: props.question,
1423
- response: value,
1424
- correct: scored.passed
1425
- });
1426
- if (scored.passed && !completedRef.current) {
1427
- completedRef.current = true;
1428
- setPassed(true);
1429
- assessment.complete({
1430
- checkId,
1431
- interactionType: INTERACTION,
1432
- score: scored.score,
1433
- maxScore: scored.maxScore,
1434
- passingScore: props.passingScore ?? scored.maxScore
1435
- });
1436
- }
1437
- };
1438
- const reveal = showSolutions || passed && props.enableSolutionsButton;
1439
- return /* @__PURE__ */ jsxs3("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1440
- /* @__PURE__ */ jsx5("p", { id: questionId, children: props.question }),
1441
- /* @__PURE__ */ jsxs3("fieldset", { "aria-labelledby": questionId, children: [
1442
- /* @__PURE__ */ jsx5("legend", { className: "lk-visually-hidden", children: "True or False" }),
1443
- /* @__PURE__ */ jsxs3("label", { style: { display: "block", marginRight: "1rem" }, children: [
1444
- /* @__PURE__ */ jsx5(
1445
- "input",
1446
- {
1447
- type: "radio",
1448
- name: `${questionId}-tf`,
1449
- checked: selected === true,
1450
- disabled: passed && !props.enableRetry,
1451
- onChange: () => submit(true)
1452
- }
1453
- ),
1454
- "True"
1455
- ] }),
1456
- /* @__PURE__ */ jsxs3("label", { style: { display: "block" }, children: [
1457
- /* @__PURE__ */ jsx5(
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(
1458
1450
  "input",
1459
1451
  {
1460
1452
  type: "radio",
1461
- name: `${questionId}-tf`,
1462
- checked: selected === false,
1463
- disabled: passed && !props.enableRetry,
1464
- onChange: () => submit(false)
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
+ }
1465
1482
  }
1466
1483
  ),
1467
- "False"
1468
- ] })
1484
+ c
1485
+ ] }, `${questionId}-${i}`))
1469
1486
  ] }),
1470
- reveal ? /* @__PURE__ */ jsxs3("p", { children: [
1471
- "Correct answer: ",
1472
- /* @__PURE__ */ jsx5("strong", { children: props.answer ? "True" : "False" })
1473
- ] }) : null,
1474
- selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx5("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1475
- props.enableRetry && passed ? /* @__PURE__ */ jsx5("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1476
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1487
+ selected && selectionCorrect !== null ? /* @__PURE__ */ jsx4("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1477
1488
  ] });
1478
1489
  }
1479
- var TrueFalseInnerForwarded = forwardRef(TrueFalseInner);
1480
- var TrueFalse = forwardRef(function TrueFalse2(props, ref) {
1481
- return /* @__PURE__ */ jsx5(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx5(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
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 }) });
1482
1493
  });
1483
-
1484
- // src/blocks/MarkTheWords.tsx
1485
- import React6, { forwardRef as forwardRef2, useEffect as useEffect5, useImperativeHandle as useImperativeHandle2, useMemo as useMemo7, useRef as useRef5, useState as useState4 } from "react";
1486
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1487
- var INTERACTION2 = "markTheWords";
1488
- function tokenize(text) {
1489
- return text.split(/(\s+)/).filter((t) => t.length > 0);
1490
- }
1491
- function MarkTheWordsInner(props, ref) {
1492
- const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1493
- const assessment = useAssessmentState(props.enclosingLessonId);
1494
- const tokens = useMemo7(() => tokenize(props.text), [props.text]);
1495
- const correctSet = useMemo7(
1496
- () => new Set(props.correctWords.map((w) => w.toLowerCase())),
1497
- [props.correctWords]
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
+ }
1498
1504
  );
1499
- const [marked, setMarked] = useState4(() => /* @__PURE__ */ new Set());
1500
- const [passed, setPassed] = useState4(false);
1501
- const [showSolutions, setShowSolutions] = useState4(false);
1502
- const completedRef = useRef5(false);
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
+ {
1607
+ role: "progressbar",
1608
+ "aria-valuemin": 0,
1609
+ "aria-valuemax": total,
1610
+ "aria-valuenow": displayed,
1611
+ "aria-label": "Lessons completed",
1612
+ children: /* @__PURE__ */ jsxs3("p", { children: [
1613
+ "Lessons completed: ",
1614
+ displayed,
1615
+ " of ",
1616
+ total
1617
+ ] })
1618
+ }
1619
+ ) });
1620
+ }
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();
1503
1643
  const reset = () => {
1504
1644
  completedRef.current = false;
1505
1645
  setPassed(false);
1506
- setMarked(/* @__PURE__ */ new Set());
1646
+ setSelected(null);
1647
+ setSelectionCorrect(null);
1507
1648
  setShowSolutions(false);
1508
1649
  };
1509
1650
  useEffect5(() => {
1510
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();
1511
1790
  }, [checkId, props.text, props.correctWords.join("\0")]);
1512
- const selectableIndices = useMemo7(() => {
1791
+ const selectableIndices = useMemo8(() => {
1513
1792
  const indices = [];
1514
1793
  tokens.forEach((t, i) => {
1515
1794
  if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
@@ -1521,11 +1800,11 @@ function MarkTheWordsInner(props, ref) {
1521
1800
  const maxScore = selectableIndices.length;
1522
1801
  const score = allMarked ? maxScore : marked.size;
1523
1802
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1524
- const handle = useMemo7(() => {
1525
- const handleMax = maxScore || 1;
1526
- return {
1803
+ const handle = useMemo8(
1804
+ () => buildAssessmentHandle({
1805
+ checkId,
1527
1806
  getScore: () => score,
1528
- getMaxScore: () => handleMax,
1807
+ getMaxScore: () => maxScore || 1,
1529
1808
  getAnswerGiven: () => marked.size > 0,
1530
1809
  resetTask: reset,
1531
1810
  showSolutions: () => setShowSolutions(true),
@@ -1535,12 +1814,22 @@ function MarkTheWordsInner(props, ref) {
1535
1814
  response: [...marked].map((i) => tokens[i]),
1536
1815
  correct: passedThreshold,
1537
1816
  score,
1538
- maxScore: handleMax
1539
- })
1540
- };
1541
- }, [checkId, marked, maxScore, passedThreshold, score, tokens]);
1542
- useImperativeHandle2(ref, () => handle, [handle]);
1543
- useRegisterAssessmentHandle(checkId, handle);
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);
1544
1833
  const toggle = (index) => {
1545
1834
  if (passed && !props.enableRetry) return;
1546
1835
  setMarked((prev) => {
@@ -1550,7 +1839,7 @@ function MarkTheWordsInner(props, ref) {
1550
1839
  return next;
1551
1840
  });
1552
1841
  };
1553
- useEffect5(() => {
1842
+ useEffect6(() => {
1554
1843
  if (!hasTargets) {
1555
1844
  if (isDevEnvironment4()) {
1556
1845
  console.warn(
@@ -1590,20 +1879,20 @@ function MarkTheWordsInner(props, ref) {
1590
1879
  score,
1591
1880
  tokens
1592
1881
  ]);
1593
- return /* @__PURE__ */ jsxs4("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1594
- !hasTargets ? /* @__PURE__ */ jsxs4("p", { role: "alert", children: [
1882
+ return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1883
+ !hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
1595
1884
  "No words in this sentence match ",
1596
- /* @__PURE__ */ jsx6("code", { children: "correctWords" }),
1885
+ /* @__PURE__ */ jsx7("code", { children: "correctWords" }),
1597
1886
  ". Check spelling and capitalization in the source text."
1598
1887
  ] }) : null,
1599
- /* @__PURE__ */ jsx6("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
1600
- /* @__PURE__ */ jsx6("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
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) => {
1601
1890
  const isWord = !/^\s+$/.test(token);
1602
1891
  const isTarget = isWord && correctSet.has(token.toLowerCase());
1603
- if (!isTarget) return /* @__PURE__ */ jsx6(React6.Fragment, { children: token }, i);
1892
+ if (!isTarget) return /* @__PURE__ */ jsx7(React8.Fragment, { children: token }, i);
1604
1893
  const selected = marked.has(i);
1605
1894
  const solution = showSolutions || passed && props.enableSolutionsButton;
1606
- return /* @__PURE__ */ jsx6(
1895
+ return /* @__PURE__ */ jsx7(
1607
1896
  "button",
1608
1897
  {
1609
1898
  type: "button",
@@ -1621,49 +1910,59 @@ function MarkTheWordsInner(props, ref) {
1621
1910
  i
1622
1911
  );
1623
1912
  }) }),
1624
- allMarked ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
1625
- props.enableRetry && passed ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1626
- props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
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
1627
1916
  ] });
1628
1917
  }
1629
- var MarkTheWordsInnerForwarded = forwardRef2(MarkTheWordsInner);
1630
- var MarkTheWords = forwardRef2(function MarkTheWords2(props, ref) {
1631
- return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
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 }) });
1632
1921
  });
1633
1922
 
1634
1923
  // src/blocks/FillInTheBlanks.tsx
1635
- import React7, { forwardRef as forwardRef3, useEffect as useEffect6, useImperativeHandle as useImperativeHandle3, useMemo as useMemo8, useRef as useRef6, useState as useState5 } from "react";
1636
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1637
- var INTERACTION3 = "fillInBlanks";
1638
- function parseTemplate(template) {
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) {
1639
1928
  const parts = [];
1640
- const blanks = [];
1929
+ const values = [];
1641
1930
  const re = /\*([^*]+)\*/g;
1642
1931
  let last = 0;
1643
1932
  let match;
1644
1933
  let n = 0;
1645
1934
  while ((match = re.exec(template)) !== null) {
1646
1935
  parts.push(template.slice(last, match.index));
1647
- const id = `blank-${n++}`;
1648
- blanks.push({ id, answer: match[1].trim() });
1649
- parts.push(id);
1936
+ values.push(match[1].trim());
1937
+ parts.push(`${idPrefix}-${n++}`);
1650
1938
  last = match.index + match[0].length;
1651
1939
  }
1652
1940
  parts.push(template.slice(last));
1653
- return { parts, blanks };
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
+ };
1654
1953
  }
1655
1954
  function FillInTheBlanksInner(props, ref) {
1656
- const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1955
+ const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1657
1956
  const assessment = useAssessmentState(props.enclosingLessonId);
1658
- const parsed = useMemo8(() => parseTemplate(props.template), [props.template]);
1957
+ const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
1659
1958
  const blanks = props.blanks ?? parsed.blanks;
1660
- const [values, setValues] = useState5(
1959
+ const [values, setValues] = useState7(
1661
1960
  () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
1662
1961
  );
1663
- const [passed, setPassed] = useState5(false);
1664
- const [showSolutions, setShowSolutions] = useState5(false);
1665
- const completedRef = useRef6(false);
1666
- const answeredRef = useRef6(false);
1962
+ const [passed, setPassed] = useState7(false);
1963
+ const [showSolutions, setShowSolutions] = useState7(false);
1964
+ const completedRef = useRef7(false);
1965
+ const answeredRef = useRef7(false);
1667
1966
  const reset = () => {
1668
1967
  completedRef.current = false;
1669
1968
  answeredRef.current = false;
@@ -1671,7 +1970,7 @@ function FillInTheBlanksInner(props, ref) {
1671
1970
  setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
1672
1971
  setShowSolutions(false);
1673
1972
  };
1674
- useEffect6(() => {
1973
+ useEffect7(() => {
1675
1974
  reset();
1676
1975
  }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
1677
1976
  const hasBlanks = blanks.length > 0;
@@ -1682,11 +1981,11 @@ function FillInTheBlanksInner(props, ref) {
1682
1981
  });
1683
1982
  const maxScore = blanks.length;
1684
1983
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1685
- const handle = useMemo8(() => {
1686
- const handleMax = maxScore || 1;
1687
- return {
1984
+ const handle = useMemo9(
1985
+ () => buildAssessmentHandle({
1986
+ checkId,
1688
1987
  getScore: () => score,
1689
- getMaxScore: () => handleMax,
1988
+ getMaxScore: () => maxScore || 1,
1690
1989
  getAnswerGiven: () => allFilled,
1691
1990
  resetTask: reset,
1692
1991
  showSolutions: () => setShowSolutions(true),
@@ -1696,12 +1995,23 @@ function FillInTheBlanksInner(props, ref) {
1696
1995
  response: values,
1697
1996
  correct: passedThreshold,
1698
1997
  score,
1699
- maxScore: handleMax
1700
- })
1701
- };
1702
- }, [allFilled, blanks.length, checkId, maxScore, passedThreshold, score, values]);
1703
- useImperativeHandle3(ref, () => handle, [handle]);
1704
- useRegisterAssessmentHandle(checkId, handle);
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);
1705
2015
  const check = () => {
1706
2016
  if (!hasBlanks) {
1707
2017
  if (isDevEnvironment4()) {
@@ -1732,20 +2042,20 @@ function FillInTheBlanksInner(props, ref) {
1732
2042
  });
1733
2043
  }
1734
2044
  };
1735
- useEffect6(() => {
2045
+ useEffect7(() => {
1736
2046
  if (!allFilled) answeredRef.current = false;
1737
2047
  }, [allFilled]);
1738
- useEffect6(() => {
2048
+ useEffect7(() => {
1739
2049
  if (props.autoCheck && allFilled) check();
1740
2050
  }, [allFilled, props.autoCheck, values, passedThreshold]);
1741
2051
  const reveal = showSolutions || passed && props.enableSolutionsButton;
1742
- return /* @__PURE__ */ jsxs5("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
1743
- /* @__PURE__ */ jsx7("p", { children: parsed.parts.map((part, i) => {
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) => {
1744
2054
  const blank = blanks.find((b) => b.id === part);
1745
- if (!blank) return /* @__PURE__ */ jsx7(React7.Fragment, { children: part }, i);
1746
- return /* @__PURE__ */ jsxs5("label", { style: { margin: "0 0.25em" }, children: [
1747
- /* @__PURE__ */ jsx7("span", { className: "lk-visually-hidden", children: blank.answer }),
1748
- /* @__PURE__ */ jsx7(
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(
1749
2059
  "input",
1750
2060
  {
1751
2061
  type: "text",
@@ -1761,52 +2071,40 @@ function FillInTheBlanksInner(props, ref) {
1761
2071
  )
1762
2072
  ] }, blank.id);
1763
2073
  }) }),
1764
- !props.autoCheck ? /* @__PURE__ */ jsx7("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
1765
- !hasBlanks ? /* @__PURE__ */ jsx7("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
1766
- allFilled ? /* @__PURE__ */ jsx7("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
1767
- props.enableRetry && passed ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1768
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
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
1769
2079
  ] });
1770
2080
  }
1771
- var FillInTheBlanksInnerForwarded = forwardRef3(FillInTheBlanksInner);
1772
- var FillInTheBlanks = forwardRef3(
2081
+ var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
2082
+ var FillInTheBlanks = forwardRef4(
1773
2083
  function FillInTheBlanks2(props, ref) {
1774
- return /* @__PURE__ */ jsx7(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx7(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2084
+ return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1775
2085
  }
1776
2086
  );
1777
2087
 
1778
2088
  // src/blocks/DragTheWords.tsx
1779
- import React8, { forwardRef as forwardRef4, useEffect as useEffect7, useImperativeHandle as useImperativeHandle4, useMemo as useMemo9, useRef as useRef7, useState as useState6 } from "react";
1780
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
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";
1781
2091
  var INTERACTION4 = "dragTheWords";
1782
2092
  function parseZones(template) {
1783
- const parts = [];
1784
- const answers = [];
1785
- const re = /\*([^*]+)\*/g;
1786
- let last = 0;
1787
- let match;
1788
- let n = 0;
1789
- while ((match = re.exec(template)) !== null) {
1790
- parts.push(template.slice(last, match.index));
1791
- answers.push(match[1].trim());
1792
- parts.push(`zone-${n++}`);
1793
- last = match.index + match[0].length;
1794
- }
1795
- parts.push(template.slice(last));
1796
- return { parts, answers };
2093
+ const { parts, values } = parseStarDelimitedTemplate(template, "zone");
2094
+ return { parts, answers: values };
1797
2095
  }
1798
2096
  function DragTheWordsInner(props, ref) {
1799
- const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2097
+ const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1800
2098
  const assessment = useAssessmentState(props.enclosingLessonId);
1801
- const { parts, answers } = useMemo9(() => parseZones(props.template), [props.template]);
1802
- const [zones, setZones] = useState6(
2099
+ const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
2100
+ const [zones, setZones] = useState8(
1803
2101
  () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
1804
2102
  );
1805
- const [pool, setPool] = useState6(() => [...props.words]);
1806
- const [keyboardWord, setKeyboardWord] = useState6(null);
1807
- const [passed, setPassed] = useState6(false);
1808
- const completedRef = useRef7(false);
1809
- const answeredRef = useRef7(false);
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);
1810
2108
  const reset = () => {
1811
2109
  completedRef.current = false;
1812
2110
  answeredRef.current = false;
@@ -1815,7 +2113,7 @@ function DragTheWordsInner(props, ref) {
1815
2113
  setPool([...props.words]);
1816
2114
  setKeyboardWord(null);
1817
2115
  };
1818
- useEffect7(() => {
2116
+ useEffect8(() => {
1819
2117
  reset();
1820
2118
  }, [checkId, props.template, props.words.join("\0")]);
1821
2119
  const hasZones = answers.length > 0;
@@ -1826,11 +2124,11 @@ function DragTheWordsInner(props, ref) {
1826
2124
  });
1827
2125
  const maxScore = answers.length;
1828
2126
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1829
- const handle = useMemo9(() => {
1830
- const handleMax = maxScore || 1;
1831
- return {
2127
+ const handle = useMemo10(
2128
+ () => buildAssessmentHandle({
2129
+ checkId,
1832
2130
  getScore: () => score,
1833
- getMaxScore: () => handleMax,
2131
+ getMaxScore: () => maxScore || 1,
1834
2132
  getAnswerGiven: () => allFilled,
1835
2133
  resetTask: reset,
1836
2134
  showSolutions: () => {
@@ -1841,12 +2139,25 @@ function DragTheWordsInner(props, ref) {
1841
2139
  response: zones,
1842
2140
  correct: passedThreshold,
1843
2141
  score,
1844
- maxScore: handleMax
1845
- })
1846
- };
1847
- }, [allFilled, answers.length, checkId, maxScore, passedThreshold, score, zones]);
1848
- useImperativeHandle4(ref, () => handle, [handle]);
1849
- useRegisterAssessmentHandle(checkId, handle);
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);
1850
2161
  const placeInZone = (zoneId, word) => {
1851
2162
  if (passed && !props.enableRetry) return;
1852
2163
  const prev = zones[zoneId];
@@ -1896,15 +2207,15 @@ function DragTheWordsInner(props, ref) {
1896
2207
  });
1897
2208
  }
1898
2209
  };
1899
- useEffect7(() => {
2210
+ useEffect8(() => {
1900
2211
  if (!allFilled) answeredRef.current = false;
1901
2212
  }, [allFilled]);
1902
- useEffect7(() => {
2213
+ useEffect8(() => {
1903
2214
  if (props.autoCheck && allFilled) check();
1904
2215
  }, [allFilled, props.autoCheck, zones, passedThreshold]);
1905
- return /* @__PURE__ */ jsxs6("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
1906
- /* @__PURE__ */ jsx8("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
1907
- /* @__PURE__ */ jsx8("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx8(
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(
1908
2219
  "button",
1909
2220
  {
1910
2221
  type: "button",
@@ -1918,9 +2229,9 @@ function DragTheWordsInner(props, ref) {
1918
2229
  },
1919
2230
  word
1920
2231
  )) }),
1921
- /* @__PURE__ */ jsx8("p", { children: parts.map((part, i) => {
1922
- if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx8(React8.Fragment, { children: part }, i);
1923
- return /* @__PURE__ */ jsx8(
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(
1924
2235
  "span",
1925
2236
  {
1926
2237
  role: "button",
@@ -1944,203 +2255,1169 @@ function DragTheWordsInner(props, ref) {
1944
2255
  part
1945
2256
  );
1946
2257
  }) }),
1947
- /* @__PURE__ */ jsx8("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
1948
- !hasZones ? /* @__PURE__ */ jsx8("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
1949
- allFilled ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
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
+ ] })
1950
3228
  ] });
1951
3229
  }
1952
- var DragTheWordsInnerForwarded = forwardRef4(DragTheWordsInner);
1953
- var DragTheWords = forwardRef4(function DragTheWords2(props, ref) {
1954
- return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1955
- });
3230
+ setLessonkitBlockType(ImageSlider, "ImageSlider");
1956
3231
 
1957
- // src/blocks/DragAndDrop.tsx
1958
- import { forwardRef as forwardRef5, useEffect as useEffect8, useImperativeHandle as useImperativeHandle5, useMemo as useMemo10, useRef as useRef8, useState as useState7 } from "react";
1959
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1960
- var INTERACTION5 = "dragAndDrop";
1961
- function DragAndDropInner(props, ref) {
1962
- const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
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);
1963
3240
  const assessment = useAssessmentState(props.enclosingLessonId);
1964
- const [assignments, setAssignments] = useState7(
1965
- () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
1966
- );
1967
- const [pool, setPool] = useState7(() => props.items.map((i) => i.id));
1968
- const [keyboardItem, setKeyboardItem] = useState7(null);
1969
- const [passed, setPassed] = useState7(false);
1970
- const completedRef = useRef8(false);
1971
- const reset = () => {
1972
- completedRef.current = false;
1973
- setPassed(false);
1974
- setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
1975
- setPool(props.items.map((i) => i.id));
1976
- setKeyboardItem(null);
1977
- };
1978
- useEffect8(() => {
1979
- reset();
1980
- }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
1981
- const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
1982
- const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
1983
- const handle = useMemo10(() => {
1984
- const maxScore = props.targets.length || 1;
1985
- let score = 0;
1986
- props.targets.forEach((t) => {
1987
- if (assignments[t.id] === t.accepts) score += 1;
1988
- });
1989
- return {
1990
- getScore: () => score,
1991
- getMaxScore: () => maxScore,
1992
- getAnswerGiven: () => allFilled,
1993
- resetTask: reset,
1994
- showSolutions: () => {
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);
1995
3251
  },
3252
+ showSolutions: () => setSelected(props.correctTargetId),
1996
3253
  getXAPIData: () => ({
1997
3254
  checkId,
1998
- interactionType: INTERACTION5,
1999
- response: assignments,
2000
- correct: allCorrect,
2001
- score,
2002
- maxScore
2003
- })
2004
- };
2005
- }, [allCorrect, allFilled, assignments, checkId, props.targets]);
2006
- useImperativeHandle5(ref, () => handle, [handle]);
2007
- useRegisterAssessmentHandle(checkId, handle);
2008
- const place = (targetId, itemId) => {
2009
- if (passed && !props.enableRetry) return;
2010
- const prev = assignments[targetId];
2011
- setAssignments((a) => ({ ...a, [targetId]: itemId }));
2012
- setPool((p) => {
2013
- const next = p.filter((id) => id !== itemId);
2014
- if (prev) next.push(prev);
2015
- return next;
2016
- });
2017
- setKeyboardItem(null);
2018
- };
2019
- const check = () => {
2020
- if (!allFilled) return;
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);
2021
3274
  assessment.answer({
2022
3275
  checkId,
2023
- interactionType: INTERACTION5,
2024
- response: assignments,
2025
- correct: allCorrect
3276
+ interactionType: INTERACTION6,
3277
+ response: selected,
3278
+ correct
2026
3279
  });
2027
- if (allCorrect && !completedRef.current) {
2028
- completedRef.current = true;
2029
- setPassed(true);
3280
+ if (correct) {
2030
3281
  assessment.complete({
2031
3282
  checkId,
2032
- interactionType: INTERACTION5,
2033
- score: props.targets.length,
2034
- maxScore: props.targets.length,
2035
- passingScore: props.passingScore ?? props.targets.length
3283
+ interactionType: INTERACTION6,
3284
+ score: 1,
3285
+ maxScore: 1,
3286
+ passingScore: props.passingScore
2036
3287
  });
2037
3288
  }
2038
3289
  };
2039
- return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2040
- /* @__PURE__ */ jsx9("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2041
- /* @__PURE__ */ jsx9("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
2042
- const item = props.items.find((i) => i.id === id);
2043
- return /* @__PURE__ */ jsx9(
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(
2044
3294
  "button",
2045
3295
  {
2046
3296
  type: "button",
2047
- draggable: true,
2048
- "data-testid": `drag-item-${id}`,
2049
- "aria-pressed": keyboardItem === id,
2050
- onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2051
- onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2052
- style: { margin: "0.25rem" },
2053
- children: item.label
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
2054
3308
  },
2055
- id
2056
- );
2057
- }) }),
2058
- /* @__PURE__ */ jsx9("ul", { children: props.targets.map((target) => {
2059
- const assigned = assignments[target.id];
2060
- const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2061
- return /* @__PURE__ */ jsxs7("li", { children: [
2062
- /* @__PURE__ */ jsx9("strong", { children: target.label }),
2063
- " ",
2064
- /* @__PURE__ */ jsx9(
2065
- "span",
2066
- {
2067
- role: "button",
2068
- tabIndex: 0,
2069
- "data-testid": `drop-${target.id}`,
2070
- onDragOver: (e) => e.preventDefault(),
2071
- onDrop: (e) => {
2072
- e.preventDefault();
2073
- const id = e.dataTransfer.getData("text/plain");
2074
- if (id) place(target.id, id);
2075
- },
2076
- onClick: () => keyboardItem && place(target.id, keyboardItem),
2077
- onKeyDown: (e) => {
2078
- if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2079
- },
2080
- style: {
2081
- display: "inline-block",
2082
- minWidth: "8em",
2083
- border: "1px dashed currentColor",
2084
- padding: "0.25em"
2085
- },
2086
- children: label
2087
- }
2088
- )
2089
- ] }, target.id);
2090
- }) }),
2091
- /* @__PURE__ */ jsx9("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2092
- allFilled ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
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
2093
3314
  ] });
2094
3315
  }
2095
- var DragAndDropInnerForwarded = forwardRef5(DragAndDropInner);
2096
- var DragAndDrop = forwardRef5(function DragAndDrop2(props, ref) {
2097
- return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
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 }) });
2098
3319
  });
3320
+ setLessonkitBlockType(FindHotspot, "FindHotspot");
2099
3321
 
2100
- // src/blocks/AssessmentSequence.tsx
2101
- import React10, { useCallback as useCallback3, useMemo as useMemo11, useState as useState8 } from "react";
2102
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2103
- function AssessmentSequence(props) {
2104
- const sequential = props.sequential !== false;
2105
- const childArray = React10.Children.toArray(props.children).filter(React10.isValidElement);
2106
- const [index, setIndex] = useState8(0);
2107
- const current = childArray[index] ?? null;
2108
- const goNext = useCallback3(() => {
2109
- setIndex((i) => Math.min(i + 1, childArray.length - 1));
2110
- }, [childArray.length]);
2111
- const goPrev = useCallback3(() => {
2112
- setIndex((i) => Math.max(i - 1, 0));
2113
- }, []);
2114
- const progress = useMemo11(
2115
- () => ({ current: index + 1, total: childArray.length }),
2116
- [index, childArray.length]
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]
2117
3367
  );
2118
- if (!sequential) {
2119
- return /* @__PURE__ */ jsx10(AssessmentSequenceProvider, { children: /* @__PURE__ */ jsx10("section", { "aria-label": "Assessment sequence", children: props.children }) });
2120
- }
2121
- return /* @__PURE__ */ jsx10(AssessmentSequenceProvider, { children: /* @__PURE__ */ jsxs8("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
2122
- /* @__PURE__ */ jsxs8("p", { children: [
2123
- "Question ",
2124
- progress.current,
2125
- " of ",
2126
- progress.total
2127
- ] }),
2128
- /* @__PURE__ */ jsx10("div", { "data-testid": "assessment-sequence-step", children: current }),
2129
- /* @__PURE__ */ jsxs8("nav", { "aria-label": "Sequence navigation", children: [
2130
- /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "sequence-prev", disabled: index === 0, onClick: goPrev, children: "Previous" }),
2131
- /* @__PURE__ */ jsx10(
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(
2132
3392
  "button",
2133
3393
  {
2134
3394
  type: "button",
2135
- "data-testid": "sequence-next",
2136
- disabled: index >= childArray.length - 1,
2137
- onClick: goNext,
2138
- children: "Next"
2139
- }
2140
- )
2141
- ] })
2142
- ] }) });
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
+ ] });
2143
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");
2144
3421
 
2145
3422
  // src/index.tsx
2146
3423
  import {
@@ -2154,14 +3431,14 @@ import {
2154
3431
  } from "@lessonkit/core";
2155
3432
 
2156
3433
  // src/theme/ThemeProvider.tsx
2157
- import React11, {
3434
+ import React25, {
2158
3435
  createContext as createContext4,
2159
- useCallback as useCallback4,
3436
+ useCallback as useCallback9,
2160
3437
  useContext as useContext4,
2161
3438
  useLayoutEffect as useLayoutEffect2,
2162
- useMemo as useMemo12,
2163
- useRef as useRef9,
2164
- useState as useState9
3439
+ useMemo as useMemo17,
3440
+ useRef as useRef12,
3441
+ useState as useState19
2165
3442
  } from "react";
2166
3443
  import {
2167
3444
  brandThemeOverrides,
@@ -2188,11 +3465,11 @@ function applyCssVariables(target, vars, previousKeys) {
2188
3465
  }
2189
3466
 
2190
3467
  // src/theme/ThemeProvider.tsx
2191
- import { jsx as jsx11 } from "react/jsx-runtime";
3468
+ import { jsx as jsx24 } from "react/jsx-runtime";
2192
3469
  var ThemeContext = createContext4(null);
2193
3470
  var useIsoLayoutEffect2 = (
2194
3471
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
2195
- typeof window !== "undefined" ? useLayoutEffect2 : React11.useEffect
3472
+ typeof window !== "undefined" ? useLayoutEffect2 : React25.useEffect
2196
3473
  );
2197
3474
  function getSystemMode() {
2198
3475
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -2211,7 +3488,7 @@ function ThemeProvider(props) {
2211
3488
  const preset = props.preset ?? "default";
2212
3489
  const mode = props.mode ?? "light";
2213
3490
  const targetKind = props.target ?? "document";
2214
- const [resolvedMode, setResolvedMode] = useState9(
3491
+ const [resolvedMode, setResolvedMode] = useState19(
2215
3492
  () => mode === "system" ? getSystemMode() : mode
2216
3493
  );
2217
3494
  useIsoLayoutEffect2(() => {
@@ -2227,20 +3504,20 @@ function ThemeProvider(props) {
2227
3504
  return () => mq.removeEventListener("change", onChange);
2228
3505
  }, [mode]);
2229
3506
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
2230
- const effectiveTheme = useMemo12(() => {
3507
+ const effectiveTheme = useMemo17(() => {
2231
3508
  const modeBase = resolveModeBase(mode, dataTheme);
2232
3509
  const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
2233
3510
  return mergeThemes(base, props.theme ?? {});
2234
3511
  }, [preset, mode, dataTheme, props.theme]);
2235
- const hostRef = useRef9(null);
2236
- const appliedKeysRef = useRef9(/* @__PURE__ */ new Set());
3512
+ const hostRef = useRef12(null);
3513
+ const appliedKeysRef = useRef12(/* @__PURE__ */ new Set());
2237
3514
  useIsoLayoutEffect2(() => {
2238
3515
  if (targetKind === "document" && typeof document !== "undefined") {
2239
3516
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
2240
3517
  return () => document.documentElement.removeAttribute("data-lk-theme");
2241
3518
  }
2242
3519
  }, [targetKind, dataTheme]);
2243
- const inject = useCallback4(() => {
3520
+ const inject = useCallback9(() => {
2244
3521
  const vars = themeToCssVariables(effectiveTheme);
2245
3522
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
2246
3523
  if (!el) return;
@@ -2257,7 +3534,7 @@ function ThemeProvider(props) {
2257
3534
  appliedKeysRef.current = /* @__PURE__ */ new Set();
2258
3535
  };
2259
3536
  }, [inject, targetKind]);
2260
- const value = useMemo12(
3537
+ const value = useMemo17(
2261
3538
  () => ({
2262
3539
  theme: effectiveTheme,
2263
3540
  preset,
@@ -2267,9 +3544,9 @@ function ThemeProvider(props) {
2267
3544
  [effectiveTheme, preset, mode, dataTheme]
2268
3545
  );
2269
3546
  if (targetKind === "document") {
2270
- return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("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 }) });
2271
3548
  }
2272
- return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("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 }) });
2273
3550
  }
2274
3551
  function useTheme() {
2275
3552
  const ctx = useContext4(ThemeContext);
@@ -2279,9 +3556,263 @@ function useTheme() {
2279
3556
  return ctx;
2280
3557
  }
2281
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
+
2282
3812
  // src/blockCatalog.ts
2283
3813
  var blockCatalogVersion = 1;
2284
3814
  var blockCatalogV2Version = 2;
3815
+ var blockCatalogV3Version = 3;
2285
3816
  var BLOCK_CATALOG = [
2286
3817
  {
2287
3818
  type: "Course",
@@ -2468,7 +3999,7 @@ var BLOCK_CATALOG = [
2468
3999
  }
2469
4000
  }
2470
4001
  ];
2471
- var assessmentBehaviourProps = [
4002
+ var assessmentBehaviourProps2 = [
2472
4003
  { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
2473
4004
  { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
2474
4005
  { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
@@ -2486,7 +4017,7 @@ var v2AssessmentEntries = [
2486
4017
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2487
4018
  { name: "question", type: "string", required: true, description: "Question text." },
2488
4019
  { name: "answer", type: "boolean", required: true, description: "Correct answer." },
2489
- ...assessmentBehaviourProps
4020
+ ...assessmentBehaviourProps2
2490
4021
  ],
2491
4022
  requiredIds: ["checkId"],
2492
4023
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2511,7 +4042,7 @@ var v2AssessmentEntries = [
2511
4042
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2512
4043
  { name: "template", type: "string", required: true, description: "Text with *blank* markers." },
2513
4044
  { name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
2514
- ...assessmentBehaviourProps
4045
+ ...assessmentBehaviourProps2
2515
4046
  ],
2516
4047
  requiredIds: ["checkId"],
2517
4048
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2535,7 +4066,7 @@ var v2AssessmentEntries = [
2535
4066
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2536
4067
  { name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
2537
4068
  { name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
2538
- ...assessmentBehaviourProps
4069
+ ...assessmentBehaviourProps2
2539
4070
  ],
2540
4071
  requiredIds: ["checkId"],
2541
4072
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2559,7 +4090,7 @@ var v2AssessmentEntries = [
2559
4090
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2560
4091
  { name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
2561
4092
  { name: "words", type: "string[]", required: true, description: "Draggable word bank." },
2562
- ...assessmentBehaviourProps
4093
+ ...assessmentBehaviourProps2
2563
4094
  ],
2564
4095
  requiredIds: ["checkId"],
2565
4096
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2583,7 +4114,7 @@ var v2AssessmentEntries = [
2583
4114
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2584
4115
  { name: "text", type: "string", required: true, description: "Source text." },
2585
4116
  { name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
2586
- ...assessmentBehaviourProps
4117
+ ...assessmentBehaviourProps2
2587
4118
  ],
2588
4119
  requiredIds: ["checkId"],
2589
4120
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2605,7 +4136,7 @@ var v2AssessmentEntries = [
2605
4136
  props: [
2606
4137
  { name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
2607
4138
  { name: "sequential", type: "boolean", required: false, description: "One question at a time." },
2608
- ...assessmentBehaviourProps.filter((p) => p.name !== "passingScore")
4139
+ ...assessmentBehaviourProps2.filter((p) => p.name !== "passingScore")
2609
4140
  ],
2610
4141
  requiredIds: [],
2611
4142
  parentConstraints: ["Lesson"],
@@ -2623,6 +4154,7 @@ var BLOCK_CATALOG_V2 = [
2623
4154
  ...BLOCK_CATALOG,
2624
4155
  ...v2AssessmentEntries
2625
4156
  ];
4157
+ var BLOCK_CATALOG_V3 = buildV3CatalogFromV2(BLOCK_CATALOG_V2);
2626
4158
  function cloneCatalogEntry(entry) {
2627
4159
  return {
2628
4160
  ...entry,
@@ -2630,6 +4162,7 @@ function cloneCatalogEntry(entry) {
2630
4162
  aliases: entry.aliases ? [...entry.aliases] : void 0,
2631
4163
  optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
2632
4164
  parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
4165
+ allowedChildTypes: entry.allowedChildTypes ? [...entry.allowedChildTypes] : void 0,
2633
4166
  a11y: { ...entry.a11y },
2634
4167
  theming: {
2635
4168
  ...entry.theming,
@@ -2642,34 +4175,48 @@ function cloneCatalogEntry(entry) {
2642
4175
  };
2643
4176
  }
2644
4177
  function buildBlockCatalog(opts) {
2645
- const version = opts?.version ?? 2;
2646
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4178
+ const version = opts?.version ?? 3;
4179
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2647
4180
  return source.map((entry) => cloneCatalogEntry(entry));
2648
4181
  }
2649
4182
  function getBlockCatalogEntry(type, opts) {
2650
- const version = opts?.version ?? 2;
2651
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4183
+ const version = opts?.version ?? 3;
4184
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2652
4185
  return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
2653
4186
  }
2654
4187
  export {
4188
+ Accordion,
2655
4189
  AssessmentSequence,
2656
4190
  BLOCK_CATALOG,
2657
4191
  BLOCK_CATALOG_V2,
4192
+ BLOCK_CATALOG_V3,
2658
4193
  Course,
4194
+ DialogCards,
2659
4195
  DragAndDrop,
2660
4196
  DragTheWords,
2661
4197
  FillInTheBlanks,
4198
+ FindHotspot,
4199
+ FindMultipleHotspots,
4200
+ Flashcards,
4201
+ Heading,
4202
+ Image,
4203
+ ImageHotspots,
4204
+ ImageSlider,
4205
+ InteractiveBook,
2662
4206
  KnowledgeCheck,
2663
4207
  Lesson,
2664
4208
  LessonkitProvider,
2665
4209
  MarkTheWords,
4210
+ Page,
2666
4211
  ProgressTracker,
2667
4212
  Quiz,
2668
4213
  Reflection,
2669
4214
  Scenario,
4215
+ Text,
2670
4216
  ThemeProvider,
2671
4217
  TrueFalse,
2672
4218
  blockCatalogV2Version,
4219
+ blockCatalogV3Version,
2673
4220
  blockCatalogVersion,
2674
4221
  buildBlockCatalog,
2675
4222
  buildTelemetryEvent2 as buildTelemetryEvent,