@lessonkit/react 1.1.0 → 1.3.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 useRef5, 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
  }
@@ -131,6 +152,9 @@ import {
131
152
  resetStoragePortForTests
132
153
  } from "@lessonkit/core";
133
154
 
155
+ // src/provider/useLessonkitProviderRuntime.ts
156
+ import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
157
+
134
158
  // src/runtime/progress.ts
135
159
  import { createProgressController } from "@lessonkit/core";
136
160
 
@@ -207,7 +231,9 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
207
231
  xapiStatementSent = true;
208
232
  }
209
233
  }
210
- forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
234
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
235
+ onBridgeMiss: opts.onLxpackBridgeMiss
236
+ });
211
237
  const emitCtx = {
212
238
  courseId: opts.event.courseId,
213
239
  sessionId: opts.event.sessionId,
@@ -218,56 +244,25 @@ async function emitCourseStartedNonTrackingPipeline(opts) {
218
244
  }
219
245
 
220
246
  // src/runtime/plugins.ts
221
- import { createPluginRegistry } from "@lessonkit/core";
247
+ import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
222
248
  function createReactPluginHost(plugins) {
223
249
  if (!plugins?.length) return null;
224
250
  return createPluginRegistry(plugins);
225
251
  }
226
252
  function buildPluginContext(opts) {
227
- return {
228
- courseId: opts.courseId,
229
- sessionId: opts.sessionId,
230
- attemptId: opts.attemptId,
231
- user: opts.user
232
- };
253
+ return buildPluginContextFromCore(opts);
233
254
  }
234
255
  function emitTelemetryWithPlugins(opts) {
235
256
  const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
236
257
  if (next === null) return;
237
258
  emitTelemetry(opts.tracking, opts.xapi, next, {
238
259
  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
260
+ extraSinks: opts.extraSinks,
261
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss
252
262
  });
253
263
  }
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
264
 
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();
265
+ // src/provider/courseStarted/emit.ts
271
266
  var courseStartedTrackingFlightKey = null;
272
267
  function isTrackingActive(tracking) {
273
268
  return tracking?.enabled !== false;
@@ -303,9 +298,10 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
303
298
  try {
304
299
  if (shouldCommit && !shouldCommit()) return false;
305
300
  tracking.track(event);
306
- await tracking.flush?.();
307
- if (shouldCommit && !shouldCommit()) return false;
308
301
  markCourseStartedEmittedToTracking(storage, sessionId, courseId);
302
+ const delivered = await tracking.flush?.();
303
+ if (delivered === false) return false;
304
+ if (shouldCommit && !shouldCommit()) return false;
309
305
  return true;
310
306
  } catch {
311
307
  return false;
@@ -322,6 +318,7 @@ async function emitCourseStartedPipelineOnly(opts) {
322
318
  event: opts.event,
323
319
  xapi: opts.xapi,
324
320
  lxpackBridge: opts.lxpackBridge,
321
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
325
322
  extraSinks: opts.extraSinks,
326
323
  skipXapi: opts.skipXapi
327
324
  });
@@ -374,6 +371,7 @@ async function emitCourseStartedToTrackingOnly(opts) {
374
371
  event,
375
372
  xapi: null,
376
373
  lxpackBridge: opts.lxpackBridge,
374
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
377
375
  extraSinks: opts.extraSinks,
378
376
  skipXapi: true
379
377
  });
@@ -427,6 +425,36 @@ function assertTrackingSinkConfig(tracking) {
427
425
  "[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
428
426
  );
429
427
  }
428
+
429
+ // src/runtime/telemetry.ts
430
+ import { createTrackingClient } from "@lessonkit/core";
431
+ function createTrackingClientFromConfig(config, observability) {
432
+ if (config.tracking?.enabled === false) return createTrackingClient();
433
+ if (config.tracking?.createClient) return config.tracking.createClient();
434
+ return createTrackingClient({
435
+ sink: config.tracking?.sink,
436
+ batchSink: config.tracking?.batchSink,
437
+ batch: config.tracking?.batch,
438
+ onBufferDrop: observability?.onTelemetryBufferDrop
439
+ });
440
+ }
441
+ async function disposeTrackingClient(client) {
442
+ try {
443
+ await client?.flush?.();
444
+ } catch {
445
+ }
446
+ try {
447
+ await client?.dispose?.();
448
+ } catch {
449
+ }
450
+ }
451
+
452
+ // src/provider/useLessonkitProviderRuntime.ts
453
+ var useIsoLayoutEffect = (
454
+ /* v8 ignore next -- SSR uses useEffect when window is unavailable */
455
+ typeof window !== "undefined" ? useLayoutEffect : useEffect
456
+ );
457
+ var defaultStorage = createSessionStoragePort();
430
458
  function useLessonkitProviderRuntime(config) {
431
459
  const normalizedCourseId = useMemo(
432
460
  () => assertValidId(config.courseId, "courseId"),
@@ -437,6 +465,14 @@ function useLessonkitProviderRuntime(config) {
437
465
  [config, normalizedCourseId]
438
466
  );
439
467
  const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
468
+ useEffect(() => {
469
+ if (useV2Runtime) return;
470
+ const g = globalThis;
471
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
472
+ console.warn(
473
+ '[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
474
+ );
475
+ }, [useV2Runtime]);
440
476
  const extraSinksRef = useRef(normalizedConfig.sinks);
441
477
  extraSinksRef.current = normalizedConfig.sinks;
442
478
  const headlessRef = useRef(null);
@@ -455,7 +491,16 @@ function useLessonkitProviderRuntime(config) {
455
491
  courseIdRef.current = normalizedCourseId;
456
492
  const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
457
493
  lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
458
- const pluginHost = useMemo(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
494
+ const observabilityRef = useRef(normalizedConfig.observability);
495
+ observabilityRef.current = normalizedConfig.observability;
496
+ const onLxpackBridgeMiss = useCallback((event) => {
497
+ observabilityRef.current?.onLxpackBridgeMiss?.(event);
498
+ }, []);
499
+ const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
500
+ const pluginHost = useMemo(
501
+ () => createReactPluginHost(normalizedConfig.plugins),
502
+ [pluginsFingerprint]
503
+ );
459
504
  const pluginHostRef = useRef(pluginHost);
460
505
  pluginHostRef.current = pluginHost;
461
506
  const progressRef = useRef(createProgressController());
@@ -471,7 +516,9 @@ function useLessonkitProviderRuntime(config) {
471
516
  headlessRef.current = createLessonkitRuntime({
472
517
  courseId: normalizedCourseId,
473
518
  runtimeVersion: "v2",
474
- session: normalizedConfig.session
519
+ session: normalizedConfig.session,
520
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
521
+ deferPluginSetup: true
475
522
  });
476
523
  progressRef.current = headlessRef.current.progress;
477
524
  } else {
@@ -485,7 +532,9 @@ function useLessonkitProviderRuntime(config) {
485
532
  headlessRef.current = createLessonkitRuntime({
486
533
  courseId: normalizedCourseId,
487
534
  runtimeVersion: "v2",
488
- session: normalizedConfig.session
535
+ session: normalizedConfig.session,
536
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
537
+ deferPluginSetup: true
489
538
  });
490
539
  }
491
540
  if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
@@ -509,7 +558,7 @@ function useLessonkitProviderRuntime(config) {
509
558
  }, []);
510
559
  const activeLessonIdRef = useRef(progress.activeLessonId);
511
560
  activeLessonIdRef.current = progress.activeLessonId;
512
- const xapiQueueRef = useRef(createInMemoryXAPIQueue());
561
+ const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
513
562
  const xapiRef = useRef(null);
514
563
  const [xapi, setXapi] = useState(null);
515
564
  const prevXapiCourseIdRef = useRef(normalizedCourseId);
@@ -530,7 +579,7 @@ function useLessonkitProviderRuntime(config) {
530
579
  }
531
580
  void xapiRef.current?.flush();
532
581
  }
533
- xapiQueueRef.current = createInMemoryXAPIQueue();
582
+ xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
534
583
  prevXapiCourseIdRef.current = courseId;
535
584
  xapiCourseStartedSentOnClientRef.current = false;
536
585
  }
@@ -609,7 +658,7 @@ function useLessonkitProviderRuntime(config) {
609
658
  );
610
659
  useIsoLayoutEffect(() => {
611
660
  const prev = trackingRef.current;
612
- const baseSink = normalizedConfig.tracking?.sink;
661
+ const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
613
662
  const userBatchSink = normalizedConfig.tracking?.batchSink;
614
663
  assertTrackingSinkConfig(normalizedConfig.tracking);
615
664
  const sink = pluginHostRef.current && baseSink ? (
@@ -630,9 +679,12 @@ function useLessonkitProviderRuntime(config) {
630
679
  }
631
680
  return userBatchSink(perEventForBatch);
632
681
  } : userBatchSink;
633
- const next = createTrackingClientFromConfig({
634
- tracking: { ...normalizedConfig.tracking, sink, batchSink }
635
- });
682
+ const next = createTrackingClientFromConfig(
683
+ {
684
+ tracking: { ...normalizedConfig.tracking, sink, batchSink }
685
+ },
686
+ observabilityRef.current
687
+ );
636
688
  trackingRef.current = next;
637
689
  trackingClientForUnmountRef.current = next;
638
690
  setTracking(next);
@@ -659,6 +711,7 @@ function useLessonkitProviderRuntime(config) {
659
711
  attemptId: attemptIdRef.current,
660
712
  user: userRef.current,
661
713
  lxpackBridge: lxpackBridgeModeRef.current,
714
+ onLxpackBridgeMiss,
662
715
  extraSinks: extraSinksRef.current,
663
716
  skipXapi: xapiCourseStartedSentOnClientRef.current,
664
717
  onXapiStatementSent: () => {
@@ -700,9 +753,10 @@ function useLessonkitProviderRuntime(config) {
700
753
  user: userRef.current
701
754
  }),
702
755
  lxpackBridge: lxpackBridgeModeRef.current,
756
+ onLxpackBridgeMiss,
703
757
  extraSinks: extraSinksRef.current
704
758
  });
705
- }, []);
759
+ }, [onLxpackBridgeMiss]);
706
760
  const emitLifecycleEvent = useCallback(
707
761
  (name, data, lessonId) => {
708
762
  const event = tryBuildTelemetryEvent({
@@ -758,12 +812,13 @@ function useLessonkitProviderRuntime(config) {
758
812
  attemptId: attemptIdRef.current,
759
813
  user: userRef.current,
760
814
  lxpackBridge: lxpackBridgeModeRef.current,
815
+ onLxpackBridgeMiss,
761
816
  extraSinks: extraSinksRef.current
762
817
  });
763
818
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
764
819
  }
765
820
  })();
766
- }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
821
+ }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
767
822
  const emitLessonCompleted = useCallback(
768
823
  (lessonId, durationMs) => {
769
824
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -812,6 +867,22 @@ function useLessonkitProviderRuntime(config) {
812
867
  })();
813
868
  };
814
869
  }, []);
870
+ useEffect(() => {
871
+ if (typeof document === "undefined") return;
872
+ const flushOnExit = () => {
873
+ void xapiRef.current?.flush();
874
+ void trackingRef.current?.flush?.();
875
+ };
876
+ const onVisibilityChange = () => {
877
+ if (document.visibilityState === "hidden") flushOnExit();
878
+ };
879
+ document.addEventListener("visibilitychange", onVisibilityChange);
880
+ window.addEventListener("pagehide", flushOnExit);
881
+ return () => {
882
+ document.removeEventListener("visibilitychange", onVisibilityChange);
883
+ window.removeEventListener("pagehide", flushOnExit);
884
+ };
885
+ }, []);
815
886
  const setActiveLesson = useCallback(
816
887
  (lessonId) => {
817
888
  if (useV2Runtime && headlessRef.current) {
@@ -875,20 +946,34 @@ function useLessonkitProviderRuntime(config) {
875
946
  session: normalizedConfig.session
876
947
  });
877
948
  }
878
- }, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
949
+ }, [
950
+ useV2Runtime,
951
+ normalizedCourseId,
952
+ sessionAttemptId,
953
+ sessionConfiguredId,
954
+ sessionUserKey,
955
+ normalizedConfig.session
956
+ ]);
957
+ useEffect(() => {
958
+ if (!useV2Runtime || !headlessRef.current) return;
959
+ headlessRef.current.updateConfig({
960
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
961
+ });
962
+ }, [useV2Runtime, pluginHost]);
879
963
  useEffect(() => {
880
- if (!pluginHost) return;
964
+ const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
965
+ if (!host) return;
881
966
  const ctx = buildPluginContext({
882
967
  courseId: courseIdRef.current,
883
968
  sessionId: sessionIdRef.current,
884
969
  attemptId: attemptIdRef.current,
885
970
  user: userRef.current
886
971
  });
887
- pluginHost.setupAll(ctx);
972
+ host.setupAll(ctx);
888
973
  return () => {
889
- pluginHost.disposeAll();
974
+ host.disposeAll();
890
975
  };
891
- }, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
976
+ }, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
892
977
  useEffect(() => {
893
978
  const nextConfigured = normalizedConfig.session?.sessionId;
894
979
  const prevConfigured = prevConfiguredSessionIdRef.current;
@@ -917,6 +1002,7 @@ function useLessonkitProviderRuntime(config) {
917
1002
  config: normalizedConfig,
918
1003
  tracking,
919
1004
  xapi,
1005
+ storage: defaultStorage,
920
1006
  session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
921
1007
  progress,
922
1008
  setActiveLesson,
@@ -1051,465 +1137,849 @@ function getLessonMountCount(lessonId) {
1051
1137
  return mountCounts.get(lessonId) ?? 0;
1052
1138
  }
1053
1139
 
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
- );
1140
+ // src/components/Quiz.tsx
1141
+ import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
1142
+ import { visuallyHiddenStyle } from "@lessonkit/accessibility";
1143
+
1144
+ // src/assessment/AssessmentLessonGuard.tsx
1145
+ import { useEffect as useEffect2 } from "react";
1146
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1147
+ var warnedAssessmentOutsideLesson = false;
1148
+ function resetAssessmentWarningsForTests() {
1149
+ warnedAssessmentOutsideLesson = false;
1157
1150
  }
1158
- function Quiz(props) {
1151
+ function AssessmentLessonGuard(props) {
1159
1152
  const enclosingLessonId = useEnclosingLessonId();
1160
1153
  const missingLesson = enclosingLessonId === void 0;
1161
1154
  useEffect2(() => {
1162
1155
  if (!missingLesson || isDevEnvironment4()) return;
1163
- if (!warnedQuizOutsideLesson) {
1164
- warnedQuizOutsideLesson = true;
1156
+ if (!warnedAssessmentOutsideLesson) {
1157
+ warnedAssessmentOutsideLesson = true;
1165
1158
  console.error(
1166
- "[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
1159
+ `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1167
1160
  );
1168
1161
  }
1169
- }, [missingLesson]);
1162
+ }, [missingLesson, props.blockLabel]);
1170
1163
  if (missingLesson && isDevEnvironment4()) {
1171
- throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1164
+ throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1172
1165
  }
1173
1166
  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." }) });
1167
+ return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs("p", { children: [
1168
+ props.blockLabel,
1169
+ " must be placed inside a Lesson."
1170
+ ] }) });
1175
1171
  }
1176
- return /* @__PURE__ */ jsx2(QuizInner, { ...props, enclosingLessonId });
1172
+ return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
1177
1173
  }
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;
1174
+
1175
+ // src/assessment/internal/buildAssessmentHandle.ts
1176
+ function buildAssessmentHandle(opts) {
1177
+ return {
1178
+ getScore: opts.getScore,
1179
+ getMaxScore: opts.getMaxScore,
1180
+ getAnswerGiven: opts.getAnswerGiven,
1181
+ resetTask: opts.resetTask,
1182
+ showSolutions: opts.showSolutions,
1183
+ getXAPIData: opts.getXAPIData,
1184
+ ...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
1185
+ ...opts.resume ? { resume: opts.resume } : {}
1202
1186
  };
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
1187
  }
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
- ] }) });
1188
+
1189
+ // src/assessment/internal/resumeState.ts
1190
+ function readBooleanField(state, key) {
1191
+ const value = state[key];
1192
+ if (value === true || value === false || value === null) return value;
1193
+ return void 0;
1194
+ }
1195
+ function readStringField(state, key) {
1196
+ const value = state[key];
1197
+ if (typeof value === "string" || value === null) return value;
1198
+ return void 0;
1199
+ }
1200
+ function readNumberField(state, key) {
1201
+ const value = state[key];
1202
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1203
+ if (value === null) return null;
1204
+ return void 0;
1205
+ }
1206
+ function readBooleanStateField(state, key, apply) {
1207
+ const value = state[key];
1208
+ if (typeof value === "boolean") apply(value);
1290
1209
  }
1291
1210
 
1292
- // src/blocks/TrueFalse.tsx
1293
- import React5, { forwardRef, useEffect as useEffect4, useImperativeHandle, useMemo as useMemo6, useRef as useRef4, useState as useState3 } from "react";
1211
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1212
+ import { useImperativeHandle as useImperativeHandle2 } from "react";
1294
1213
 
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;
1301
- }
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>`);
1316
- }
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
- ] }) });
1214
+ // src/compound/CompoundProvider.tsx
1215
+ import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
1216
+ import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
1217
+
1218
+ // src/compound/aggregateScores.ts
1219
+ function aggregateAssessmentScores(handles, opts) {
1220
+ let score = 0;
1221
+ let maxScore = 0;
1222
+ let allAnswered = true;
1223
+ for (const entry of handles) {
1224
+ const handle = "handle" in entry ? entry.handle : entry;
1225
+ const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
1226
+ score += handle.getScore();
1227
+ maxScore += handle.getMaxScore();
1228
+ const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
1229
+ if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
1322
1230
  }
1323
- return /* @__PURE__ */ jsx3(Fragment, { children: props.children(enclosingLessonId) });
1231
+ return { score, maxScore, allAnswered };
1232
+ }
1233
+
1234
+ // src/compound/CompoundHydrationBridge.tsx
1235
+ import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
1236
+ import { jsx as jsx3 } from "react/jsx-runtime";
1237
+ var CompoundHydrationBridgeContext = createContext3(
1238
+ null
1239
+ );
1240
+ function CompoundHydrationBridgeProvider({ children }) {
1241
+ const bridgeRef = useRef2(null);
1242
+ return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
1243
+ }
1244
+ function useCompoundHydrationBridgeRef() {
1245
+ return useContext3(CompoundHydrationBridgeContext);
1324
1246
  }
1325
1247
 
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";
1248
+ // src/compound/CompoundPageIndexContext.tsx
1249
+ import { createContext as createContext4, useContext as useContext4 } from "react";
1328
1250
  import { jsx as jsx4 } from "react/jsx-runtime";
1329
- var AssessmentSequenceContext = createContext3(null);
1330
- function AssessmentSequenceProvider({ children }) {
1251
+ var CompoundPageIndexContext = createContext4(void 0);
1252
+ function CompoundPageIndexProvider({
1253
+ pageIndex,
1254
+ children
1255
+ }) {
1256
+ return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
1257
+ }
1258
+ function useCompoundPageIndex() {
1259
+ return useContext4(CompoundPageIndexContext);
1260
+ }
1261
+
1262
+ // src/compound/CompoundProvider.tsx
1263
+ import { jsx as jsx5 } from "react/jsx-runtime";
1264
+ var CompoundRegistryContext = createContext5(null);
1265
+ var CompoundHandlesVersionContext = createContext5(0);
1266
+ function CompoundProvider({
1267
+ children,
1268
+ activePageIndex: _activePageIndex,
1269
+ onActivePageIndexChange: _onActivePageIndexChange
1270
+ }) {
1331
1271
  const registryRef = useRef3(/* @__PURE__ */ new Map());
1332
- const register = useCallback2((checkId, handle) => {
1333
- registryRef.current.set(checkId, handle);
1272
+ const [handlesVersion, setHandlesVersion] = useState2(0);
1273
+ const register = useCallback2((checkId, handle, pageIndex) => {
1274
+ const prev = registryRef.current.get(checkId);
1275
+ if (prev && prev.handle !== handle) {
1276
+ const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
1277
+ if (isDevEnvironment4()) {
1278
+ console.error(message);
1279
+ } else {
1280
+ console.warn(message);
1281
+ }
1282
+ }
1283
+ registryRef.current.set(checkId, { handle, pageIndex });
1284
+ if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
1285
+ setHandlesVersion((v) => v + 1);
1286
+ }
1334
1287
  return () => {
1335
- registryRef.current.delete(checkId);
1288
+ const current = registryRef.current.get(checkId);
1289
+ if (current?.handle === handle) {
1290
+ registryRef.current.delete(checkId);
1291
+ setHandlesVersion((v) => v + 1);
1292
+ }
1336
1293
  };
1337
1294
  }, []);
1338
- const value = useMemo5(
1295
+ const registryValue = useMemo4(
1339
1296
  () => ({
1340
1297
  register,
1341
- getHandles: () => registryRef.current
1298
+ getHandles: () => {
1299
+ const handles = /* @__PURE__ */ new Map();
1300
+ for (const [checkId, entry] of registryRef.current) {
1301
+ handles.set(checkId, entry.handle);
1302
+ }
1303
+ return handles;
1304
+ },
1305
+ getRegisteredHandles: () => registryRef.current
1342
1306
  }),
1343
1307
  [register]
1344
1308
  );
1345
- return /* @__PURE__ */ jsx4(AssessmentSequenceContext.Provider, { value, children });
1309
+ return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
1346
1310
  }
1347
- function useAssessmentSequenceRegistry() {
1348
- return useContext3(AssessmentSequenceContext);
1311
+ function useCompoundRegistry() {
1312
+ const registry = useContext5(CompoundRegistryContext);
1313
+ const handlesVersion = useContext5(CompoundHandlesVersionContext);
1314
+ if (!registry) return null;
1315
+ return { ...registry, handlesVersion };
1316
+ }
1317
+ function useCompoundHandlesVersion() {
1318
+ return useContext5(CompoundHandlesVersionContext);
1349
1319
  }
1350
1320
  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]);
1321
+ const registry = useContext5(CompoundRegistryContext);
1322
+ const pageIndex = useCompoundPageIndex();
1323
+ React5.useLayoutEffect(() => {
1324
+ if (!registry || !handle) return;
1325
+ return registry.register(checkId, handle, pageIndex);
1326
+ }, [registry, checkId, handle, pageIndex]);
1327
+ }
1328
+ function useCompoundHandleRef(ref, opts) {
1329
+ const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
1330
+ const bridgeRef = useCompoundHydrationBridgeRef();
1331
+ const setIndexClamped = useCallback2(
1332
+ (index) => {
1333
+ const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
1334
+ setActivePageIndex(next);
1335
+ },
1336
+ [pageCount, setActivePageIndex]
1337
+ );
1338
+ useImperativeHandle(
1339
+ ref,
1340
+ () => ({
1341
+ getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
1342
+ getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
1343
+ getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
1344
+ answerPageIndex: activePageIndex
1345
+ }).allAnswered,
1346
+ resetTask: () => {
1347
+ for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
1348
+ },
1349
+ showSolutions: () => {
1350
+ if (!opts.enableSolutionsButton) return;
1351
+ for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
1352
+ },
1353
+ getCurrentState: () => {
1354
+ const childStates = {};
1355
+ for (const [checkId, entry] of getRegisteredHandles()) {
1356
+ if (entry.handle.getCurrentState) {
1357
+ childStates[checkId] = entry.handle.getCurrentState();
1358
+ }
1359
+ }
1360
+ return createCompoundResumeState({ activePageIndex, childStates });
1361
+ },
1362
+ resume: (state) => {
1363
+ bridgeRef?.current?.notifyImperativeResume(state);
1364
+ }
1365
+ }),
1366
+ [activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
1367
+ );
1356
1368
  }
1357
1369
 
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);
1370
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1371
+ function useAssessmentHandleRegistration(checkId, handle, ref) {
1372
+ useImperativeHandle2(ref, () => handle, [handle]);
1373
+ useRegisterAssessmentHandle(checkId, handle);
1374
+ }
1375
+
1376
+ // src/assessment/internal/usePluginScoring.ts
1377
+ import { useCallback as useCallback3 } from "react";
1378
+
1379
+ // src/assessment/scoring.ts
1380
+ function resolvePassingThreshold(passingScore, maxScore) {
1381
+ return passingScore ?? maxScore;
1382
+ }
1383
+ function meetsPassingThreshold(score, maxScore, passingScore) {
1384
+ const threshold = resolvePassingThreshold(passingScore, maxScore);
1385
+ return score >= threshold;
1386
+ }
1387
+ function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
1388
+ const maxScore = custom?.maxScore ?? fallbackMax;
1389
+ if (custom?.passed !== void 0) {
1390
+ const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
1391
+ return { score: score2, maxScore, passed: custom.passed };
1392
+ }
1393
+ if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1394
+ const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1395
+ return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
1396
+ }
1397
+ const score = fallbackCorrect ? maxScore : 0;
1398
+ const passed = meetsPassingThreshold(score, maxScore, passingScore);
1399
+ return { score, maxScore, passed };
1400
+ }
1401
+
1402
+ // src/assessment/internal/usePluginScoring.ts
1403
+ function usePluginScoring(checkId, lessonId) {
1365
1404
  const { plugins, config, session } = useLessonkit();
1405
+ const getPluginScore = useCallback3(
1406
+ (response) => {
1407
+ const pluginCtx = buildPluginContext({
1408
+ courseId: config.courseId,
1409
+ sessionId: session.sessionId,
1410
+ attemptId: session.attemptId,
1411
+ user: session.user
1412
+ });
1413
+ return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
1414
+ },
1415
+ [checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
1416
+ );
1417
+ const scoreResponse = useCallback3(
1418
+ (response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
1419
+ [getPluginScore]
1420
+ );
1421
+ const isChoiceCorrect = useCallback3(
1422
+ (choice, answer, custom, passingScore) => {
1423
+ if (!custom) return choice === answer;
1424
+ if (custom.passed !== void 0) return custom.passed;
1425
+ if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1426
+ return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1427
+ }
1428
+ return choice === answer;
1429
+ },
1430
+ []
1431
+ );
1432
+ return { getPluginScore, scoreResponse, isChoiceCorrect };
1433
+ }
1434
+
1435
+ // src/components/Quiz.tsx
1436
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
1437
+ function QuizInner(props, ref) {
1438
+ const { enclosingLessonId } = props;
1439
+ const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1440
+ const quiz = useQuizState(enclosingLessonId);
1441
+ const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
1366
1442
  const [selected, setSelected] = useState3(null);
1367
1443
  const [selectionCorrect, setSelectionCorrect] = useState3(null);
1368
- const [showSolutions, setShowSolutions] = useState3(false);
1369
- const [passed, setPassed] = useState3(false);
1444
+ const [quizPassed, setQuizPassed] = useState3(false);
1445
+ const [completedScore, setCompletedScore] = useState3(null);
1446
+ const [completedMaxScore, setCompletedMaxScore] = useState3(null);
1370
1447
  const completedRef = useRef4(false);
1371
- const questionId = React5.useId();
1372
- const reset = () => {
1448
+ const telemetryReplayedRef = useRef4(false);
1449
+ const questionId = useId();
1450
+ const choicesKey = props.choices.join("\0");
1451
+ useEffect3(() => {
1373
1452
  completedRef.current = false;
1374
- setPassed(false);
1453
+ telemetryReplayedRef.current = false;
1454
+ setQuizPassed(false);
1375
1455
  setSelected(null);
1376
1456
  setSelectionCorrect(null);
1377
- setShowSolutions(false);
1457
+ setCompletedScore(null);
1458
+ setCompletedMaxScore(null);
1459
+ }, [checkId, props.answer, props.question, choicesKey]);
1460
+ const passed = quizPassed;
1461
+ const resolveScores = () => {
1462
+ const maxScore = completedMaxScore ?? 1;
1463
+ if (quizPassed) {
1464
+ return { score: completedScore ?? maxScore, maxScore };
1465
+ }
1466
+ if (selected !== null && selectionCorrect) {
1467
+ return { score: completedMaxScore ?? maxScore, maxScore };
1468
+ }
1469
+ return { score: 0, maxScore };
1378
1470
  };
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,
1388
- getAnswerGiven: () => selected !== null,
1389
- resetTask: reset,
1390
- showSolutions: () => setShowSolutions(true),
1391
- getXAPIData: () => ({
1392
- checkId,
1393
- interactionType: INTERACTION,
1394
- 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({
1471
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1472
+ if (!nextPassed || telemetryReplayedRef.current) return;
1473
+ telemetryReplayedRef.current = true;
1474
+ if (nextSelected !== null) {
1475
+ quiz.answer({
1430
1476
  checkId,
1431
- interactionType: INTERACTION,
1432
- score: scored.score,
1433
- maxScore: scored.maxScore,
1434
- passingScore: props.passingScore ?? scored.maxScore
1477
+ question: props.question,
1478
+ choice: nextSelected,
1479
+ correct: nextCorrect ?? false
1435
1480
  });
1436
1481
  }
1482
+ quiz.complete({
1483
+ checkId,
1484
+ score: nextScore,
1485
+ maxScore: nextMaxScore,
1486
+ passingScore: props.passingScore ?? nextMaxScore
1487
+ });
1437
1488
  };
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)
1489
+ const handle = useMemo5(
1490
+ () => buildAssessmentHandle({
1491
+ checkId,
1492
+ getScore: () => resolveScores().score,
1493
+ getMaxScore: () => resolveScores().maxScore,
1494
+ getAnswerGiven: () => selected !== null,
1495
+ resetTask: () => {
1496
+ completedRef.current = false;
1497
+ telemetryReplayedRef.current = false;
1498
+ setQuizPassed(false);
1499
+ setSelected(null);
1500
+ setSelectionCorrect(null);
1501
+ setCompletedScore(null);
1502
+ setCompletedMaxScore(null);
1503
+ },
1504
+ showSolutions: () => {
1505
+ },
1506
+ getXAPIData: () => {
1507
+ const { score, maxScore } = resolveScores();
1508
+ return {
1509
+ checkId,
1510
+ interactionType: "mcq",
1511
+ response: selected ?? void 0,
1512
+ correct: selectionCorrect ?? void 0,
1513
+ score,
1514
+ maxScore
1515
+ };
1516
+ },
1517
+ getCurrentState: () => ({
1518
+ selected,
1519
+ selectionCorrect,
1520
+ quizPassed,
1521
+ completedScore,
1522
+ completedMaxScore
1523
+ }),
1524
+ resume: (state) => {
1525
+ const nextSelected = readStringField(state, "selected");
1526
+ if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
1527
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1528
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1529
+ setSelectionCorrect(nextCorrect);
1530
+ }
1531
+ const nextCompletedScore = readNumberField(state, "completedScore");
1532
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1533
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1534
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1535
+ const nextPassed = readBooleanField(state, "quizPassed");
1536
+ if (nextPassed === true || nextPassed === false) {
1537
+ setQuizPassed(nextPassed);
1538
+ completedRef.current = nextPassed;
1539
+ if (nextPassed) {
1540
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1541
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
1542
+ replayTelemetry(
1543
+ nextSelected ?? null,
1544
+ nextCorrect ?? null,
1545
+ nextPassed,
1546
+ score,
1547
+ maxScore
1548
+ );
1452
1549
  }
1453
- ),
1454
- "True"
1455
- ] }),
1456
- /* @__PURE__ */ jsxs3("label", { style: { display: "block" }, children: [
1457
- /* @__PURE__ */ jsx5(
1550
+ }
1551
+ }
1552
+ }),
1553
+ [
1554
+ checkId,
1555
+ completedMaxScore,
1556
+ completedScore,
1557
+ props.passingScore,
1558
+ props.question,
1559
+ quiz,
1560
+ quizPassed,
1561
+ selected,
1562
+ selectionCorrect
1563
+ ]
1564
+ );
1565
+ useAssessmentHandleRegistration(checkId, handle, ref);
1566
+ return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1567
+ /* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
1568
+ /* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
1569
+ /* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
1570
+ props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
1571
+ /* @__PURE__ */ jsx6(
1458
1572
  "input",
1459
1573
  {
1460
1574
  type: "radio",
1461
- name: `${questionId}-tf`,
1462
- checked: selected === false,
1463
- disabled: passed && !props.enableRetry,
1464
- onChange: () => submit(false)
1575
+ name: questionId,
1576
+ value: c,
1577
+ checked: selected === c,
1578
+ disabled: passed,
1579
+ "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1580
+ onChange: () => {
1581
+ if (passed) return;
1582
+ setSelected(c);
1583
+ const custom = getPluginScore(c);
1584
+ const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
1585
+ setSelectionCorrect(correct);
1586
+ quiz.answer({
1587
+ checkId,
1588
+ question: props.question,
1589
+ choice: c,
1590
+ correct
1591
+ });
1592
+ if (correct && !completedRef.current) {
1593
+ completedRef.current = true;
1594
+ setQuizPassed(true);
1595
+ const maxScore = custom?.maxScore ?? 1;
1596
+ const score = custom?.score ?? maxScore;
1597
+ setCompletedScore(score);
1598
+ setCompletedMaxScore(maxScore);
1599
+ quiz.complete({
1600
+ checkId,
1601
+ score,
1602
+ maxScore,
1603
+ passingScore: props.passingScore ?? maxScore
1604
+ });
1605
+ }
1606
+ }
1465
1607
  }
1466
1608
  ),
1467
- "False"
1468
- ] })
1609
+ c
1610
+ ] }, `${questionId}-${i}`))
1469
1611
  ] }),
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
1612
+ selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1477
1613
  ] });
1478
1614
  }
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 }) });
1615
+ var QuizInnerForwarded = forwardRef(QuizInner);
1616
+ var Quiz = forwardRef(function Quiz2(props, ref) {
1617
+ return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1482
1618
  });
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) {
1619
+ function KnowledgeCheck(props) {
1620
+ return /* @__PURE__ */ jsx6(
1621
+ Quiz,
1622
+ {
1623
+ checkId: props.checkId,
1624
+ question: props.question,
1625
+ choices: props.choices,
1626
+ answer: props.answer,
1627
+ passingScore: props.passingScore
1628
+ }
1629
+ );
1630
+ }
1631
+ function resetQuizWarningsForTests() {
1632
+ resetAssessmentWarningsForTests();
1633
+ }
1634
+
1635
+ // src/components.tsx
1636
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
1637
+ function Course(props) {
1638
+ const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1639
+ const providerConfig = useMemo6(
1640
+ () => ({ ...props.config, courseId }),
1641
+ [props.config, courseId]
1642
+ );
1643
+ return /* @__PURE__ */ jsx7(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
1644
+ /* @__PURE__ */ jsx7("h1", { children: props.title }),
1645
+ /* @__PURE__ */ jsx7("div", { children: props.children })
1646
+ ] }) });
1647
+ }
1648
+ function Lesson(props) {
1649
+ const lessonId = useMemo6(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1650
+ const autoComplete = props.autoCompleteOnUnmount !== false;
1651
+ const { setActiveLesson, config } = useLessonkit();
1652
+ const { completeLesson } = useCompletion();
1653
+ const lessonMountGenerationRef = useRef5(0);
1654
+ const liveCourseIdRef = useRef5(config.courseId);
1655
+ liveCourseIdRef.current = config.courseId;
1656
+ useEffect4(() => {
1657
+ const unregister = registerLessonMount(lessonId);
1658
+ const generation = ++lessonMountGenerationRef.current;
1659
+ const mountedCourseId = config.courseId;
1660
+ let effectSurvivedTick = false;
1661
+ queueMicrotask(() => {
1662
+ queueMicrotask(() => {
1663
+ effectSurvivedTick = true;
1664
+ });
1665
+ });
1666
+ setActiveLesson(lessonId);
1667
+ return () => {
1668
+ unregister();
1669
+ if (getLessonMountCount(lessonId) > 0) {
1670
+ return;
1671
+ }
1672
+ if (!autoComplete) return;
1673
+ queueMicrotask(() => {
1674
+ if (!effectSurvivedTick) return;
1675
+ if (lessonMountGenerationRef.current !== generation) return;
1676
+ if (liveCourseIdRef.current !== mountedCourseId) return;
1677
+ completeLesson(lessonId, { courseId: mountedCourseId });
1678
+ });
1679
+ };
1680
+ }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1681
+ return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
1682
+ /* @__PURE__ */ jsx7("h2", { children: props.title }),
1683
+ /* @__PURE__ */ jsx7("div", { children: props.children })
1684
+ ] }) });
1685
+ }
1686
+ function Scenario(props) {
1687
+ const blockId = useMemo6(
1688
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1689
+ [props.blockId]
1690
+ );
1691
+ return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1692
+ }
1693
+ function Reflection(props) {
1694
+ const blockId = useMemo6(
1695
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1696
+ [props.blockId]
1697
+ );
1698
+ const promptId = useId2();
1699
+ const hintId = useId2();
1700
+ const [internalValue, setInternalValue] = useState4("");
1701
+ const isControlled = props.value !== void 0;
1702
+ const value = isControlled ? props.value : internalValue;
1703
+ const handleChange = (event) => {
1704
+ if (!isControlled) setInternalValue(event.target.value);
1705
+ props.onChange?.(event.target.value);
1706
+ };
1707
+ return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1708
+ props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
1709
+ props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
1710
+ props.children,
1711
+ /* @__PURE__ */ jsx7(
1712
+ "textarea",
1713
+ {
1714
+ value,
1715
+ onChange: handleChange,
1716
+ "aria-labelledby": props.prompt ? promptId : void 0,
1717
+ "aria-describedby": props.hint ? hintId : void 0,
1718
+ "aria-label": props.prompt ? void 0 : "Reflection response"
1719
+ }
1720
+ )
1721
+ ] });
1722
+ }
1723
+ function ProgressTracker(props) {
1724
+ const { progress } = useLessonkit();
1725
+ const completed = progress.completedLessonIds.size;
1726
+ if (props.totalLessons != null) {
1727
+ const total = props.totalLessons;
1728
+ const displayed = Math.min(completed, total);
1729
+ return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
1730
+ "div",
1731
+ {
1732
+ role: "progressbar",
1733
+ "aria-valuemin": 0,
1734
+ "aria-valuemax": total,
1735
+ "aria-valuenow": displayed,
1736
+ "aria-label": "Lessons completed",
1737
+ children: /* @__PURE__ */ jsxs3("p", { children: [
1738
+ "Lessons completed: ",
1739
+ displayed,
1740
+ " of ",
1741
+ total
1742
+ ] })
1743
+ }
1744
+ ) });
1745
+ }
1746
+ return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
1747
+ "Lessons completed: ",
1748
+ completed
1749
+ ] }) });
1750
+ }
1751
+
1752
+ // src/blocks/TrueFalse.tsx
1753
+ import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
1754
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1755
+ var INTERACTION = "trueFalse";
1756
+ function TrueFalseInner(props, ref) {
1757
+ const { enclosingLessonId } = props;
1758
+ const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1759
+ const assessment = useAssessmentState(enclosingLessonId);
1760
+ const { config } = useLessonkit();
1761
+ const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
1762
+ const [selected, setSelected] = useState5(null);
1763
+ const [selectionCorrect, setSelectionCorrect] = useState5(null);
1764
+ const [showSolutions, setShowSolutions] = useState5(false);
1765
+ const [passed, setPassed] = useState5(false);
1766
+ const [completedScore, setCompletedScore] = useState5(null);
1767
+ const [completedMaxScore, setCompletedMaxScore] = useState5(null);
1768
+ const completedRef = useRef6(false);
1769
+ const telemetryReplayedRef = useRef6(false);
1770
+ const questionId = React9.useId();
1771
+ const reset = () => {
1772
+ completedRef.current = false;
1773
+ telemetryReplayedRef.current = false;
1774
+ setPassed(false);
1775
+ setSelected(null);
1776
+ setSelectionCorrect(null);
1777
+ setShowSolutions(false);
1778
+ setCompletedScore(null);
1779
+ setCompletedMaxScore(null);
1780
+ };
1781
+ useEffect5(() => {
1782
+ reset();
1783
+ }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1784
+ const resolveScores = () => {
1785
+ const maxScore = completedMaxScore ?? 1;
1786
+ if (passed) {
1787
+ return { score: completedScore ?? maxScore, maxScore };
1788
+ }
1789
+ if (selectionCorrect) {
1790
+ return { score: completedMaxScore ?? maxScore, maxScore };
1791
+ }
1792
+ return { score: 0, maxScore };
1793
+ };
1794
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1795
+ if (!nextPassed || telemetryReplayedRef.current) return;
1796
+ telemetryReplayedRef.current = true;
1797
+ if (nextSelected !== null) {
1798
+ assessment.answer({
1799
+ checkId,
1800
+ interactionType: INTERACTION,
1801
+ question: props.question,
1802
+ response: nextSelected,
1803
+ correct: nextCorrect ?? false
1804
+ });
1805
+ }
1806
+ assessment.complete({
1807
+ checkId,
1808
+ interactionType: INTERACTION,
1809
+ score: nextScore,
1810
+ maxScore: nextMaxScore,
1811
+ passingScore: props.passingScore ?? nextMaxScore
1812
+ });
1813
+ };
1814
+ const handle = useMemo7(
1815
+ () => buildAssessmentHandle({
1816
+ checkId,
1817
+ getScore: () => resolveScores().score,
1818
+ getMaxScore: () => resolveScores().maxScore,
1819
+ getAnswerGiven: () => selected !== null,
1820
+ resetTask: reset,
1821
+ showSolutions: () => setShowSolutions(true),
1822
+ getXAPIData: () => {
1823
+ const { score, maxScore } = resolveScores();
1824
+ return {
1825
+ checkId,
1826
+ interactionType: INTERACTION,
1827
+ response: selected ?? void 0,
1828
+ correct: selectionCorrect ?? void 0,
1829
+ score,
1830
+ maxScore
1831
+ };
1832
+ },
1833
+ getCurrentState: () => ({
1834
+ selected,
1835
+ selectionCorrect,
1836
+ passed,
1837
+ showSolutions,
1838
+ completedScore,
1839
+ completedMaxScore
1840
+ }),
1841
+ resume: (state) => {
1842
+ const nextSelected = readBooleanField(state, "selected");
1843
+ if (nextSelected === true || nextSelected === false || nextSelected === null) {
1844
+ setSelected(nextSelected);
1845
+ }
1846
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1847
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1848
+ setSelectionCorrect(nextCorrect);
1849
+ }
1850
+ const nextCompletedScore = readNumberField(state, "completedScore");
1851
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1852
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1853
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1854
+ const nextPassed = readBooleanField(state, "passed");
1855
+ if (nextPassed === true || nextPassed === false) {
1856
+ setPassed(nextPassed);
1857
+ completedRef.current = nextPassed;
1858
+ if (nextPassed) {
1859
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1860
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
1861
+ replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
1862
+ }
1863
+ }
1864
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
1865
+ }
1866
+ }),
1867
+ [
1868
+ assessment,
1869
+ checkId,
1870
+ completedMaxScore,
1871
+ completedScore,
1872
+ passed,
1873
+ props.passingScore,
1874
+ props.question,
1875
+ selected,
1876
+ selectionCorrect,
1877
+ showSolutions
1878
+ ]
1879
+ );
1880
+ useAssessmentHandleRegistration(checkId, handle, ref);
1881
+ const submit = (value) => {
1882
+ if (passed && !props.enableRetry) return;
1883
+ setSelected(value);
1884
+ const correct = value === props.answer;
1885
+ const scored = scoreResponse(value, correct, 1, props.passingScore);
1886
+ setSelectionCorrect(scored.passed);
1887
+ assessment.answer({
1888
+ checkId,
1889
+ interactionType: INTERACTION,
1890
+ question: props.question,
1891
+ response: value,
1892
+ correct: scored.passed
1893
+ });
1894
+ if (scored.passed && !completedRef.current) {
1895
+ completedRef.current = true;
1896
+ setPassed(true);
1897
+ setCompletedScore(scored.score);
1898
+ setCompletedMaxScore(scored.maxScore);
1899
+ assessment.complete({
1900
+ checkId,
1901
+ interactionType: INTERACTION,
1902
+ score: scored.score,
1903
+ maxScore: scored.maxScore,
1904
+ passingScore: props.passingScore ?? scored.maxScore
1905
+ });
1906
+ }
1907
+ };
1908
+ const reveal = showSolutions || passed && props.enableSolutionsButton;
1909
+ return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1910
+ /* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
1911
+ /* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
1912
+ /* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
1913
+ /* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
1914
+ /* @__PURE__ */ jsx8(
1915
+ "input",
1916
+ {
1917
+ type: "radio",
1918
+ name: `${questionId}-tf`,
1919
+ checked: selected === true,
1920
+ disabled: passed && !props.enableRetry,
1921
+ onChange: () => submit(true)
1922
+ }
1923
+ ),
1924
+ "True"
1925
+ ] }),
1926
+ /* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
1927
+ /* @__PURE__ */ jsx8(
1928
+ "input",
1929
+ {
1930
+ type: "radio",
1931
+ name: `${questionId}-tf`,
1932
+ checked: selected === false,
1933
+ disabled: passed && !props.enableRetry,
1934
+ onChange: () => submit(false)
1935
+ }
1936
+ ),
1937
+ "False"
1938
+ ] })
1939
+ ] }),
1940
+ reveal ? /* @__PURE__ */ jsxs4("p", { children: [
1941
+ "Correct answer: ",
1942
+ /* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
1943
+ ] }) : null,
1944
+ selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1945
+ props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1946
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1947
+ ] });
1948
+ }
1949
+ var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
1950
+ var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
1951
+ return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1952
+ });
1953
+
1954
+ // src/blocks/MarkTheWords.tsx
1955
+ import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
1956
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1957
+ var INTERACTION2 = "markTheWords";
1958
+ function tokenize(text) {
1489
1959
  return text.split(/(\s+)/).filter((t) => t.length > 0);
1490
1960
  }
1491
1961
  function MarkTheWordsInner(props, ref) {
1492
- const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1962
+ const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1493
1963
  const assessment = useAssessmentState(props.enclosingLessonId);
1494
- const tokens = useMemo7(() => tokenize(props.text), [props.text]);
1495
- const correctSet = useMemo7(
1964
+ const tokens = useMemo8(() => tokenize(props.text), [props.text]);
1965
+ const correctSet = useMemo8(
1496
1966
  () => new Set(props.correctWords.map((w) => w.toLowerCase())),
1497
1967
  [props.correctWords]
1498
1968
  );
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);
1969
+ const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
1970
+ const [passed, setPassed] = useState6(false);
1971
+ const [showSolutions, setShowSolutions] = useState6(false);
1972
+ const completedRef = useRef7(false);
1503
1973
  const reset = () => {
1504
1974
  completedRef.current = false;
1505
1975
  setPassed(false);
1506
1976
  setMarked(/* @__PURE__ */ new Set());
1507
1977
  setShowSolutions(false);
1508
1978
  };
1509
- useEffect5(() => {
1979
+ useEffect6(() => {
1510
1980
  reset();
1511
1981
  }, [checkId, props.text, props.correctWords.join("\0")]);
1512
- const selectableIndices = useMemo7(() => {
1982
+ const selectableIndices = useMemo8(() => {
1513
1983
  const indices = [];
1514
1984
  tokens.forEach((t, i) => {
1515
1985
  if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
@@ -1521,11 +1991,11 @@ function MarkTheWordsInner(props, ref) {
1521
1991
  const maxScore = selectableIndices.length;
1522
1992
  const score = allMarked ? maxScore : marked.size;
1523
1993
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1524
- const handle = useMemo7(() => {
1525
- const handleMax = maxScore || 1;
1526
- return {
1994
+ const handle = useMemo8(
1995
+ () => buildAssessmentHandle({
1996
+ checkId,
1527
1997
  getScore: () => score,
1528
- getMaxScore: () => handleMax,
1998
+ getMaxScore: () => maxScore || 1,
1529
1999
  getAnswerGiven: () => marked.size > 0,
1530
2000
  resetTask: reset,
1531
2001
  showSolutions: () => setShowSolutions(true),
@@ -1535,12 +2005,22 @@ function MarkTheWordsInner(props, ref) {
1535
2005
  response: [...marked].map((i) => tokens[i]),
1536
2006
  correct: passedThreshold,
1537
2007
  score,
1538
- maxScore: handleMax
1539
- })
1540
- };
1541
- }, [checkId, marked, maxScore, passedThreshold, score, tokens]);
1542
- useImperativeHandle2(ref, () => handle, [handle]);
1543
- useRegisterAssessmentHandle(checkId, handle);
2008
+ maxScore: maxScore || 1
2009
+ }),
2010
+ getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
2011
+ resume: (state) => {
2012
+ const raw = state.marked;
2013
+ if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
2014
+ readBooleanStateField(state, "passed", (value) => {
2015
+ setPassed(value);
2016
+ completedRef.current = value;
2017
+ });
2018
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
2019
+ }
2020
+ }),
2021
+ [checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
2022
+ );
2023
+ useAssessmentHandleRegistration(checkId, handle, ref);
1544
2024
  const toggle = (index) => {
1545
2025
  if (passed && !props.enableRetry) return;
1546
2026
  setMarked((prev) => {
@@ -1550,7 +2030,7 @@ function MarkTheWordsInner(props, ref) {
1550
2030
  return next;
1551
2031
  });
1552
2032
  };
1553
- useEffect5(() => {
2033
+ useEffect6(() => {
1554
2034
  if (!hasTargets) {
1555
2035
  if (isDevEnvironment4()) {
1556
2036
  console.warn(
@@ -1568,7 +2048,7 @@ function MarkTheWordsInner(props, ref) {
1568
2048
  interactionType: INTERACTION2,
1569
2049
  question: props.text,
1570
2050
  response: [...marked].map((i) => tokens[i]),
1571
- correct: true
2051
+ correct: passedThreshold
1572
2052
  });
1573
2053
  assessment.complete({
1574
2054
  checkId,
@@ -1590,20 +2070,20 @@ function MarkTheWordsInner(props, ref) {
1590
2070
  score,
1591
2071
  tokens
1592
2072
  ]);
1593
- return /* @__PURE__ */ jsxs4("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1594
- !hasTargets ? /* @__PURE__ */ jsxs4("p", { role: "alert", children: [
2073
+ return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
2074
+ !hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
1595
2075
  "No words in this sentence match ",
1596
- /* @__PURE__ */ jsx6("code", { children: "correctWords" }),
2076
+ /* @__PURE__ */ jsx9("code", { children: "correctWords" }),
1597
2077
  ". Check spelling and capitalization in the source text."
1598
2078
  ] }) : 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) => {
2079
+ /* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
2080
+ /* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1601
2081
  const isWord = !/^\s+$/.test(token);
1602
2082
  const isTarget = isWord && correctSet.has(token.toLowerCase());
1603
- if (!isTarget) return /* @__PURE__ */ jsx6(React6.Fragment, { children: token }, i);
2083
+ if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
1604
2084
  const selected = marked.has(i);
1605
2085
  const solution = showSolutions || passed && props.enableSolutionsButton;
1606
- return /* @__PURE__ */ jsx6(
2086
+ return /* @__PURE__ */ jsx9(
1607
2087
  "button",
1608
2088
  {
1609
2089
  type: "button",
@@ -1621,57 +2101,69 @@ function MarkTheWordsInner(props, ref) {
1621
2101
  i
1622
2102
  );
1623
2103
  }) }),
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
2104
+ allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
2105
+ props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2106
+ props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1627
2107
  ] });
1628
2108
  }
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 }) });
2109
+ var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
2110
+ var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
2111
+ return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1632
2112
  });
1633
2113
 
1634
2114
  // 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) {
2115
+ import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
2116
+
2117
+ // src/assessment/internal/parseStarDelimitedTemplate.ts
2118
+ function parseStarDelimitedTemplate(template, idPrefix) {
1639
2119
  const parts = [];
1640
- const blanks = [];
2120
+ const values = [];
1641
2121
  const re = /\*([^*]+)\*/g;
1642
2122
  let last = 0;
1643
2123
  let match;
1644
2124
  let n = 0;
1645
2125
  while ((match = re.exec(template)) !== null) {
1646
2126
  parts.push(template.slice(last, match.index));
1647
- const id = `blank-${n++}`;
1648
- blanks.push({ id, answer: match[1].trim() });
1649
- parts.push(id);
2127
+ values.push(match[1].trim());
2128
+ parts.push(`${idPrefix}-${n++}`);
1650
2129
  last = match.index + match[0].length;
1651
2130
  }
1652
2131
  parts.push(template.slice(last));
1653
- return { parts, blanks };
2132
+ return { parts, values };
2133
+ }
2134
+
2135
+ // src/blocks/FillInTheBlanks.tsx
2136
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2137
+ var INTERACTION3 = "fillInBlanks";
2138
+ function parseTemplate(template) {
2139
+ const { parts, values } = parseStarDelimitedTemplate(template, "blank");
2140
+ return {
2141
+ parts,
2142
+ blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
2143
+ };
1654
2144
  }
1655
2145
  function FillInTheBlanksInner(props, ref) {
1656
- const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2146
+ const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1657
2147
  const assessment = useAssessmentState(props.enclosingLessonId);
1658
- const parsed = useMemo8(() => parseTemplate(props.template), [props.template]);
2148
+ const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
1659
2149
  const blanks = props.blanks ?? parsed.blanks;
1660
- const [values, setValues] = useState5(
2150
+ const [values, setValues] = useState7(
1661
2151
  () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
1662
2152
  );
1663
- const [passed, setPassed] = useState5(false);
1664
- const [showSolutions, setShowSolutions] = useState5(false);
1665
- const completedRef = useRef6(false);
1666
- const answeredRef = useRef6(false);
2153
+ const [passed, setPassed] = useState7(false);
2154
+ const [showSolutions, setShowSolutions] = useState7(false);
2155
+ const [submitted, setSubmitted] = useState7(false);
2156
+ const completedRef = useRef8(false);
2157
+ const answeredRef = useRef8(false);
1667
2158
  const reset = () => {
1668
2159
  completedRef.current = false;
1669
2160
  answeredRef.current = false;
1670
2161
  setPassed(false);
1671
2162
  setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
1672
2163
  setShowSolutions(false);
2164
+ setSubmitted(false);
1673
2165
  };
1674
- useEffect6(() => {
2166
+ useEffect7(() => {
1675
2167
  reset();
1676
2168
  }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
1677
2169
  const hasBlanks = blanks.length > 0;
@@ -1682,11 +2174,11 @@ function FillInTheBlanksInner(props, ref) {
1682
2174
  });
1683
2175
  const maxScore = blanks.length;
1684
2176
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1685
- const handle = useMemo8(() => {
1686
- const handleMax = maxScore || 1;
1687
- return {
2177
+ const handle = useMemo9(
2178
+ () => buildAssessmentHandle({
2179
+ checkId,
1688
2180
  getScore: () => score,
1689
- getMaxScore: () => handleMax,
2181
+ getMaxScore: () => maxScore || 1,
1690
2182
  getAnswerGiven: () => allFilled,
1691
2183
  resetTask: reset,
1692
2184
  showSolutions: () => setShowSolutions(true),
@@ -1696,30 +2188,45 @@ function FillInTheBlanksInner(props, ref) {
1696
2188
  response: values,
1697
2189
  correct: passedThreshold,
1698
2190
  score,
1699
- maxScore: handleMax
1700
- })
1701
- };
1702
- }, [allFilled, blanks.length, checkId, maxScore, passedThreshold, score, values]);
1703
- useImperativeHandle3(ref, () => handle, [handle]);
1704
- useRegisterAssessmentHandle(checkId, handle);
1705
- const check = () => {
1706
- if (!hasBlanks) {
1707
- if (isDevEnvironment4()) {
1708
- console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
1709
- }
2191
+ maxScore: maxScore || 1
2192
+ }),
2193
+ getCurrentState: () => ({ values, passed, showSolutions, submitted }),
2194
+ resume: (state) => {
2195
+ const raw = state.values;
2196
+ if (raw && typeof raw === "object") setValues({ ...raw });
2197
+ readBooleanStateField(state, "passed", (value) => {
2198
+ setPassed(value);
2199
+ completedRef.current = value;
2200
+ answeredRef.current = value;
2201
+ });
2202
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
2203
+ readBooleanStateField(state, "submitted", (value) => {
2204
+ setSubmitted(value);
2205
+ if (value) answeredRef.current = true;
2206
+ });
2207
+ }
2208
+ }),
2209
+ [allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, submitted, values]
2210
+ );
2211
+ useAssessmentHandleRegistration(checkId, handle, ref);
2212
+ const check = () => {
2213
+ if (!hasBlanks) {
2214
+ if (isDevEnvironment4()) {
2215
+ console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
2216
+ }
1710
2217
  return;
1711
2218
  }
1712
2219
  if (!allFilled) return;
1713
- if (!answeredRef.current) {
1714
- answeredRef.current = true;
1715
- assessment.answer({
1716
- checkId,
1717
- interactionType: INTERACTION3,
1718
- question: props.template,
1719
- response: values,
1720
- correct: passedThreshold
1721
- });
1722
- }
2220
+ if (answeredRef.current || submitted) return;
2221
+ answeredRef.current = true;
2222
+ setSubmitted(true);
2223
+ assessment.answer({
2224
+ checkId,
2225
+ interactionType: INTERACTION3,
2226
+ question: props.template,
2227
+ response: values,
2228
+ correct: passedThreshold
2229
+ });
1723
2230
  if (passedThreshold && !completedRef.current) {
1724
2231
  completedRef.current = true;
1725
2232
  setPassed(true);
@@ -1732,20 +2239,23 @@ function FillInTheBlanksInner(props, ref) {
1732
2239
  });
1733
2240
  }
1734
2241
  };
1735
- useEffect6(() => {
1736
- if (!allFilled) answeredRef.current = false;
2242
+ useEffect7(() => {
2243
+ if (!allFilled) {
2244
+ answeredRef.current = false;
2245
+ setSubmitted(false);
2246
+ }
1737
2247
  }, [allFilled]);
1738
- useEffect6(() => {
2248
+ useEffect7(() => {
1739
2249
  if (props.autoCheck && allFilled) check();
1740
2250
  }, [allFilled, props.autoCheck, values, passedThreshold]);
1741
2251
  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) => {
2252
+ return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2253
+ /* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
1744
2254
  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(
2255
+ if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
2256
+ return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
2257
+ /* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
2258
+ /* @__PURE__ */ jsx10(
1749
2259
  "input",
1750
2260
  {
1751
2261
  type: "text",
@@ -1761,61 +2271,51 @@ function FillInTheBlanksInner(props, ref) {
1761
2271
  )
1762
2272
  ] }, blank.id);
1763
2273
  }) }),
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
2274
+ !props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2275
+ !hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2276
+ submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2277
+ props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2278
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1769
2279
  ] });
1770
2280
  }
1771
- var FillInTheBlanksInnerForwarded = forwardRef3(FillInTheBlanksInner);
1772
- var FillInTheBlanks = forwardRef3(
2281
+ var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
2282
+ var FillInTheBlanks = forwardRef4(
1773
2283
  function FillInTheBlanks2(props, ref) {
1774
- return /* @__PURE__ */ jsx7(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx7(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2284
+ return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1775
2285
  }
1776
2286
  );
1777
2287
 
1778
2288
  // 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";
2289
+ import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
2290
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1781
2291
  var INTERACTION4 = "dragTheWords";
1782
2292
  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 };
2293
+ const { parts, values } = parseStarDelimitedTemplate(template, "zone");
2294
+ return { parts, answers: values };
1797
2295
  }
1798
2296
  function DragTheWordsInner(props, ref) {
1799
- const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2297
+ const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1800
2298
  const assessment = useAssessmentState(props.enclosingLessonId);
1801
- const { parts, answers } = useMemo9(() => parseZones(props.template), [props.template]);
1802
- const [zones, setZones] = useState6(
2299
+ const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
2300
+ const [zones, setZones] = useState8(
1803
2301
  () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
1804
2302
  );
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);
2303
+ const [pool, setPool] = useState8(() => [...props.words]);
2304
+ const [keyboardWord, setKeyboardWord] = useState8(null);
2305
+ const [passed, setPassed] = useState8(false);
2306
+ const [submitted, setSubmitted] = useState8(false);
2307
+ const completedRef = useRef9(false);
2308
+ const answeredRef = useRef9(false);
1810
2309
  const reset = () => {
1811
2310
  completedRef.current = false;
1812
2311
  answeredRef.current = false;
1813
2312
  setPassed(false);
2313
+ setSubmitted(false);
1814
2314
  setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
1815
2315
  setPool([...props.words]);
1816
2316
  setKeyboardWord(null);
1817
2317
  };
1818
- useEffect7(() => {
2318
+ useEffect8(() => {
1819
2319
  reset();
1820
2320
  }, [checkId, props.template, props.words.join("\0")]);
1821
2321
  const hasZones = answers.length > 0;
@@ -1826,11 +2326,11 @@ function DragTheWordsInner(props, ref) {
1826
2326
  });
1827
2327
  const maxScore = answers.length;
1828
2328
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1829
- const handle = useMemo9(() => {
1830
- const handleMax = maxScore || 1;
1831
- return {
2329
+ const handle = useMemo10(
2330
+ () => buildAssessmentHandle({
2331
+ checkId,
1832
2332
  getScore: () => score,
1833
- getMaxScore: () => handleMax,
2333
+ getMaxScore: () => maxScore || 1,
1834
2334
  getAnswerGiven: () => allFilled,
1835
2335
  resetTask: reset,
1836
2336
  showSolutions: () => {
@@ -1841,12 +2341,29 @@ function DragTheWordsInner(props, ref) {
1841
2341
  response: zones,
1842
2342
  correct: passedThreshold,
1843
2343
  score,
1844
- maxScore: handleMax
1845
- })
1846
- };
1847
- }, [allFilled, answers.length, checkId, maxScore, passedThreshold, score, zones]);
1848
- useImperativeHandle4(ref, () => handle, [handle]);
1849
- useRegisterAssessmentHandle(checkId, handle);
2344
+ maxScore: maxScore || 1
2345
+ }),
2346
+ getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
2347
+ resume: (state) => {
2348
+ const rawZones = state.zones;
2349
+ if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
2350
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2351
+ readBooleanStateField(state, "passed", (value) => {
2352
+ setPassed(value);
2353
+ completedRef.current = value;
2354
+ answeredRef.current = value;
2355
+ });
2356
+ readBooleanStateField(state, "submitted", (value) => {
2357
+ setSubmitted(value);
2358
+ if (value) answeredRef.current = true;
2359
+ });
2360
+ const kw = state.keyboardWord;
2361
+ if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2362
+ }
2363
+ }),
2364
+ [allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, submitted, zones]
2365
+ );
2366
+ useAssessmentHandleRegistration(checkId, handle, ref);
1850
2367
  const placeInZone = (zoneId, word) => {
1851
2368
  if (passed && !props.enableRetry) return;
1852
2369
  const prev = zones[zoneId];
@@ -1874,16 +2391,16 @@ function DragTheWordsInner(props, ref) {
1874
2391
  return;
1875
2392
  }
1876
2393
  if (!allFilled) return;
1877
- if (!answeredRef.current) {
1878
- answeredRef.current = true;
1879
- assessment.answer({
1880
- checkId,
1881
- interactionType: INTERACTION4,
1882
- question: props.template,
1883
- response: zones,
1884
- correct: passedThreshold
1885
- });
1886
- }
2394
+ if (answeredRef.current || submitted) return;
2395
+ answeredRef.current = true;
2396
+ setSubmitted(true);
2397
+ assessment.answer({
2398
+ checkId,
2399
+ interactionType: INTERACTION4,
2400
+ question: props.template,
2401
+ response: zones,
2402
+ correct: passedThreshold
2403
+ });
1887
2404
  if (passedThreshold && !completedRef.current) {
1888
2405
  completedRef.current = true;
1889
2406
  setPassed(true);
@@ -1896,15 +2413,18 @@ function DragTheWordsInner(props, ref) {
1896
2413
  });
1897
2414
  }
1898
2415
  };
1899
- useEffect7(() => {
1900
- if (!allFilled) answeredRef.current = false;
2416
+ useEffect8(() => {
2417
+ if (!allFilled) {
2418
+ answeredRef.current = false;
2419
+ setSubmitted(false);
2420
+ }
1901
2421
  }, [allFilled]);
1902
- useEffect7(() => {
2422
+ useEffect8(() => {
1903
2423
  if (props.autoCheck && allFilled) check();
1904
2424
  }, [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(
2425
+ return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2426
+ /* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2427
+ /* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
1908
2428
  "button",
1909
2429
  {
1910
2430
  type: "button",
@@ -1918,229 +2438,1616 @@ function DragTheWordsInner(props, ref) {
1918
2438
  },
1919
2439
  word
1920
2440
  )) }),
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(
2441
+ /* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
2442
+ if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
2443
+ return /* @__PURE__ */ jsx11(
1924
2444
  "span",
1925
2445
  {
1926
- role: "button",
1927
- tabIndex: 0,
1928
- "data-testid": part,
1929
- onDragOver: (e) => e.preventDefault(),
1930
- onDrop: onDrop(part),
1931
- onClick: () => keyboardWord && placeInZone(part, keyboardWord),
1932
- onKeyDown: (e) => {
1933
- if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
1934
- },
2446
+ role: "button",
2447
+ tabIndex: 0,
2448
+ "data-testid": part,
2449
+ onDragOver: (e) => e.preventDefault(),
2450
+ onDrop: onDrop(part),
2451
+ onClick: () => keyboardWord && placeInZone(part, keyboardWord),
2452
+ onKeyDown: (e) => {
2453
+ if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
2454
+ },
2455
+ style: {
2456
+ display: "inline-block",
2457
+ minWidth: "6em",
2458
+ border: "1px dashed currentColor",
2459
+ padding: "0.2em 0.5em",
2460
+ margin: "0 0.2em"
2461
+ },
2462
+ children: zones[part] || "___"
2463
+ },
2464
+ part
2465
+ );
2466
+ }) }),
2467
+ /* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2468
+ !hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2469
+ submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2470
+ ] });
2471
+ }
2472
+ var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
2473
+ var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
2474
+ return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2475
+ });
2476
+
2477
+ // src/blocks/DragAndDrop.tsx
2478
+ import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
2479
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2480
+ var INTERACTION5 = "dragAndDrop";
2481
+ function DragAndDropInner(props, ref) {
2482
+ const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2483
+ const assessment = useAssessmentState(props.enclosingLessonId);
2484
+ const [assignments, setAssignments] = useState9(
2485
+ () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2486
+ );
2487
+ const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
2488
+ const [keyboardItem, setKeyboardItem] = useState9(null);
2489
+ const [passed, setPassed] = useState9(false);
2490
+ const [checked, setChecked] = useState9(false);
2491
+ const completedRef = useRef10(false);
2492
+ const reset = () => {
2493
+ completedRef.current = false;
2494
+ setPassed(false);
2495
+ setChecked(false);
2496
+ setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2497
+ setPool(props.items.map((i) => i.id));
2498
+ setKeyboardItem(null);
2499
+ };
2500
+ useEffect9(() => {
2501
+ reset();
2502
+ }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2503
+ const hasTargets = props.targets.length > 0;
2504
+ const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2505
+ let score = 0;
2506
+ props.targets.forEach((t) => {
2507
+ if (assignments[t.id] === t.accepts) score += 1;
2508
+ });
2509
+ const maxScore = props.targets.length || 1;
2510
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
2511
+ const handle = useMemo11(() => {
2512
+ return buildAssessmentHandle({
2513
+ checkId,
2514
+ getScore: () => score,
2515
+ getMaxScore: () => maxScore,
2516
+ getAnswerGiven: () => hasTargets && allFilled,
2517
+ resetTask: reset,
2518
+ showSolutions: () => {
2519
+ },
2520
+ getXAPIData: () => ({
2521
+ checkId,
2522
+ interactionType: INTERACTION5,
2523
+ response: assignments,
2524
+ correct: passedThreshold,
2525
+ score,
2526
+ maxScore
2527
+ }),
2528
+ getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
2529
+ resume: (state) => {
2530
+ const rawAssignments = state.assignments;
2531
+ if (rawAssignments && typeof rawAssignments === "object") {
2532
+ setAssignments({ ...rawAssignments });
2533
+ }
2534
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2535
+ readBooleanStateField(state, "passed", (value) => {
2536
+ setPassed(value);
2537
+ completedRef.current = value;
2538
+ });
2539
+ readBooleanStateField(state, "checked", setChecked);
2540
+ const item = state.keyboardItem;
2541
+ if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
2542
+ }
2543
+ });
2544
+ }, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
2545
+ useAssessmentHandleRegistration(checkId, handle, ref);
2546
+ const place = (targetId, itemId) => {
2547
+ if (passed && !props.enableRetry) return;
2548
+ setChecked(false);
2549
+ const prev = assignments[targetId];
2550
+ setAssignments((a) => ({ ...a, [targetId]: itemId }));
2551
+ setPool((p) => {
2552
+ const next = p.filter((id) => id !== itemId);
2553
+ if (prev) next.push(prev);
2554
+ return next;
2555
+ });
2556
+ setKeyboardItem(null);
2557
+ };
2558
+ const check = () => {
2559
+ if (!allFilled) return;
2560
+ setChecked(true);
2561
+ assessment.answer({
2562
+ checkId,
2563
+ interactionType: INTERACTION5,
2564
+ response: assignments,
2565
+ correct: passedThreshold
2566
+ });
2567
+ if (passedThreshold && !completedRef.current) {
2568
+ completedRef.current = true;
2569
+ setPassed(true);
2570
+ assessment.complete({
2571
+ checkId,
2572
+ interactionType: INTERACTION5,
2573
+ score,
2574
+ maxScore,
2575
+ passingScore: props.passingScore ?? maxScore
2576
+ });
2577
+ }
2578
+ };
2579
+ return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2580
+ /* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2581
+ /* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
2582
+ const item = props.items.find((i) => i.id === id);
2583
+ if (!item) return [];
2584
+ return /* @__PURE__ */ jsx12(
2585
+ "button",
2586
+ {
2587
+ type: "button",
2588
+ draggable: true,
2589
+ "data-testid": `drag-item-${id}`,
2590
+ "aria-pressed": keyboardItem === id,
2591
+ onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2592
+ onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2593
+ style: { margin: "0.25rem" },
2594
+ children: item.label
2595
+ },
2596
+ id
2597
+ );
2598
+ }) }),
2599
+ /* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
2600
+ const assigned = assignments[target.id];
2601
+ const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2602
+ return /* @__PURE__ */ jsxs8("li", { children: [
2603
+ /* @__PURE__ */ jsx12("strong", { children: target.label }),
2604
+ " ",
2605
+ /* @__PURE__ */ jsx12(
2606
+ "span",
2607
+ {
2608
+ role: "button",
2609
+ tabIndex: 0,
2610
+ "data-testid": `drop-${target.id}`,
2611
+ onDragOver: (e) => e.preventDefault(),
2612
+ onDrop: (e) => {
2613
+ e.preventDefault();
2614
+ const id = e.dataTransfer.getData("text/plain");
2615
+ if (id) place(target.id, id);
2616
+ },
2617
+ onClick: () => keyboardItem && place(target.id, keyboardItem),
2618
+ onKeyDown: (e) => {
2619
+ if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2620
+ },
2621
+ style: {
2622
+ display: "inline-block",
2623
+ minWidth: "8em",
2624
+ border: "1px dashed currentColor",
2625
+ padding: "0.25em"
2626
+ },
2627
+ children: label
2628
+ }
2629
+ )
2630
+ ] }, target.id);
2631
+ }) }),
2632
+ /* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
2633
+ checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
2634
+ ] });
2635
+ }
2636
+ var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
2637
+ var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
2638
+ return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2639
+ });
2640
+
2641
+ // src/blocks/AssessmentSequence.tsx
2642
+ import React16, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useId as useId3, useMemo as useMemo13, useRef as useRef13, useState as useState10 } from "react";
2643
+ import { deriveId } from "@lessonkit/core";
2644
+
2645
+ // src/compound/useCompoundShell.ts
2646
+ import { useMemo as useMemo12 } from "react";
2647
+ import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
2648
+
2649
+ // src/compound/useCompoundNavigation.ts
2650
+ import { useCallback as useCallback4 } from "react";
2651
+ function useCompoundNavigation(pageCount, index, setIndex) {
2652
+ const goNext = useCallback4(() => {
2653
+ if (pageCount < 1) return;
2654
+ setIndex((i) => Math.min(i + 1, pageCount - 1));
2655
+ }, [pageCount, setIndex]);
2656
+ const goPrev = useCallback4(() => {
2657
+ setIndex((i) => Math.max(i - 1, 0));
2658
+ }, [setIndex]);
2659
+ const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
2660
+ return {
2661
+ index: clampedIndex,
2662
+ setIndex,
2663
+ goNext,
2664
+ goPrev,
2665
+ progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
2666
+ };
2667
+ }
2668
+
2669
+ // src/compound/useCompoundPersistence.ts
2670
+ import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
2671
+ import {
2672
+ clampCompoundPageIndex as clampCompoundPageIndex2,
2673
+ createCompoundResumeState as createCompoundResumeState2,
2674
+ createSessionStoragePort as createSessionStoragePort3,
2675
+ loadCompoundState as loadCompoundState2
2676
+ } from "@lessonkit/core";
2677
+
2678
+ // src/compound/resumeChildHandles.ts
2679
+ function filterRegisteredChildStates(handles, childStates) {
2680
+ const filtered = {};
2681
+ for (const [key, value] of Object.entries(childStates)) {
2682
+ if (handles.has(key)) {
2683
+ filtered[key] = value;
2684
+ }
2685
+ }
2686
+ return filtered;
2687
+ }
2688
+ function resumeChildHandles(handles, childStates, opts) {
2689
+ const pendingKeys = Object.keys(childStates);
2690
+ const alreadyResumed = opts?.alreadyResumed;
2691
+ if (opts?.waitForHandles && pendingKeys.length > 0) {
2692
+ if (handles.size === 0) return false;
2693
+ const registeredPending = pendingKeys.filter((k) => handles.has(k));
2694
+ if (registeredPending.length === 0) {
2695
+ return false;
2696
+ }
2697
+ if (registeredPending.length < pendingKeys.length) {
2698
+ for (const key of registeredPending) {
2699
+ if (alreadyResumed?.has(key)) continue;
2700
+ const handle = handles.get(key);
2701
+ const child = childStates[key];
2702
+ if (handle?.resume && child) {
2703
+ handle.resume(child);
2704
+ alreadyResumed?.add(key);
2705
+ }
2706
+ }
2707
+ return false;
2708
+ }
2709
+ }
2710
+ for (const [checkId, handle] of handles) {
2711
+ if (alreadyResumed?.has(checkId)) continue;
2712
+ const child = childStates[checkId];
2713
+ if (child && handle.resume) {
2714
+ handle.resume(child);
2715
+ alreadyResumed?.add(checkId);
2716
+ }
2717
+ }
2718
+ return true;
2719
+ }
2720
+
2721
+ // src/compound/useCompoundResume.ts
2722
+ import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
2723
+ import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
2724
+ import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
2725
+ var warnedCompoundPersistFailure = false;
2726
+ function warnCompoundPersistFailure() {
2727
+ if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
2728
+ warnedCompoundPersistFailure = true;
2729
+ console.warn(
2730
+ "[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
2731
+ );
2732
+ }
2733
+ function useCompoundResume(opts) {
2734
+ const lessonkitCtx = useContext6(LessonkitContext);
2735
+ const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
2736
+ const resumedRef = useRef11(false);
2737
+ const resumeKeyRef = useRef11("");
2738
+ const prevEnabledRef = useRef11(opts.enabled);
2739
+ useEffect10(() => {
2740
+ storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
2741
+ }, [opts.storage, lessonkitCtx?.storage]);
2742
+ useEffect10(() => {
2743
+ if (!prevEnabledRef.current && opts.enabled) {
2744
+ resumedRef.current = false;
2745
+ }
2746
+ prevEnabledRef.current = opts.enabled;
2747
+ const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
2748
+ if (resumeKeyRef.current !== key) {
2749
+ resumeKeyRef.current = key;
2750
+ resumedRef.current = false;
2751
+ }
2752
+ if (!opts.enabled || !opts.courseId || resumedRef.current) return;
2753
+ const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
2754
+ if (saved) {
2755
+ resumedRef.current = true;
2756
+ opts.onResume?.(saved);
2757
+ }
2758
+ }, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
2759
+ return useCallback5(
2760
+ (state) => {
2761
+ if (!opts.enabled || !opts.courseId) return;
2762
+ const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
2763
+ if (!persisted) warnCompoundPersistFailure();
2764
+ },
2765
+ [opts.enabled, opts.courseId, opts.compoundId]
2766
+ );
2767
+ }
2768
+
2769
+ // src/compound/useCompoundPersistence.ts
2770
+ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort3()) {
2771
+ if (!enabled || !courseId || pageCount < 1) return 0;
2772
+ const saved = loadCompoundState2(storage, courseId, compoundId);
2773
+ if (!saved) return 0;
2774
+ return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
2775
+ }
2776
+ function stripOrphanChildStates(handles, childStates) {
2777
+ return filterRegisteredChildStates(handles, childStates);
2778
+ }
2779
+ function useCompoundPersistence(opts) {
2780
+ const lessonkitCtx = useContext7(LessonkitContext);
2781
+ const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
2782
+ const ctx = useCompoundRegistry();
2783
+ const handlesVersion = useCompoundHandlesVersion();
2784
+ const bridgeRef = useCompoundHydrationBridgeRef();
2785
+ const pendingChildResumeRef = useRef12(null);
2786
+ const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
2787
+ const loadedChildStatesRef = useRef12({});
2788
+ const skipSaveUntilHydratedRef = useRef12(false);
2789
+ const hydrationKeyRef = useRef12("");
2790
+ const hydrationInitRef = useRef12(false);
2791
+ const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
2792
+ if (hydrationKeyRef.current !== hydrationKey) {
2793
+ hydrationKeyRef.current = hydrationKey;
2794
+ hydrationInitRef.current = false;
2795
+ loadedChildStatesRef.current = {};
2796
+ skipSaveUntilHydratedRef.current = false;
2797
+ pendingChildResumeRef.current = null;
2798
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2799
+ }
2800
+ if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
2801
+ hydrationInitRef.current = true;
2802
+ const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
2803
+ if (saved && Object.keys(saved.childStates).length > 0) {
2804
+ loadedChildStatesRef.current = { ...saved.childStates };
2805
+ skipSaveUntilHydratedRef.current = true;
2806
+ pendingChildResumeRef.current = saved;
2807
+ }
2808
+ }
2809
+ const buildState = useCallback6(() => {
2810
+ const childStates = {
2811
+ ...loadedChildStatesRef.current
2812
+ };
2813
+ if (ctx) {
2814
+ for (const [checkId, entry] of ctx.getRegisteredHandles()) {
2815
+ const handle = entry.handle;
2816
+ if (handle.getCurrentState) {
2817
+ childStates[checkId] = handle.getCurrentState();
2818
+ delete loadedChildStatesRef.current[checkId];
2819
+ }
2820
+ }
2821
+ }
2822
+ return createCompoundResumeState2({
2823
+ activePageIndex: clampCompoundPageIndex2(opts.index, opts.pageCount),
2824
+ childStates
2825
+ });
2826
+ }, [ctx, opts.index, opts.pageCount]);
2827
+ const buildStateRef = useRef12(buildState);
2828
+ buildStateRef.current = buildState;
2829
+ const finalizeHydration = useCallback6(
2830
+ (childStates) => {
2831
+ loadedChildStatesRef.current = {
2832
+ ...loadedChildStatesRef.current,
2833
+ ...childStates
2834
+ };
2835
+ skipSaveUntilHydratedRef.current = false;
2836
+ pendingChildResumeRef.current = null;
2837
+ },
2838
+ []
2839
+ );
2840
+ const applyPendingChildResume = useCallback6(() => {
2841
+ const pending = pendingChildResumeRef.current;
2842
+ if (!pending || !ctx) return;
2843
+ const handles = ctx.getHandles();
2844
+ const applied = resumeChildHandles(handles, pending.childStates, {
2845
+ waitForHandles: true,
2846
+ alreadyResumed: resumedChildKeysRef.current
2847
+ });
2848
+ if (!applied) {
2849
+ const handlesAtWait = handles.size;
2850
+ queueMicrotask(() => {
2851
+ if (pendingChildResumeRef.current !== pending) return;
2852
+ const handlesNow = ctx.getHandles();
2853
+ if (handlesNow.size !== handlesAtWait) return;
2854
+ const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
2855
+ resumeChildHandles(handlesNow, registeredOnly2, {
2856
+ alreadyResumed: resumedChildKeysRef.current
2857
+ });
2858
+ finalizeHydration(registeredOnly2);
2859
+ });
2860
+ return;
2861
+ }
2862
+ const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
2863
+ finalizeHydration(registeredOnly);
2864
+ }, [ctx, finalizeHydration]);
2865
+ const saveResume = useCompoundResume({
2866
+ courseId: opts.courseId,
2867
+ compoundId: opts.compoundId,
2868
+ enabled: opts.enabled,
2869
+ storage,
2870
+ onResume: (state) => {
2871
+ const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
2872
+ loadedChildStatesRef.current = { ...state.childStates };
2873
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2874
+ opts.setIndex(clamped);
2875
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2876
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
2877
+ queueMicrotask(() => applyPendingChildResume());
2878
+ }
2879
+ });
2880
+ const persistNow = useCallback6(() => {
2881
+ if (!opts.enabled || !opts.courseId) return;
2882
+ saveResume(buildStateRef.current());
2883
+ }, [opts.enabled, opts.courseId, saveResume]);
2884
+ const notifyImperativeResume = useCallback6(
2885
+ (state) => {
2886
+ const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
2887
+ loadedChildStatesRef.current = { ...state.childStates };
2888
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2889
+ opts.setIndex(clamped);
2890
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2891
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
2892
+ queueMicrotask(() => applyPendingChildResume());
2893
+ },
2894
+ [opts.pageCount, opts.setIndex, applyPendingChildResume]
2895
+ );
2896
+ useEffect11(() => {
2897
+ if (!bridgeRef) return;
2898
+ bridgeRef.current = { notifyImperativeResume };
2899
+ return () => {
2900
+ if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
2901
+ bridgeRef.current = null;
2902
+ }
2903
+ };
2904
+ }, [bridgeRef, notifyImperativeResume]);
2905
+ useEffect11(() => {
2906
+ persistNow();
2907
+ }, [persistNow, opts.index, opts.pageCount, handlesVersion]);
2908
+ useEffect11(() => {
2909
+ applyPendingChildResume();
2910
+ }, [opts.index, handlesVersion, applyPendingChildResume]);
2911
+ useEffect11(() => {
2912
+ if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
2913
+ const flushOnExit = () => {
2914
+ if (document.visibilityState === "hidden") persistNow();
2915
+ };
2916
+ document.addEventListener("visibilitychange", flushOnExit);
2917
+ window.addEventListener("pagehide", flushOnExit);
2918
+ return () => {
2919
+ document.removeEventListener("visibilitychange", flushOnExit);
2920
+ window.removeEventListener("pagehide", flushOnExit);
2921
+ };
2922
+ }, [opts.enabled, opts.courseId, persistNow]);
2923
+ }
2924
+
2925
+ // src/compound/useCompoundShell.ts
2926
+ function useCompoundShell(opts) {
2927
+ const ctx = useCompoundRegistry();
2928
+ useCompoundPersistence({
2929
+ courseId: opts.courseId,
2930
+ compoundId: opts.compoundId,
2931
+ pageCount: opts.pageCount,
2932
+ index: opts.index,
2933
+ setIndex: opts.setIndex,
2934
+ enabled: opts.persistEnabled,
2935
+ storage: opts.storage
2936
+ });
2937
+ const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
2938
+ const visibleIndex = clampCompoundPageIndex3(opts.index, opts.pageCount);
2939
+ useCompoundHandleRef(opts.ref, {
2940
+ activePageIndex: visibleIndex,
2941
+ setActivePageIndex: opts.setIndex,
2942
+ getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
2943
+ getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
2944
+ pageCount: opts.pageCount,
2945
+ enableSolutionsButton: opts.enableSolutionsButton
2946
+ });
2947
+ return { visibleIndex, goNext, goPrev, progress, ctx };
2948
+ }
2949
+ function useCompoundInitialIndex(opts) {
2950
+ return useMemo12(
2951
+ () => readCompoundInitialIndex(
2952
+ opts.courseId,
2953
+ opts.compoundId,
2954
+ opts.pageCount,
2955
+ opts.persistEnabled,
2956
+ opts.storage
2957
+ ),
2958
+ [opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
2959
+ );
2960
+ }
2961
+
2962
+ // src/compound/validateChildren.ts
2963
+ import React15 from "react";
2964
+ import {
2965
+ ACCORDION_FORBIDDEN_CHILD_TYPES,
2966
+ COMPOUND_MAX_NESTING_DEPTH,
2967
+ isChildTypeAllowed
2968
+ } from "@lessonkit/core";
2969
+
2970
+ // src/compound/blockType.ts
2971
+ var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
2972
+ function setLessonkitBlockType(component, blockType) {
2973
+ component[LESSONKIT_BLOCK_TYPE] = blockType;
2974
+ if (!component.displayName) {
2975
+ component.displayName = blockType;
2976
+ }
2977
+ return component;
2978
+ }
2979
+ function getLessonkitBlockType(component) {
2980
+ if (!component || typeof component !== "object" && typeof component !== "function") {
2981
+ return void 0;
2982
+ }
2983
+ const typed = component;
2984
+ return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
2985
+ }
2986
+
2987
+ // src/compound/validateChildren.ts
2988
+ var warnedPairs = /* @__PURE__ */ new Set();
2989
+ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
2990
+ "Page",
2991
+ "InteractiveBook",
2992
+ "Slide",
2993
+ "SlideDeck",
2994
+ "AssessmentSequence"
2995
+ ]);
2996
+ function warnOrThrow(msg, strict) {
2997
+ if (strict) throw new Error(msg);
2998
+ if (!warnedPairs.has(msg)) {
2999
+ warnedPairs.add(msg);
3000
+ console.warn(msg);
3001
+ }
3002
+ }
3003
+ function validateNode(parent, node, depth, strict) {
3004
+ React15.Children.forEach(node, (child) => {
3005
+ if (!React15.isValidElement(child)) return;
3006
+ const blockType = getLessonkitBlockType(child.type);
3007
+ if (!blockType) {
3008
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
3009
+ validateNode(parent, child.props.children, depth, strict);
3010
+ }
3011
+ return;
3012
+ }
3013
+ if (!isChildTypeAllowed(parent, blockType)) {
3014
+ const key = `${parent}:${blockType}`;
3015
+ if (!warnedPairs.has(key)) {
3016
+ warnedPairs.add(key);
3017
+ const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
3018
+ if (strict) throw new Error(msg);
3019
+ console.warn(msg);
3020
+ }
3021
+ }
3022
+ if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
3023
+ const maxDepth = COMPOUND_MAX_NESTING_DEPTH[parent];
3024
+ if (depth >= maxDepth) {
3025
+ warnOrThrow(
3026
+ `[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
3027
+ strict
3028
+ );
3029
+ }
3030
+ const nestedParent = blockType;
3031
+ validateNode(nestedParent, child.props.children, depth + 1, strict);
3032
+ } else if (blockType === "Accordion") {
3033
+ const sections = child.props.sections;
3034
+ if (sections) validateAccordionSections(sections, strict);
3035
+ } else if (child.props && typeof child.props === "object" && "children" in child.props) {
3036
+ validateSubtreeForForbidden(
3037
+ child.props.children,
3038
+ ACCORDION_FORBIDDEN_CHILD_TYPES,
3039
+ strict
3040
+ );
3041
+ }
3042
+ });
3043
+ }
3044
+ function validateSubtreeForForbidden(node, forbidden, strict) {
3045
+ React15.Children.forEach(node, (child) => {
3046
+ if (!React15.isValidElement(child)) return;
3047
+ const blockType = getLessonkitBlockType(child.type);
3048
+ if (blockType && forbidden.includes(blockType)) {
3049
+ warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
3050
+ }
3051
+ if (blockType === "Accordion") {
3052
+ const sections = child.props.sections;
3053
+ if (sections) validateAccordionSections(sections, strict);
3054
+ return;
3055
+ }
3056
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
3057
+ validateSubtreeForForbidden(
3058
+ child.props.children,
3059
+ forbidden,
3060
+ strict
3061
+ );
3062
+ }
3063
+ });
3064
+ }
3065
+ function validateAccordionSections(sections, strict) {
3066
+ if (!isDevEnvironment4() && !strict) return;
3067
+ for (const section of sections) {
3068
+ validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
3069
+ }
3070
+ }
3071
+ function validateCompoundChildren(parent, children, strict) {
3072
+ if (!isDevEnvironment4() && !strict) return;
3073
+ validateNode(parent, children, 0, strict);
3074
+ }
3075
+
3076
+ // src/compound/warnPersistence.ts
3077
+ var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
3078
+ function warnSharedCompoundStorageKey(opts) {
3079
+ if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
3080
+ console.warn(
3081
+ `[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
3082
+ );
3083
+ }
3084
+
3085
+ // src/blocks/AssessmentSequence.tsx
3086
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
3087
+ var AssessmentSequenceInner = forwardRef7(
3088
+ function AssessmentSequenceInner2(props, ref) {
3089
+ const { compoundId, childArray, index, setIndex, persistEnabled } = props;
3090
+ const sequential = props.sequential !== false;
3091
+ const { config } = useLessonkit();
3092
+ const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
3093
+ courseId: config.courseId,
3094
+ compoundId,
3095
+ pageCount: childArray.length,
3096
+ index,
3097
+ setIndex,
3098
+ persistEnabled,
3099
+ ref,
3100
+ enableSolutionsButton: props.enableSolutionsButton
3101
+ });
3102
+ validateCompoundChildren("AssessmentSequence", props.children);
3103
+ if (!sequential) {
3104
+ return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
3105
+ }
3106
+ return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
3107
+ /* @__PURE__ */ jsxs9("p", { children: [
3108
+ "Question ",
3109
+ progress.current,
3110
+ " of ",
3111
+ progress.total
3112
+ ] }),
3113
+ /* @__PURE__ */ jsx13("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx13("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ jsx13(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
3114
+ /* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
3115
+ /* @__PURE__ */ jsx13(
3116
+ "button",
3117
+ {
3118
+ type: "button",
3119
+ "data-testid": "sequence-prev",
3120
+ disabled: visibleIndex === 0 || childArray.length === 0,
3121
+ onClick: goPrev,
3122
+ children: "Previous"
3123
+ }
3124
+ ),
3125
+ /* @__PURE__ */ jsx13(
3126
+ "button",
3127
+ {
3128
+ type: "button",
3129
+ "data-testid": "sequence-next",
3130
+ disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
3131
+ onClick: goNext,
3132
+ children: "Next"
3133
+ }
3134
+ )
3135
+ ] })
3136
+ ] });
3137
+ }
3138
+ );
3139
+ var AssessmentSequence = forwardRef7(
3140
+ function AssessmentSequence2(props, ref) {
3141
+ const reactInstanceId = useId3();
3142
+ const autoCompoundIdRef = useRef13(null);
3143
+ if (!props.blockId && !autoCompoundIdRef.current) {
3144
+ autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
3145
+ }
3146
+ const compoundId = useMemo13(
3147
+ () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
3148
+ [props.blockId]
3149
+ );
3150
+ const childArray = React16.Children.toArray(props.children).filter(
3151
+ React16.isValidElement
3152
+ );
3153
+ const { config, storage } = useLessonkit();
3154
+ const persistEnabled = config.session?.persistCompoundState !== false;
3155
+ useEffect12(() => {
3156
+ warnSharedCompoundStorageKey({
3157
+ persistEnabled,
3158
+ hasExplicitBlockId: Boolean(props.blockId),
3159
+ componentName: "AssessmentSequence"
3160
+ });
3161
+ }, [persistEnabled, props.blockId]);
3162
+ const initialIndex = useCompoundInitialIndex({
3163
+ courseId: config.courseId,
3164
+ compoundId,
3165
+ pageCount: childArray.length,
3166
+ persistEnabled,
3167
+ storage
3168
+ });
3169
+ const [index, setIndex] = useState10(initialIndex);
3170
+ const setIndexStable = useCallback7((i) => setIndex(i), []);
3171
+ useEffect12(() => {
3172
+ setIndex(initialIndex);
3173
+ }, [config.courseId, compoundId, initialIndex]);
3174
+ return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
3175
+ AssessmentSequenceInner,
3176
+ {
3177
+ ...props,
3178
+ ref,
3179
+ compoundId,
3180
+ childArray,
3181
+ index,
3182
+ setIndex,
3183
+ persistEnabled
3184
+ }
3185
+ ) });
3186
+ }
3187
+ );
3188
+ setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
3189
+
3190
+ // src/blocks/Text.tsx
3191
+ import "react";
3192
+ import { jsx as jsx14 } from "react/jsx-runtime";
3193
+ function Text(props) {
3194
+ return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
3195
+ }
3196
+ setLessonkitBlockType(Text, "Text");
3197
+
3198
+ // src/blocks/Heading.tsx
3199
+ import { jsx as jsx15 } from "react/jsx-runtime";
3200
+ function Heading(props) {
3201
+ const Tag = `h${props.level}`;
3202
+ return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
3203
+ }
3204
+ setLessonkitBlockType(Heading, "Heading");
3205
+
3206
+ // src/blocks/Image.tsx
3207
+ import { jsx as jsx16 } from "react/jsx-runtime";
3208
+ function Image(props) {
3209
+ return /* @__PURE__ */ jsx16(
3210
+ "img",
3211
+ {
3212
+ src: props.src,
3213
+ alt: props.alt,
3214
+ "data-lk-block-id": props.blockId,
3215
+ "data-testid": props.blockId ? `image-${props.blockId}` : "image",
3216
+ style: { maxWidth: "100%", height: "auto" }
3217
+ }
3218
+ );
3219
+ }
3220
+ setLessonkitBlockType(Image, "Image");
3221
+
3222
+ // src/blocks/Page.tsx
3223
+ import { useEffect as useEffect13 } from "react";
3224
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
3225
+ function Page(props) {
3226
+ validateCompoundChildren("Page", props.children);
3227
+ const { track } = useLessonkit();
3228
+ const lessonId = useEnclosingLessonId();
3229
+ useEffect13(() => {
3230
+ if (props.hidden || !lessonId || props.parentType) return;
3231
+ track(
3232
+ "compound_page_viewed",
3233
+ {
3234
+ blockId: props.blockId,
3235
+ pageIndex: props.pageIndex ?? 0,
3236
+ parentType: props.parentType
3237
+ },
3238
+ { lessonId }
3239
+ );
3240
+ }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
3241
+ return /* @__PURE__ */ jsxs10(
3242
+ "section",
3243
+ {
3244
+ "aria-label": props.title ?? "Page",
3245
+ "data-lk-block-id": props.blockId,
3246
+ "data-testid": `page-${props.blockId}`,
3247
+ hidden: props.hidden ? true : void 0,
3248
+ children: [
3249
+ props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
3250
+ /* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
3251
+ ]
3252
+ }
3253
+ );
3254
+ }
3255
+ setLessonkitBlockType(Page, "Page");
3256
+
3257
+ // src/blocks/InteractiveBook.tsx
3258
+ import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
3259
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
3260
+ var InteractiveBookInner = forwardRef8(
3261
+ function InteractiveBookInner2(props, ref) {
3262
+ const { blockId, pages, index, setIndex, persistEnabled } = props;
3263
+ validateCompoundChildren("InteractiveBook", pages);
3264
+ const { config, track } = useLessonkit();
3265
+ const lessonId = useEnclosingLessonId();
3266
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3267
+ courseId: config.courseId,
3268
+ compoundId: blockId,
3269
+ pageCount: pages.length,
3270
+ index,
3271
+ setIndex,
3272
+ persistEnabled,
3273
+ ref
3274
+ });
3275
+ const pageTitles = useMemo14(
3276
+ () => pages.map((page) => page.props.title),
3277
+ [pages]
3278
+ );
3279
+ useEffect14(() => {
3280
+ if (!lessonId || pages.length === 0) return;
3281
+ track(
3282
+ "book_page_viewed",
3283
+ {
3284
+ blockId,
3285
+ pageIndex: visibleIndex,
3286
+ pageTitle: pageTitles[visibleIndex]
3287
+ },
3288
+ { lessonId }
3289
+ );
3290
+ }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
3291
+ return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
3292
+ /* @__PURE__ */ jsx18("h3", { children: props.title }),
3293
+ /* @__PURE__ */ jsxs11("p", { children: [
3294
+ "Page ",
3295
+ progress.current,
3296
+ " of ",
3297
+ progress.total
3298
+ ] }),
3299
+ props.showBookScore && ctx ? /* @__PURE__ */ jsxs11("p", { "data-testid": "book-score", children: [
3300
+ "Score: ",
3301
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3302
+ " /",
3303
+ " ",
3304
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3305
+ ] }) : null,
3306
+ /* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
3307
+ (page, i) => React19.cloneElement(page, {
3308
+ key: page.key ?? page.props.blockId,
3309
+ hidden: i !== visibleIndex,
3310
+ pageIndex: i,
3311
+ parentType: "InteractiveBook"
3312
+ })
3313
+ ) }),
3314
+ /* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
3315
+ /* @__PURE__ */ jsx18(
3316
+ "button",
3317
+ {
3318
+ type: "button",
3319
+ "data-testid": "book-prev",
3320
+ disabled: visibleIndex === 0 || pages.length === 0,
3321
+ onClick: goPrev,
3322
+ children: "Previous"
3323
+ }
3324
+ ),
3325
+ /* @__PURE__ */ jsx18(
3326
+ "button",
3327
+ {
3328
+ type: "button",
3329
+ "data-testid": "book-next",
3330
+ disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
3331
+ onClick: goNext,
3332
+ children: "Next"
3333
+ }
3334
+ )
3335
+ ] })
3336
+ ] });
3337
+ }
3338
+ );
3339
+ var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
3340
+ const blockId = useMemo14(
3341
+ () => normalizeComponentId(props.blockId, "blockId"),
3342
+ [props.blockId]
3343
+ );
3344
+ const pages = React19.Children.toArray(props.children).filter(
3345
+ React19.isValidElement
3346
+ );
3347
+ const { config, storage } = useLessonkit();
3348
+ const persistEnabled = config.session?.persistCompoundState !== false;
3349
+ const initialIndex = useCompoundInitialIndex({
3350
+ courseId: config.courseId,
3351
+ compoundId: blockId,
3352
+ pageCount: pages.length,
3353
+ persistEnabled,
3354
+ storage
3355
+ });
3356
+ const [index, setIndex] = useState11(initialIndex);
3357
+ const setIndexStable = useCallback8((i) => setIndex(i), []);
3358
+ useEffect14(() => {
3359
+ setIndex(initialIndex);
3360
+ }, [config.courseId, blockId, initialIndex]);
3361
+ return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
3362
+ InteractiveBookInner,
3363
+ {
3364
+ ...props,
3365
+ ref,
3366
+ blockId,
3367
+ pages,
3368
+ index,
3369
+ setIndex,
3370
+ persistEnabled
3371
+ }
3372
+ ) });
3373
+ });
3374
+ setLessonkitBlockType(InteractiveBook, "InteractiveBook");
3375
+
3376
+ // src/blocks/Slide.tsx
3377
+ import { useEffect as useEffect15 } from "react";
3378
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3379
+ function Slide(props) {
3380
+ validateCompoundChildren("Slide", props.children);
3381
+ const { track } = useLessonkit();
3382
+ const lessonId = useEnclosingLessonId();
3383
+ useEffect15(() => {
3384
+ if (props.hidden || !lessonId || props.parentType) return;
3385
+ track(
3386
+ "compound_page_viewed",
3387
+ {
3388
+ blockId: props.blockId,
3389
+ pageIndex: props.slideIndex ?? 0,
3390
+ parentType: props.parentType
3391
+ },
3392
+ { lessonId }
3393
+ );
3394
+ }, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
3395
+ return /* @__PURE__ */ jsxs12(
3396
+ "section",
3397
+ {
3398
+ "aria-label": props.title ?? "Slide",
3399
+ "data-lk-block-id": props.blockId,
3400
+ "data-testid": `slide-${props.blockId}`,
3401
+ hidden: props.hidden ? true : void 0,
3402
+ children: [
3403
+ props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
3404
+ /* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
3405
+ ]
3406
+ }
3407
+ );
3408
+ }
3409
+ setLessonkitBlockType(Slide, "Slide");
3410
+
3411
+ // src/blocks/SlideDeck.tsx
3412
+ import React21, { forwardRef as forwardRef9, useCallback as useCallback9, useEffect as useEffect17, useMemo as useMemo15, useRef as useRef14, useState as useState12 } from "react";
3413
+
3414
+ // src/compound/useCompoundKeyboardNav.ts
3415
+ import { useEffect as useEffect16 } from "react";
3416
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
3417
+ function isEditableTarget(target) {
3418
+ if (!(target instanceof HTMLElement)) return false;
3419
+ if (INTERACTIVE_TAGS.has(target.tagName)) return true;
3420
+ if (target.isContentEditable) return true;
3421
+ if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
3422
+ return true;
3423
+ }
3424
+ return false;
3425
+ }
3426
+ function useCompoundKeyboardNav(opts) {
3427
+ const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
3428
+ useEffect16(() => {
3429
+ const el = containerRef.current;
3430
+ if (!el || pageCount === 0) return;
3431
+ const onKeyDown = (event) => {
3432
+ if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
3433
+ return;
3434
+ }
3435
+ if (isEditableTarget(event.target)) return;
3436
+ switch (event.key) {
3437
+ case "ArrowRight":
3438
+ case "ArrowDown":
3439
+ if (visibleIndex < pageCount - 1) {
3440
+ event.preventDefault();
3441
+ goNext();
3442
+ }
3443
+ break;
3444
+ case "ArrowLeft":
3445
+ case "ArrowUp":
3446
+ if (visibleIndex > 0) {
3447
+ event.preventDefault();
3448
+ goPrev();
3449
+ }
3450
+ break;
3451
+ case "Home":
3452
+ if (visibleIndex !== 0) {
3453
+ event.preventDefault();
3454
+ setIndex(0);
3455
+ }
3456
+ break;
3457
+ case "End":
3458
+ if (visibleIndex !== pageCount - 1) {
3459
+ event.preventDefault();
3460
+ setIndex(pageCount - 1);
3461
+ }
3462
+ break;
3463
+ default:
3464
+ break;
3465
+ }
3466
+ };
3467
+ el.addEventListener("keydown", onKeyDown);
3468
+ return () => el.removeEventListener("keydown", onKeyDown);
3469
+ }, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
3470
+ }
3471
+
3472
+ // src/blocks/SlideDeck.tsx
3473
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3474
+ var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
3475
+ const { blockId, slides, index, setIndex, persistEnabled } = props;
3476
+ validateCompoundChildren("SlideDeck", slides);
3477
+ const { config, track } = useLessonkit();
3478
+ const lessonId = useEnclosingLessonId();
3479
+ const containerRef = useRef14(null);
3480
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3481
+ courseId: config.courseId,
3482
+ compoundId: blockId,
3483
+ pageCount: slides.length,
3484
+ index,
3485
+ setIndex,
3486
+ persistEnabled,
3487
+ ref
3488
+ });
3489
+ const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
3490
+ useCompoundKeyboardNav({
3491
+ containerRef,
3492
+ visibleIndex,
3493
+ pageCount: slides.length,
3494
+ goNext,
3495
+ goPrev,
3496
+ setIndex: setIndexStable
3497
+ });
3498
+ const slideTitles = useMemo15(
3499
+ () => slides.map((slide) => slide.props.title),
3500
+ [slides]
3501
+ );
3502
+ useEffect17(() => {
3503
+ if (!lessonId || slides.length === 0) return;
3504
+ track(
3505
+ "slide_viewed",
3506
+ {
3507
+ blockId,
3508
+ slideIndex: visibleIndex,
3509
+ slideTitle: slideTitles[visibleIndex]
3510
+ },
3511
+ { lessonId }
3512
+ );
3513
+ }, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
3514
+ return /* @__PURE__ */ jsxs13(
3515
+ "section",
3516
+ {
3517
+ ref: containerRef,
3518
+ tabIndex: -1,
3519
+ "aria-label": props.title,
3520
+ "data-testid": "slide-deck",
3521
+ "data-lk-block-id": blockId,
3522
+ children: [
3523
+ /* @__PURE__ */ jsx20("h3", { children: props.title }),
3524
+ /* @__PURE__ */ jsxs13("p", { children: [
3525
+ "Slide ",
3526
+ progress.current,
3527
+ " of ",
3528
+ progress.total
3529
+ ] }),
3530
+ props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
3531
+ "Score: ",
3532
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3533
+ " /",
3534
+ " ",
3535
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3536
+ ] }) : null,
3537
+ /* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
3538
+ (slide, i) => React21.cloneElement(slide, {
3539
+ key: slide.key ?? slide.props.blockId,
3540
+ hidden: i !== visibleIndex,
3541
+ slideIndex: i,
3542
+ parentType: "SlideDeck"
3543
+ })
3544
+ ) }),
3545
+ /* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
3546
+ /* @__PURE__ */ jsx20(
3547
+ "button",
3548
+ {
3549
+ type: "button",
3550
+ "data-testid": "slide-prev",
3551
+ disabled: visibleIndex === 0 || slides.length === 0,
3552
+ onClick: goPrev,
3553
+ children: "Previous slide"
3554
+ }
3555
+ ),
3556
+ /* @__PURE__ */ jsx20(
3557
+ "button",
3558
+ {
3559
+ type: "button",
3560
+ "data-testid": "slide-next",
3561
+ disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
3562
+ onClick: goNext,
3563
+ children: "Next slide"
3564
+ }
3565
+ )
3566
+ ] })
3567
+ ]
3568
+ }
3569
+ );
3570
+ });
3571
+ var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
3572
+ const blockId = useMemo15(
3573
+ () => normalizeComponentId(props.blockId, "blockId"),
3574
+ [props.blockId]
3575
+ );
3576
+ const slides = React21.Children.toArray(props.children).filter(
3577
+ React21.isValidElement
3578
+ );
3579
+ const { config, storage } = useLessonkit();
3580
+ const persistEnabled = config.session?.persistCompoundState !== false;
3581
+ const initialIndex = useCompoundInitialIndex({
3582
+ courseId: config.courseId,
3583
+ compoundId: blockId,
3584
+ pageCount: slides.length,
3585
+ persistEnabled,
3586
+ storage
3587
+ });
3588
+ const [index, setIndex] = useState12(initialIndex);
3589
+ const setIndexStable = useCallback9((i) => setIndex(i), []);
3590
+ useEffect17(() => {
3591
+ setIndex(initialIndex);
3592
+ }, [config.courseId, blockId, initialIndex]);
3593
+ return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
3594
+ SlideDeckInner,
3595
+ {
3596
+ ...props,
3597
+ ref,
3598
+ blockId,
3599
+ slides,
3600
+ index,
3601
+ setIndex,
3602
+ persistEnabled
3603
+ }
3604
+ ) });
3605
+ });
3606
+ setLessonkitBlockType(SlideDeck, "SlideDeck");
3607
+
3608
+ // src/blocks/Accordion.tsx
3609
+ import { useId as useId4, useState as useState13 } from "react";
3610
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
3611
+ function Accordion(props) {
3612
+ if (isDevEnvironment4()) {
3613
+ validateAccordionSections(props.sections);
3614
+ }
3615
+ const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
3616
+ const { track } = useLessonkit();
3617
+ const lessonId = useEnclosingLessonId();
3618
+ const baseId = useId4();
3619
+ const toggle = (sectionId) => {
3620
+ setOpen((prev) => {
3621
+ const next = new Set(prev);
3622
+ const expanded = !next.has(sectionId);
3623
+ if (expanded) next.add(sectionId);
3624
+ else next.delete(sectionId);
3625
+ track(
3626
+ "accordion_section_toggled",
3627
+ { blockId: props.blockId, sectionId, expanded },
3628
+ lessonId ? { lessonId } : void 0
3629
+ );
3630
+ return next;
3631
+ });
3632
+ };
3633
+ return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3634
+ const expanded = open.has(section.id);
3635
+ const panelId = `${baseId}-${section.id}`;
3636
+ const triggerId = `${baseId}-trigger-${section.id}`;
3637
+ return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
3638
+ /* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
3639
+ "button",
3640
+ {
3641
+ id: triggerId,
3642
+ type: "button",
3643
+ "aria-expanded": expanded,
3644
+ "aria-controls": panelId,
3645
+ "data-testid": `accordion-trigger-${section.id}`,
3646
+ onClick: () => toggle(section.id),
3647
+ children: section.title
3648
+ }
3649
+ ) }),
3650
+ expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3651
+ ] }, section.id);
3652
+ }) });
3653
+ }
3654
+ setLessonkitBlockType(Accordion, "Accordion");
3655
+
3656
+ // src/blocks/DialogCards.tsx
3657
+ import { useState as useState14 } from "react";
3658
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
3659
+ function DialogCards(props) {
3660
+ const [index, setIndex] = useState14(0);
3661
+ const [flipped, setFlipped] = useState14(false);
3662
+ const card = props.cards[index];
3663
+ if (!card) return null;
3664
+ return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3665
+ /* @__PURE__ */ jsxs15("p", { children: [
3666
+ "Card ",
3667
+ index + 1,
3668
+ " of ",
3669
+ props.cards.length
3670
+ ] }),
3671
+ /* @__PURE__ */ jsx22(
3672
+ "button",
3673
+ {
3674
+ type: "button",
3675
+ "data-testid": "dialog-card-flip",
3676
+ "aria-pressed": flipped,
3677
+ onClick: () => setFlipped((f) => !f),
3678
+ style: { minHeight: "6rem", width: "100%" },
3679
+ children: flipped ? card.back : card.front
3680
+ }
3681
+ ),
3682
+ /* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
3683
+ /* @__PURE__ */ jsx22(
3684
+ "button",
3685
+ {
3686
+ type: "button",
3687
+ "data-testid": "dialog-prev",
3688
+ disabled: index === 0,
3689
+ onClick: () => {
3690
+ setIndex((i) => i - 1);
3691
+ setFlipped(false);
3692
+ },
3693
+ children: "Previous"
3694
+ }
3695
+ ),
3696
+ /* @__PURE__ */ jsx22(
3697
+ "button",
3698
+ {
3699
+ type: "button",
3700
+ "data-testid": "dialog-next",
3701
+ disabled: index >= props.cards.length - 1,
3702
+ onClick: () => {
3703
+ setIndex((i) => i + 1);
3704
+ setFlipped(false);
3705
+ },
3706
+ children: "Next"
3707
+ }
3708
+ )
3709
+ ] })
3710
+ ] });
3711
+ }
3712
+ setLessonkitBlockType(DialogCards, "DialogCards");
3713
+
3714
+ // src/blocks/Flashcards.tsx
3715
+ import { useState as useState15 } from "react";
3716
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
3717
+ function Flashcards(props) {
3718
+ const [index, setIndex] = useState15(0);
3719
+ const [face, setFace] = useState15("front");
3720
+ const { track } = useLessonkit();
3721
+ const lessonId = useEnclosingLessonId();
3722
+ const card = props.cards[index];
3723
+ if (!card) return null;
3724
+ const flip = () => {
3725
+ const next = face === "front" ? "back" : "front";
3726
+ setFace(next);
3727
+ track(
3728
+ "flashcard_flipped",
3729
+ { blockId: props.blockId, cardIndex: index, face: next },
3730
+ lessonId ? { lessonId } : void 0
3731
+ );
3732
+ };
3733
+ return /* @__PURE__ */ jsxs16("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3734
+ /* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3735
+ props.selfScore ? /* @__PURE__ */ jsx23("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3736
+ /* @__PURE__ */ jsx23(
3737
+ "button",
3738
+ {
3739
+ type: "button",
3740
+ "data-testid": "flashcard-next",
3741
+ disabled: index >= props.cards.length - 1,
3742
+ onClick: () => {
3743
+ setIndex((i) => i + 1);
3744
+ setFace("front");
3745
+ },
3746
+ children: "Next card"
3747
+ }
3748
+ )
3749
+ ] });
3750
+ }
3751
+ setLessonkitBlockType(Flashcards, "Flashcards");
3752
+
3753
+ // src/blocks/ImageHotspots.tsx
3754
+ import { useState as useState16 } from "react";
3755
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3756
+ function ImageHotspots(props) {
3757
+ const [active, setActive] = useState16(null);
3758
+ const { track } = useLessonkit();
3759
+ const lessonId = useEnclosingLessonId();
3760
+ const open = (hotspotId) => {
3761
+ setActive(hotspotId);
3762
+ track(
3763
+ "hotspot_opened",
3764
+ { blockId: props.blockId, hotspotId },
3765
+ lessonId ? { lessonId } : void 0
3766
+ );
3767
+ };
3768
+ return /* @__PURE__ */ jsxs17("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3769
+ /* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
3770
+ /* @__PURE__ */ jsx24("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3771
+ props.hotspots.map((h) => /* @__PURE__ */ jsx24(
3772
+ "button",
3773
+ {
3774
+ type: "button",
3775
+ "aria-expanded": active === h.id,
3776
+ "aria-label": h.label,
3777
+ "data-testid": `hotspot-${h.id}`,
3778
+ style: {
3779
+ position: "absolute",
3780
+ left: `${h.x}%`,
3781
+ top: `${h.y}%`,
3782
+ transform: "translate(-50%, -50%)"
3783
+ },
3784
+ onClick: () => open(h.id),
3785
+ children: "+"
3786
+ },
3787
+ h.id
3788
+ ))
3789
+ ] }),
3790
+ active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3791
+ props.hotspots.find((h) => h.id === active)?.content,
3792
+ /* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3793
+ ] }) : null
3794
+ ] });
3795
+ }
3796
+ setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3797
+
3798
+ // src/blocks/ImageSlider.tsx
3799
+ import { useState as useState17 } from "react";
3800
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
3801
+ function ImageSlider(props) {
3802
+ const [index, setIndex] = useState17(0);
3803
+ const { track } = useLessonkit();
3804
+ const lessonId = useEnclosingLessonId();
3805
+ const slide = props.slides[index];
3806
+ if (!slide) return null;
3807
+ const goTo = (next) => {
3808
+ setIndex(next);
3809
+ track(
3810
+ "image_slider_changed",
3811
+ { blockId: props.blockId, slideIndex: next },
3812
+ lessonId ? { lessonId } : void 0
3813
+ );
3814
+ };
3815
+ return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
3816
+ /* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
3817
+ slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
3818
+ /* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
3819
+ /* @__PURE__ */ jsx25(
3820
+ "button",
3821
+ {
3822
+ type: "button",
3823
+ "data-testid": "slider-prev",
3824
+ disabled: index === 0,
3825
+ onClick: () => goTo(index - 1),
3826
+ children: "Previous"
3827
+ }
3828
+ ),
3829
+ /* @__PURE__ */ jsxs18("span", { children: [
3830
+ index + 1,
3831
+ " / ",
3832
+ props.slides.length
3833
+ ] }),
3834
+ /* @__PURE__ */ jsx25(
3835
+ "button",
3836
+ {
3837
+ type: "button",
3838
+ "data-testid": "slider-next",
3839
+ disabled: index >= props.slides.length - 1,
3840
+ onClick: () => goTo(index + 1),
3841
+ children: "Next"
3842
+ }
3843
+ )
3844
+ ] })
3845
+ ] });
3846
+ }
3847
+ setLessonkitBlockType(ImageSlider, "ImageSlider");
3848
+
3849
+ // src/blocks/FindHotspot.tsx
3850
+ import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useState as useState18 } from "react";
3851
+ import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
3852
+ var INTERACTION6 = "findHotspot";
3853
+ function FindHotspotInner(props, ref) {
3854
+ const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3855
+ const [selected, setSelected] = useState18(null);
3856
+ const [checked, setChecked] = useState18(false);
3857
+ const assessment = useAssessmentState(props.enclosingLessonId);
3858
+ const targetIdsKey = props.targets.map((t) => t.id).join("\0");
3859
+ useEffect18(() => {
3860
+ setSelected(null);
3861
+ setChecked(false);
3862
+ }, [checkId, props.correctTargetId, targetIdsKey]);
3863
+ const correct = selected === props.correctTargetId;
3864
+ const handle = useMemo16(
3865
+ () => buildAssessmentHandle({
3866
+ checkId,
3867
+ getScore: () => checked && correct ? 1 : 0,
3868
+ getMaxScore: () => 1,
3869
+ getAnswerGiven: () => selected !== null,
3870
+ resetTask: () => {
3871
+ setSelected(null);
3872
+ setChecked(false);
3873
+ },
3874
+ showSolutions: () => setSelected(props.correctTargetId),
3875
+ getXAPIData: () => ({
3876
+ checkId,
3877
+ interactionType: INTERACTION6,
3878
+ response: selected ?? void 0,
3879
+ correct: checked ? correct : void 0,
3880
+ score: checked && correct ? 1 : 0,
3881
+ maxScore: 1
3882
+ }),
3883
+ getCurrentState: () => ({ selected, checked }),
3884
+ resume: (state) => {
3885
+ const nextSelected = readStringField(state, "selected");
3886
+ if (typeof nextSelected === "string" || nextSelected === null) {
3887
+ const valid = nextSelected === null || props.targets.some((t) => t.id === nextSelected);
3888
+ setSelected(valid ? nextSelected : null);
3889
+ }
3890
+ readBooleanStateField(state, "checked", setChecked);
3891
+ }
3892
+ }),
3893
+ [checkId, selected, checked, correct, props.correctTargetId, props.targets]
3894
+ );
3895
+ useAssessmentHandleRegistration(checkId, handle, ref);
3896
+ const selectTarget = (id) => {
3897
+ setSelected(id);
3898
+ setChecked(false);
3899
+ };
3900
+ const submit = () => {
3901
+ if (!selected || checked) return;
3902
+ setChecked(true);
3903
+ assessment.answer({
3904
+ checkId,
3905
+ interactionType: INTERACTION6,
3906
+ response: selected,
3907
+ correct
3908
+ });
3909
+ if (correct) {
3910
+ assessment.complete({
3911
+ checkId,
3912
+ interactionType: INTERACTION6,
3913
+ score: 1,
3914
+ maxScore: 1,
3915
+ passingScore: props.passingScore ?? 1
3916
+ });
3917
+ }
3918
+ };
3919
+ return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
3920
+ /* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
3921
+ /* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3922
+ props.targets.map((t) => /* @__PURE__ */ jsx26(
3923
+ "button",
3924
+ {
3925
+ type: "button",
3926
+ "aria-label": t.label,
3927
+ "aria-pressed": selected === t.id,
3928
+ "data-testid": `target-${t.id}`,
1935
3929
  style: {
1936
- display: "inline-block",
1937
- minWidth: "6em",
1938
- border: "1px dashed currentColor",
1939
- padding: "0.2em 0.5em",
1940
- margin: "0 0.2em"
3930
+ position: "absolute",
3931
+ left: `${t.x}%`,
3932
+ top: `${t.y}%`,
3933
+ transform: "translate(-50%, -50%)"
1941
3934
  },
1942
- children: zones[part] || "___"
3935
+ onClick: () => selectTarget(t.id),
3936
+ children: t.label
1943
3937
  },
1944
- part
1945
- );
1946
- }) }),
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
3938
+ t.id
3939
+ ))
3940
+ ] }),
3941
+ /* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
3942
+ checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
1950
3943
  ] });
1951
3944
  }
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 }) });
3945
+ var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
3946
+ var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
3947
+ return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
1955
3948
  });
3949
+ setLessonkitBlockType(FindHotspot, "FindHotspot");
1956
3950
 
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]);
3951
+ // src/blocks/FindMultipleHotspots.tsx
3952
+ import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
3953
+ import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
3954
+ var INTERACTION7 = "findMultipleHotspots";
3955
+ function FindMultipleHotspotsInner(props, ref) {
3956
+ const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3957
+ const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
3958
+ const [checked, setChecked] = useState19(false);
1963
3959
  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;
3960
+ const toggle = (id) => {
3961
+ setSelected((prev) => {
3962
+ const next = new Set(prev);
3963
+ if (next.has(id)) next.delete(id);
3964
+ else next.add(id);
3965
+ return next;
1988
3966
  });
1989
- return {
1990
- getScore: () => score,
1991
- getMaxScore: () => maxScore,
1992
- getAnswerGiven: () => allFilled,
1993
- resetTask: reset,
1994
- showSolutions: () => {
3967
+ setChecked(false);
3968
+ };
3969
+ const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
3970
+ const handle = useMemo17(
3971
+ () => buildAssessmentHandle({
3972
+ checkId,
3973
+ getScore: () => checked && correct ? 1 : 0,
3974
+ getMaxScore: () => 1,
3975
+ getAnswerGiven: () => selected.size > 0,
3976
+ resetTask: () => {
3977
+ setSelected(/* @__PURE__ */ new Set());
3978
+ setChecked(false);
1995
3979
  },
3980
+ showSolutions: () => setSelected(new Set(props.correctTargetIds)),
1996
3981
  getXAPIData: () => ({
1997
3982
  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;
3983
+ interactionType: INTERACTION7,
3984
+ response: [...selected],
3985
+ correct: checked ? correct : void 0,
3986
+ score: checked && correct ? 1 : 0,
3987
+ maxScore: 1
3988
+ }),
3989
+ getCurrentState: () => ({ selected: [...selected], checked }),
3990
+ resume: (state) => {
3991
+ const raw = state.selected;
3992
+ if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
3993
+ readBooleanStateField(state, "checked", setChecked);
3994
+ }
3995
+ }),
3996
+ [checkId, selected, checked, correct, props.correctTargetIds]
3997
+ );
3998
+ useAssessmentHandleRegistration(checkId, handle, ref);
3999
+ const submit = () => {
4000
+ if (selected.size === 0 || checked) return;
4001
+ setChecked(true);
2021
4002
  assessment.answer({
2022
4003
  checkId,
2023
- interactionType: INTERACTION5,
2024
- response: assignments,
2025
- correct: allCorrect
4004
+ interactionType: INTERACTION7,
4005
+ response: [...selected],
4006
+ correct
2026
4007
  });
2027
- if (allCorrect && !completedRef.current) {
2028
- completedRef.current = true;
2029
- setPassed(true);
4008
+ if (correct) {
2030
4009
  assessment.complete({
2031
4010
  checkId,
2032
- interactionType: INTERACTION5,
2033
- score: props.targets.length,
2034
- maxScore: props.targets.length,
2035
- passingScore: props.passingScore ?? props.targets.length
4011
+ interactionType: INTERACTION7,
4012
+ score: 1,
4013
+ maxScore: 1,
4014
+ passingScore: props.passingScore ?? 1
2036
4015
  });
2037
4016
  }
2038
4017
  };
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(
4018
+ return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
4019
+ /* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
4020
+ /* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4021
+ props.targets.map((t) => /* @__PURE__ */ jsx27(
2044
4022
  "button",
2045
4023
  {
2046
4024
  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
4025
+ "aria-label": t.label,
4026
+ "aria-pressed": selected.has(t.id),
4027
+ "data-testid": `target-${t.id}`,
4028
+ style: {
4029
+ position: "absolute",
4030
+ left: `${t.x}%`,
4031
+ top: `${t.y}%`,
4032
+ transform: "translate(-50%, -50%)"
4033
+ },
4034
+ onClick: () => toggle(t.id),
4035
+ children: t.label
2054
4036
  },
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
4037
+ t.id
4038
+ ))
4039
+ ] }),
4040
+ /* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
4041
+ checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
2093
4042
  ] });
2094
4043
  }
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 }) });
2098
- });
2099
-
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]
2117
- );
2118
- if (!sequential) {
2119
- return /* @__PURE__ */ jsx10(AssessmentSequenceProvider, { children: /* @__PURE__ */ jsx10("section", { "aria-label": "Assessment sequence", children: props.children }) });
4044
+ var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
4045
+ var FindMultipleHotspots = forwardRef11(
4046
+ function FindMultipleHotspots2(props, ref) {
4047
+ return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
2120
4048
  }
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(
2132
- "button",
2133
- {
2134
- type: "button",
2135
- "data-testid": "sequence-next",
2136
- disabled: index >= childArray.length - 1,
2137
- onClick: goNext,
2138
- children: "Next"
2139
- }
2140
- )
2141
- ] })
2142
- ] }) });
2143
- }
4049
+ );
4050
+ setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
2144
4051
 
2145
4052
  // src/index.tsx
2146
4053
  import {
@@ -2154,14 +4061,14 @@ import {
2154
4061
  } from "@lessonkit/core";
2155
4062
 
2156
4063
  // src/theme/ThemeProvider.tsx
2157
- import React11, {
2158
- createContext as createContext4,
2159
- useCallback as useCallback4,
2160
- useContext as useContext4,
4064
+ import React29, {
4065
+ createContext as createContext6,
4066
+ useCallback as useCallback10,
4067
+ useContext as useContext8,
2161
4068
  useLayoutEffect as useLayoutEffect2,
2162
- useMemo as useMemo12,
2163
- useRef as useRef9,
2164
- useState as useState9
4069
+ useMemo as useMemo18,
4070
+ useRef as useRef15,
4071
+ useState as useState20
2165
4072
  } from "react";
2166
4073
  import {
2167
4074
  brandThemeOverrides,
@@ -2188,11 +4095,11 @@ function applyCssVariables(target, vars, previousKeys) {
2188
4095
  }
2189
4096
 
2190
4097
  // src/theme/ThemeProvider.tsx
2191
- import { jsx as jsx11 } from "react/jsx-runtime";
2192
- var ThemeContext = createContext4(null);
4098
+ import { jsx as jsx28 } from "react/jsx-runtime";
4099
+ var ThemeContext = createContext6(null);
2193
4100
  var useIsoLayoutEffect2 = (
2194
4101
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
2195
- typeof window !== "undefined" ? useLayoutEffect2 : React11.useEffect
4102
+ typeof window !== "undefined" ? useLayoutEffect2 : React29.useEffect
2196
4103
  );
2197
4104
  function getSystemMode() {
2198
4105
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -2211,7 +4118,7 @@ function ThemeProvider(props) {
2211
4118
  const preset = props.preset ?? "default";
2212
4119
  const mode = props.mode ?? "light";
2213
4120
  const targetKind = props.target ?? "document";
2214
- const [resolvedMode, setResolvedMode] = useState9(
4121
+ const [resolvedMode, setResolvedMode] = useState20(
2215
4122
  () => mode === "system" ? getSystemMode() : mode
2216
4123
  );
2217
4124
  useIsoLayoutEffect2(() => {
@@ -2227,20 +4134,20 @@ function ThemeProvider(props) {
2227
4134
  return () => mq.removeEventListener("change", onChange);
2228
4135
  }, [mode]);
2229
4136
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
2230
- const effectiveTheme = useMemo12(() => {
4137
+ const effectiveTheme = useMemo18(() => {
2231
4138
  const modeBase = resolveModeBase(mode, dataTheme);
2232
4139
  const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
2233
4140
  return mergeThemes(base, props.theme ?? {});
2234
4141
  }, [preset, mode, dataTheme, props.theme]);
2235
- const hostRef = useRef9(null);
2236
- const appliedKeysRef = useRef9(/* @__PURE__ */ new Set());
4142
+ const hostRef = useRef15(null);
4143
+ const appliedKeysRef = useRef15(/* @__PURE__ */ new Set());
2237
4144
  useIsoLayoutEffect2(() => {
2238
4145
  if (targetKind === "document" && typeof document !== "undefined") {
2239
4146
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
2240
4147
  return () => document.documentElement.removeAttribute("data-lk-theme");
2241
4148
  }
2242
4149
  }, [targetKind, dataTheme]);
2243
- const inject = useCallback4(() => {
4150
+ const inject = useCallback10(() => {
2244
4151
  const vars = themeToCssVariables(effectiveTheme);
2245
4152
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
2246
4153
  if (!el) return;
@@ -2257,7 +4164,7 @@ function ThemeProvider(props) {
2257
4164
  appliedKeysRef.current = /* @__PURE__ */ new Set();
2258
4165
  };
2259
4166
  }, [inject, targetKind]);
2260
- const value = useMemo12(
4167
+ const value = useMemo18(
2261
4168
  () => ({
2262
4169
  theme: effectiveTheme,
2263
4170
  preset,
@@ -2267,21 +4174,331 @@ function ThemeProvider(props) {
2267
4174
  [effectiveTheme, preset, mode, dataTheme]
2268
4175
  );
2269
4176
  if (targetKind === "document") {
2270
- return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
4177
+ return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
2271
4178
  }
2272
- return /* @__PURE__ */ jsx11(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx11("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
4179
+ return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
2273
4180
  }
2274
4181
  function useTheme() {
2275
- const ctx = useContext4(ThemeContext);
4182
+ const ctx = useContext8(ThemeContext);
2276
4183
  if (!ctx) {
2277
4184
  throw new Error("useTheme must be used within a ThemeProvider");
2278
4185
  }
2279
4186
  return ctx;
2280
4187
  }
2281
4188
 
4189
+ // src/catalogV3Entries.ts
4190
+ import {
4191
+ ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
4192
+ INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
4193
+ PAGE_ALLOWED_CHILD_TYPES,
4194
+ SLIDE_ALLOWED_CHILD_TYPES,
4195
+ SLIDE_DECK_ALLOWED_CHILD_TYPES,
4196
+ COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
4197
+ } from "@lessonkit/core";
4198
+ var COMPOUND_PARENTS = [
4199
+ "Lesson",
4200
+ "Page",
4201
+ "InteractiveBook",
4202
+ "Slide",
4203
+ "SlideDeck",
4204
+ "AssessmentSequence"
4205
+ ];
4206
+ function extendParents(entry) {
4207
+ if (!entry.parentConstraints?.length) return entry;
4208
+ const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
4209
+ return { ...entry, parentConstraints: [...merged] };
4210
+ }
4211
+ var assessmentBehaviourProps = [
4212
+ { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
4213
+ { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
4214
+ { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
4215
+ { name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
4216
+ ];
4217
+ var v3CompoundAndContentEntries = [
4218
+ {
4219
+ type: "Text",
4220
+ category: "content",
4221
+ description: "Paragraph text content.",
4222
+ props: [
4223
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
4224
+ { name: "children", type: "ReactNode", required: true, description: "Text body." }
4225
+ ],
4226
+ requiredIds: [],
4227
+ parentConstraints: [...COMPOUND_PARENTS],
4228
+ a11y: { element: "p", ariaLabel: "Text", keyboard: "N/A", notes: "Semantic paragraph." },
4229
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
4230
+ telemetry: { emits: [] }
4231
+ },
4232
+ {
4233
+ type: "Heading",
4234
+ category: "content",
4235
+ description: "Heading levels 1\u20133.",
4236
+ props: [
4237
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
4238
+ { name: "level", type: "1 | 2 | 3", required: true, description: "Heading level." },
4239
+ { name: "children", type: "ReactNode", required: true, description: "Heading text." }
4240
+ ],
4241
+ requiredIds: [],
4242
+ parentConstraints: [...COMPOUND_PARENTS],
4243
+ a11y: { element: "h1-h3", ariaLabel: "Heading", keyboard: "N/A", notes: "Use one level per outline." },
4244
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
4245
+ telemetry: { emits: [] }
4246
+ },
4247
+ {
4248
+ type: "Image",
4249
+ category: "content",
4250
+ description: "Image with required alt text.",
4251
+ props: [
4252
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
4253
+ { name: "src", type: "string", required: true, description: "Image URL." },
4254
+ { name: "alt", type: "string", required: true, description: "Alt text." }
4255
+ ],
4256
+ requiredIds: [],
4257
+ parentConstraints: [...COMPOUND_PARENTS],
4258
+ a11y: { element: "img", ariaLabel: "Image", keyboard: "N/A", notes: "Requires alt." },
4259
+ theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
4260
+ telemetry: { emits: [] }
4261
+ },
4262
+ {
4263
+ type: "Page",
4264
+ category: "container",
4265
+ compoundContract: true,
4266
+ h5pMachineName: "H5P.Column",
4267
+ h5pAlias: "Column",
4268
+ description: "Column layout container (H5P Column / Page).",
4269
+ allowedChildTypes: [...PAGE_ALLOWED_CHILD_TYPES],
4270
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Page,
4271
+ props: [
4272
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4273
+ { name: "title", type: "string", required: false, description: "Page title." },
4274
+ { name: "children", type: "ReactNode", required: true, description: "Page content." }
4275
+ ],
4276
+ requiredIds: [],
4277
+ optionalIds: ["blockId"],
4278
+ parentConstraints: ["Lesson", "InteractiveBook"],
4279
+ a11y: { element: "section", ariaLabel: "Page", keyboard: "N/A", notes: "H5P Column equivalent." },
4280
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
4281
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
4282
+ },
4283
+ {
4284
+ type: "InteractiveBook",
4285
+ category: "container",
4286
+ compoundContract: true,
4287
+ h5pMachineName: "H5P.InteractiveBook",
4288
+ h5pAlias: "Interactive Book",
4289
+ description: "Multi-page book with chapter navigation.",
4290
+ allowedChildTypes: [...INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
4291
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.InteractiveBook,
4292
+ props: [
4293
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4294
+ { name: "title", type: "string", required: true, description: "Book title." },
4295
+ { name: "showBookScore", type: "boolean", required: false, description: "Show aggregate score." },
4296
+ { name: "children", type: "Page[]", required: true, description: "Page chapters." }
4297
+ ],
4298
+ requiredIds: ["blockId"],
4299
+ parentConstraints: ["Lesson"],
4300
+ a11y: {
4301
+ element: "section",
4302
+ ariaLabel: "Interactive book",
4303
+ keyboard: "Previous/Next chapter navigation.",
4304
+ notes: "H5P Interactive Book equivalent."
4305
+ },
4306
+ theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
4307
+ telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
4308
+ },
4309
+ {
4310
+ type: "Slide",
4311
+ category: "container",
4312
+ compoundContract: true,
4313
+ h5pMachineName: "H5P.CoursePresentation",
4314
+ h5pAlias: "Course Presentation slide",
4315
+ description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
4316
+ allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
4317
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Slide,
4318
+ props: [
4319
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4320
+ { name: "title", type: "string", required: false, description: "Slide title." },
4321
+ { name: "children", type: "ReactNode", required: true, description: "Slide content." }
4322
+ ],
4323
+ requiredIds: [],
4324
+ optionalIds: ["blockId"],
4325
+ parentConstraints: ["SlideDeck"],
4326
+ a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
4327
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
4328
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
4329
+ },
4330
+ {
4331
+ type: "SlideDeck",
4332
+ category: "container",
4333
+ compoundContract: true,
4334
+ h5pMachineName: "H5P.CoursePresentation",
4335
+ h5pAlias: "Course Presentation",
4336
+ description: "Multi-slide presentation with keyboard navigation.",
4337
+ allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
4338
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.SlideDeck,
4339
+ props: [
4340
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4341
+ { name: "title", type: "string", required: true, description: "Deck title." },
4342
+ { name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
4343
+ { name: "children", type: "Slide[]", required: true, description: "Slides." }
4344
+ ],
4345
+ requiredIds: ["blockId"],
4346
+ parentConstraints: ["Lesson"],
4347
+ a11y: {
4348
+ element: "section",
4349
+ ariaLabel: "Slide deck",
4350
+ keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
4351
+ notes: "H5P Course Presentation equivalent."
4352
+ },
4353
+ theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
4354
+ telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
4355
+ },
4356
+ {
4357
+ type: "Accordion",
4358
+ category: "content",
4359
+ h5pMachineName: "H5P.Accordion",
4360
+ h5pAlias: "Accordion",
4361
+ description: "Expandable sections.",
4362
+ props: [
4363
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4364
+ { name: "sections", type: "AccordionSection[]", required: true, description: "Sections." }
4365
+ ],
4366
+ requiredIds: ["blockId"],
4367
+ parentConstraints: [...COMPOUND_PARENTS],
4368
+ a11y: { element: "section", ariaLabel: "Accordion", keyboard: "Button toggles sections.", notes: "No nested accordions." },
4369
+ theming: { surface: "global-inherit", stylingNotes: "Disclosure pattern." },
4370
+ telemetry: { emits: ["accordion_section_toggled"] }
4371
+ },
4372
+ {
4373
+ type: "DialogCards",
4374
+ category: "content",
4375
+ h5pMachineName: "H5P.Dialogcards",
4376
+ h5pAlias: "Dialog Cards",
4377
+ description: "Flip cards with front/back text.",
4378
+ props: [
4379
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4380
+ { name: "cards", type: "DialogCard[]", required: true, description: "Cards." }
4381
+ ],
4382
+ requiredIds: ["blockId"],
4383
+ parentConstraints: [...COMPOUND_PARENTS],
4384
+ a11y: { element: "section", ariaLabel: "Dialog cards", keyboard: "Flip and navigate cards.", notes: "Reduced motion safe." },
4385
+ theming: { surface: "global-inherit", stylingNotes: "Card flip." },
4386
+ telemetry: { emits: [] }
4387
+ },
4388
+ {
4389
+ type: "Flashcards",
4390
+ category: "content",
4391
+ h5pMachineName: "H5P.Flashcards",
4392
+ h5pAlias: "Flashcards",
4393
+ description: "Study flashcards with optional self-score.",
4394
+ props: [
4395
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4396
+ { name: "cards", type: "Flashcard[]", required: true, description: "Cards." },
4397
+ { name: "selfScore", type: "boolean", required: false, description: "Self-score mode." }
4398
+ ],
4399
+ requiredIds: ["blockId"],
4400
+ parentConstraints: [...COMPOUND_PARENTS],
4401
+ a11y: { element: "section", ariaLabel: "Flashcards", keyboard: "Flip and next.", notes: "Not LMS-scored by default." },
4402
+ theming: { surface: "global-inherit", stylingNotes: "Study mode." },
4403
+ telemetry: { emits: ["flashcard_flipped"] }
4404
+ },
4405
+ {
4406
+ type: "ImageHotspots",
4407
+ category: "content",
4408
+ h5pMachineName: "H5P.ImageHotspots",
4409
+ h5pAlias: "Image Hotspots",
4410
+ description: "Image with clickable hotspot popovers.",
4411
+ props: [
4412
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4413
+ { name: "src", type: "string", required: true, description: "Image URL." },
4414
+ { name: "alt", type: "string", required: true, description: "Alt text." },
4415
+ { name: "hotspots", type: "HotspotSpec[]", required: true, description: "Hotspots." }
4416
+ ],
4417
+ requiredIds: ["blockId"],
4418
+ parentConstraints: [...COMPOUND_PARENTS],
4419
+ a11y: { element: "section", ariaLabel: "Image hotspots", keyboard: "Buttons on image.", notes: "Popover dialog." },
4420
+ theming: { surface: "global-inherit", stylingNotes: "Positioned hotspots." },
4421
+ telemetry: { emits: ["hotspot_opened"] }
4422
+ },
4423
+ {
4424
+ type: "ImageSlider",
4425
+ category: "content",
4426
+ h5pMachineName: "H5P.ImageSlider",
4427
+ h5pAlias: "Image Slider",
4428
+ description: "Carousel of images.",
4429
+ props: [
4430
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4431
+ { name: "slides", type: "ImageSlide[]", required: true, description: "Slides." }
4432
+ ],
4433
+ requiredIds: ["blockId"],
4434
+ parentConstraints: [...COMPOUND_PARENTS],
4435
+ a11y: { element: "section", ariaLabel: "Image slider", keyboard: "Previous/next slide.", notes: "Carousel." },
4436
+ theming: { surface: "global-inherit", stylingNotes: "Slider." },
4437
+ telemetry: { emits: ["image_slider_changed"] }
4438
+ },
4439
+ {
4440
+ type: "FindHotspot",
4441
+ category: "assessment",
4442
+ assessmentContract: true,
4443
+ h5pMachineName: "H5P.ImageHotspotQuestion",
4444
+ h5pAlias: "Find the Hotspot",
4445
+ description: "Select the correct region on an image.",
4446
+ props: [
4447
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4448
+ { name: "src", type: "string", required: true, description: "Image URL." },
4449
+ { name: "alt", type: "string", required: true, description: "Alt text." },
4450
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
4451
+ { name: "correctTargetId", type: "string", required: true, description: "Correct target id." },
4452
+ ...assessmentBehaviourProps
4453
+ ],
4454
+ requiredIds: ["checkId"],
4455
+ parentConstraints: [...COMPOUND_PARENTS],
4456
+ a11y: { element: "section", ariaLabel: "Find the hotspot", keyboard: "Select target buttons.", notes: "Scored." },
4457
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4458
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4459
+ },
4460
+ {
4461
+ type: "FindMultipleHotspots",
4462
+ category: "assessment",
4463
+ assessmentContract: true,
4464
+ h5pMachineName: "H5P.ImageMultipleHotspotQuestion",
4465
+ h5pAlias: "Find Multiple Hotspots",
4466
+ description: "Select all correct regions on an image.",
4467
+ props: [
4468
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4469
+ { name: "src", type: "string", required: true, description: "Image URL." },
4470
+ { name: "alt", type: "string", required: true, description: "Alt text." },
4471
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
4472
+ { name: "correctTargetIds", type: "string[]", required: true, description: "Correct target ids." },
4473
+ ...assessmentBehaviourProps
4474
+ ],
4475
+ requiredIds: ["checkId"],
4476
+ parentConstraints: [...COMPOUND_PARENTS],
4477
+ a11y: { element: "section", ariaLabel: "Find multiple hotspots", keyboard: "Toggle targets.", notes: "Scored." },
4478
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4479
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4480
+ }
4481
+ ];
4482
+ function buildV3CatalogFromV2(v2) {
4483
+ const patched = v2.map((entry) => {
4484
+ const base = extendParents(entry);
4485
+ if (entry.type === "AssessmentSequence") {
4486
+ return {
4487
+ ...base,
4488
+ compoundContract: true,
4489
+ allowedChildTypes: [...ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
4490
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.AssessmentSequence
4491
+ };
4492
+ }
4493
+ return base;
4494
+ });
4495
+ return [...patched, ...v3CompoundAndContentEntries];
4496
+ }
4497
+
2282
4498
  // src/blockCatalog.ts
2283
4499
  var blockCatalogVersion = 1;
2284
4500
  var blockCatalogV2Version = 2;
4501
+ var blockCatalogV3Version = 3;
2285
4502
  var BLOCK_CATALOG = [
2286
4503
  {
2287
4504
  type: "Course",
@@ -2468,7 +4685,7 @@ var BLOCK_CATALOG = [
2468
4685
  }
2469
4686
  }
2470
4687
  ];
2471
- var assessmentBehaviourProps = [
4688
+ var assessmentBehaviourProps2 = [
2472
4689
  { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
2473
4690
  { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
2474
4691
  { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
@@ -2486,7 +4703,7 @@ var v2AssessmentEntries = [
2486
4703
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2487
4704
  { name: "question", type: "string", required: true, description: "Question text." },
2488
4705
  { name: "answer", type: "boolean", required: true, description: "Correct answer." },
2489
- ...assessmentBehaviourProps
4706
+ ...assessmentBehaviourProps2
2490
4707
  ],
2491
4708
  requiredIds: ["checkId"],
2492
4709
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2511,7 +4728,7 @@ var v2AssessmentEntries = [
2511
4728
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2512
4729
  { name: "template", type: "string", required: true, description: "Text with *blank* markers." },
2513
4730
  { name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
2514
- ...assessmentBehaviourProps
4731
+ ...assessmentBehaviourProps2
2515
4732
  ],
2516
4733
  requiredIds: ["checkId"],
2517
4734
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2535,7 +4752,7 @@ var v2AssessmentEntries = [
2535
4752
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2536
4753
  { name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
2537
4754
  { name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
2538
- ...assessmentBehaviourProps
4755
+ ...assessmentBehaviourProps2
2539
4756
  ],
2540
4757
  requiredIds: ["checkId"],
2541
4758
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2559,7 +4776,7 @@ var v2AssessmentEntries = [
2559
4776
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2560
4777
  { name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
2561
4778
  { name: "words", type: "string[]", required: true, description: "Draggable word bank." },
2562
- ...assessmentBehaviourProps
4779
+ ...assessmentBehaviourProps2
2563
4780
  ],
2564
4781
  requiredIds: ["checkId"],
2565
4782
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2583,7 +4800,7 @@ var v2AssessmentEntries = [
2583
4800
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2584
4801
  { name: "text", type: "string", required: true, description: "Source text." },
2585
4802
  { name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
2586
- ...assessmentBehaviourProps
4803
+ ...assessmentBehaviourProps2
2587
4804
  ],
2588
4805
  requiredIds: ["checkId"],
2589
4806
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2605,7 +4822,7 @@ var v2AssessmentEntries = [
2605
4822
  props: [
2606
4823
  { name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
2607
4824
  { name: "sequential", type: "boolean", required: false, description: "One question at a time." },
2608
- ...assessmentBehaviourProps.filter((p) => p.name !== "passingScore")
4825
+ ...assessmentBehaviourProps2.filter((p) => p.name !== "passingScore")
2609
4826
  ],
2610
4827
  requiredIds: [],
2611
4828
  parentConstraints: ["Lesson"],
@@ -2623,6 +4840,7 @@ var BLOCK_CATALOG_V2 = [
2623
4840
  ...BLOCK_CATALOG,
2624
4841
  ...v2AssessmentEntries
2625
4842
  ];
4843
+ var BLOCK_CATALOG_V3 = buildV3CatalogFromV2(BLOCK_CATALOG_V2);
2626
4844
  function cloneCatalogEntry(entry) {
2627
4845
  return {
2628
4846
  ...entry,
@@ -2630,6 +4848,7 @@ function cloneCatalogEntry(entry) {
2630
4848
  aliases: entry.aliases ? [...entry.aliases] : void 0,
2631
4849
  optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
2632
4850
  parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
4851
+ allowedChildTypes: entry.allowedChildTypes ? [...entry.allowedChildTypes] : void 0,
2633
4852
  a11y: { ...entry.a11y },
2634
4853
  theming: {
2635
4854
  ...entry.theming,
@@ -2642,34 +4861,50 @@ function cloneCatalogEntry(entry) {
2642
4861
  };
2643
4862
  }
2644
4863
  function buildBlockCatalog(opts) {
2645
- const version = opts?.version ?? 2;
2646
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4864
+ const version = opts?.version ?? 3;
4865
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2647
4866
  return source.map((entry) => cloneCatalogEntry(entry));
2648
4867
  }
2649
4868
  function getBlockCatalogEntry(type, opts) {
2650
- const version = opts?.version ?? 2;
2651
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4869
+ const version = opts?.version ?? 3;
4870
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2652
4871
  return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
2653
4872
  }
2654
4873
  export {
4874
+ Accordion,
2655
4875
  AssessmentSequence,
2656
4876
  BLOCK_CATALOG,
2657
4877
  BLOCK_CATALOG_V2,
4878
+ BLOCK_CATALOG_V3,
2658
4879
  Course,
4880
+ DialogCards,
2659
4881
  DragAndDrop,
2660
4882
  DragTheWords,
2661
4883
  FillInTheBlanks,
4884
+ FindHotspot,
4885
+ FindMultipleHotspots,
4886
+ Flashcards,
4887
+ Heading,
4888
+ Image,
4889
+ ImageHotspots,
4890
+ ImageSlider,
4891
+ InteractiveBook,
2662
4892
  KnowledgeCheck,
2663
4893
  Lesson,
2664
4894
  LessonkitProvider,
2665
4895
  MarkTheWords,
4896
+ Page,
2666
4897
  ProgressTracker,
2667
4898
  Quiz,
2668
4899
  Reflection,
2669
4900
  Scenario,
4901
+ Slide,
4902
+ SlideDeck,
4903
+ Text,
2670
4904
  ThemeProvider,
2671
4905
  TrueFalse,
2672
4906
  blockCatalogV2Version,
4907
+ blockCatalogV3Version,
2673
4908
  blockCatalogVersion,
2674
4909
  buildBlockCatalog,
2675
4910
  buildTelemetryEvent2 as buildTelemetryEvent,