@lessonkit/react 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,33 +30,47 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ Accordion: () => Accordion,
33
34
  AssessmentSequence: () => AssessmentSequence,
34
35
  BLOCK_CATALOG: () => BLOCK_CATALOG,
35
36
  BLOCK_CATALOG_V2: () => BLOCK_CATALOG_V2,
37
+ BLOCK_CATALOG_V3: () => BLOCK_CATALOG_V3,
36
38
  Course: () => Course,
39
+ DialogCards: () => DialogCards,
37
40
  DragAndDrop: () => DragAndDrop,
38
41
  DragTheWords: () => DragTheWords,
39
42
  FillInTheBlanks: () => FillInTheBlanks,
43
+ FindHotspot: () => FindHotspot,
44
+ FindMultipleHotspots: () => FindMultipleHotspots,
45
+ Flashcards: () => Flashcards,
46
+ Heading: () => Heading,
47
+ Image: () => Image,
48
+ ImageHotspots: () => ImageHotspots,
49
+ ImageSlider: () => ImageSlider,
50
+ InteractiveBook: () => InteractiveBook,
40
51
  KnowledgeCheck: () => KnowledgeCheck,
41
52
  Lesson: () => Lesson,
42
53
  LessonkitProvider: () => LessonkitProvider,
43
54
  MarkTheWords: () => MarkTheWords,
55
+ Page: () => Page,
44
56
  ProgressTracker: () => ProgressTracker,
45
57
  Quiz: () => Quiz,
46
58
  Reflection: () => Reflection,
47
59
  Scenario: () => Scenario,
60
+ Text: () => Text,
48
61
  ThemeProvider: () => ThemeProvider,
49
62
  TrueFalse: () => TrueFalse,
50
63
  blockCatalogV2Version: () => blockCatalogV2Version,
64
+ blockCatalogV3Version: () => blockCatalogV3Version,
51
65
  blockCatalogVersion: () => blockCatalogVersion,
52
66
  buildBlockCatalog: () => buildBlockCatalog,
53
- buildTelemetryEvent: () => import_core10.buildTelemetryEvent,
54
- createLessonkitRuntime: () => import_core10.createLessonkitRuntime,
55
- createPluginRegistry: () => import_core10.createPluginRegistry,
56
- createTelemetryPipeline: () => import_core10.createTelemetryPipeline,
57
- defineAssessmentPlugin: () => import_core10.defineAssessmentPlugin,
58
- defineLifecyclePlugin: () => import_core10.defineLifecyclePlugin,
59
- defineTelemetryPlugin: () => import_core10.defineTelemetryPlugin,
67
+ buildTelemetryEvent: () => import_core17.buildTelemetryEvent,
68
+ createLessonkitRuntime: () => import_core17.createLessonkitRuntime,
69
+ createPluginRegistry: () => import_core17.createPluginRegistry,
70
+ createTelemetryPipeline: () => import_core17.createTelemetryPipeline,
71
+ defineAssessmentPlugin: () => import_core17.defineAssessmentPlugin,
72
+ defineLifecyclePlugin: () => import_core17.defineLifecyclePlugin,
73
+ defineTelemetryPlugin: () => import_core17.defineTelemetryPlugin,
60
74
  getBlockCatalogEntry: () => getBlockCatalogEntry,
61
75
  resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
62
76
  resetQuizWarningsForTests: () => resetQuizWarningsForTests,
@@ -71,31 +85,8 @@ __export(index_exports, {
71
85
  module.exports = __toCommonJS(index_exports);
72
86
 
73
87
  // src/components.tsx
74
- var import_react6 = require("react");
75
- var import_accessibility = require("@lessonkit/accessibility");
76
-
77
- // src/assessment/scoring.ts
78
- function resolvePassingThreshold(passingScore, maxScore) {
79
- return passingScore ?? maxScore;
80
- }
81
- function meetsPassingThreshold(score, maxScore, passingScore) {
82
- const threshold = resolvePassingThreshold(passingScore, maxScore);
83
- return score >= threshold;
84
- }
85
- function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
86
- const maxScore = custom?.maxScore ?? fallbackMax;
87
- if (custom?.passed !== void 0) {
88
- const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
89
- return { score: score2, maxScore, passed: custom.passed };
90
- }
91
- if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
92
- const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
93
- return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
94
- }
95
- const score = fallbackCorrect ? maxScore : 0;
96
- const passed = meetsPassingThreshold(score, maxScore, passingScore);
97
- return { score, maxScore, passed };
98
- }
88
+ var import_react11 = require("react");
89
+ var import_accessibility2 = require("@lessonkit/accessibility");
99
90
 
100
91
  // src/context.tsx
101
92
  var import_react2 = require("react");
@@ -103,7 +94,39 @@ var import_react2 = require("react");
103
94
  // src/provider/useLessonkitProviderRuntime.ts
104
95
  var import_react = require("react");
105
96
  var import_core8 = require("@lessonkit/core");
106
- var import_xapi4 = require("@lessonkit/xapi");
97
+
98
+ // src/runtime/observability.ts
99
+ var import_xapi = require("@lessonkit/xapi");
100
+ function createXapiQueueFromObservability(observability) {
101
+ const opts = {};
102
+ if (observability?.onXapiQueueDepth) {
103
+ opts.onDepth = observability.onXapiQueueDepth;
104
+ }
105
+ if (observability?.onXapiQueueCap) {
106
+ opts.onCap = observability.onXapiQueueCap;
107
+ }
108
+ return (0, import_xapi.createInMemoryXAPIQueue)(opts);
109
+ }
110
+ function wrapTrackingSink(sink, observability) {
111
+ if (!sink || !observability?.onTelemetrySinkError) return sink;
112
+ const onError = observability.onTelemetrySinkError;
113
+ return ((event) => {
114
+ try {
115
+ const result = sink(event);
116
+ if (result != null && typeof result.catch === "function") {
117
+ return result.catch((err) => {
118
+ onError(err, { sinkId: "tracking" });
119
+ });
120
+ }
121
+ return result;
122
+ } catch (err) {
123
+ onError(err, { sinkId: "tracking" });
124
+ return void 0;
125
+ }
126
+ });
127
+ }
128
+
129
+ // src/provider/useLessonkitProviderRuntime.ts
107
130
  var import_xapi5 = require("@lessonkit/xapi");
108
131
 
109
132
  // src/runtime/emitTelemetry.ts
@@ -111,11 +134,20 @@ var import_core2 = require("@lessonkit/core");
111
134
 
112
135
  // src/runtime/telemetryPipeline.ts
113
136
  var import_core = require("@lessonkit/core");
114
- var import_xapi = require("@lessonkit/xapi");
137
+ var import_xapi2 = require("@lessonkit/xapi");
115
138
 
116
139
  // src/runtime/lxpackBridge.ts
117
140
  var import_bridge = require("@lessonkit/lxpack/bridge");
118
- function forwardTelemetryToLxpack(event, mode = "auto") {
141
+ var BRIDGE_MISS_EVENT_NAMES = /* @__PURE__ */ new Set([
142
+ "course_completed",
143
+ "lesson_completed",
144
+ "assessment_completed",
145
+ "quiz_completed"
146
+ ]);
147
+ function forwardTelemetryToLxpack(event, mode = "auto", opts) {
148
+ if (mode === "auto" && opts?.onBridgeMiss && BRIDGE_MISS_EVENT_NAMES.has(event.name) && !(0, import_bridge.getLxpackBridge)()) {
149
+ opts.onBridgeMiss(event);
150
+ }
119
151
  (0, import_bridge.forwardTelemetryToBridge)(event, mode);
120
152
  }
121
153
 
@@ -131,7 +163,7 @@ function createLegacyPipeline(opts, extraSinks = []) {
131
163
  id: "xapi",
132
164
  emit(event) {
133
165
  try {
134
- const statement = (0, import_xapi.telemetryEventToXAPIStatement)(event);
166
+ const statement = (0, import_xapi2.telemetryEventToXAPIStatement)(event);
135
167
  if (statement) opts.xapi?.send(statement);
136
168
  } catch (err) {
137
169
  if (isDevEnvironment()) {
@@ -146,7 +178,9 @@ function createLegacyPipeline(opts, extraSinks = []) {
146
178
  {
147
179
  id: "lxpack-bridge",
148
180
  emit(event) {
149
- forwardTelemetryToLxpack(event, opts.lxpackBridge);
181
+ forwardTelemetryToLxpack(event, opts.lxpackBridge, {
182
+ onBridgeMiss: opts.onLxpackBridgeMiss
183
+ });
150
184
  }
151
185
  },
152
186
  ...extraSinks
@@ -173,7 +207,8 @@ function emitTelemetry(tracking, xapi, event, opts) {
173
207
  const legacy = {
174
208
  tracking,
175
209
  xapi,
176
- lxpackBridge: opts?.lxpackBridge ?? "auto"
210
+ lxpackBridge: opts?.lxpackBridge ?? "auto",
211
+ onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
177
212
  };
178
213
  emitThroughPipeline(event, legacy, opts?.extraSinks);
179
214
  }
@@ -185,14 +220,14 @@ var import_core3 = require("@lessonkit/core");
185
220
  var import_core4 = require("@lessonkit/core");
186
221
 
187
222
  // src/runtime/xapi.ts
188
- var import_xapi2 = require("@lessonkit/xapi");
223
+ var import_xapi3 = require("@lessonkit/xapi");
189
224
  function createXapiClientFromConfig(config, queue) {
190
225
  if (config.xapi?.enabled === false) return null;
191
226
  if (config.xapi?.client) return config.xapi.client;
192
227
  if (!config.courseId) return null;
193
228
  const hasTransport = typeof config.xapi?.transport === "function";
194
229
  if (!hasTransport && config.xapi?.enabled !== true) return null;
195
- return (0, import_xapi2.createXAPIClient)({
230
+ return (0, import_xapi3.createXAPIClient)({
196
231
  courseId: config.courseId,
197
232
  transport: config.xapi?.transport,
198
233
  queue
@@ -203,7 +238,7 @@ function createXapiClientFromConfig(config, queue) {
203
238
  var import_core5 = require("@lessonkit/core");
204
239
 
205
240
  // src/runtime/courseStartedPipeline.ts
206
- var import_xapi3 = require("@lessonkit/xapi");
241
+ var import_xapi4 = require("@lessonkit/xapi");
207
242
  function isDevEnvironment3() {
208
243
  const g = globalThis;
209
244
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
@@ -240,13 +275,15 @@ async function emitExtraSinks(sinks, event, emitCtx) {
240
275
  async function emitCourseStartedNonTrackingPipeline(opts) {
241
276
  let xapiStatementSent = false;
242
277
  if (!opts.skipXapi && opts.xapi) {
243
- const statement = (0, import_xapi3.telemetryEventToXAPIStatement)(opts.event);
278
+ const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(opts.event);
244
279
  if (statement) {
245
280
  opts.xapi.send(statement);
246
281
  xapiStatementSent = true;
247
282
  }
248
283
  }
249
- forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
284
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
285
+ onBridgeMiss: opts.onLxpackBridgeMiss
286
+ });
250
287
  const emitCtx = {
251
288
  courseId: opts.event.courseId,
252
289
  sessionId: opts.event.sessionId,
@@ -263,50 +300,19 @@ function createReactPluginHost(plugins) {
263
300
  return (0, import_core6.createPluginRegistry)(plugins);
264
301
  }
265
302
  function buildPluginContext(opts) {
266
- return {
267
- courseId: opts.courseId,
268
- sessionId: opts.sessionId,
269
- attemptId: opts.attemptId,
270
- user: opts.user
271
- };
303
+ return (0, import_core6.buildPluginContext)(opts);
272
304
  }
273
305
  function emitTelemetryWithPlugins(opts) {
274
306
  const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
275
307
  if (next === null) return;
276
308
  emitTelemetry(opts.tracking, opts.xapi, next, {
277
309
  lxpackBridge: opts.lxpackBridge ?? "auto",
278
- extraSinks: opts.extraSinks
279
- });
280
- }
281
-
282
- // src/runtime/telemetry.ts
283
- var import_core7 = require("@lessonkit/core");
284
- function createTrackingClientFromConfig(config) {
285
- if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
286
- if (config.tracking?.createClient) return config.tracking.createClient();
287
- return (0, import_core7.createTrackingClient)({
288
- sink: config.tracking?.sink,
289
- batchSink: config.tracking?.batchSink,
290
- batch: config.tracking?.batch
310
+ extraSinks: opts.extraSinks,
311
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss
291
312
  });
292
313
  }
293
- async function disposeTrackingClient(client) {
294
- try {
295
- await client?.flush?.();
296
- } catch {
297
- }
298
- try {
299
- await client?.dispose?.();
300
- } catch {
301
- }
302
- }
303
314
 
304
- // src/provider/useLessonkitProviderRuntime.ts
305
- var useIsoLayoutEffect = (
306
- /* v8 ignore next -- SSR uses useEffect when window is unavailable */
307
- typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect
308
- );
309
- var defaultStorage = (0, import_core3.createSessionStoragePort)();
315
+ // src/provider/courseStarted/emit.ts
310
316
  var courseStartedTrackingFlightKey = null;
311
317
  function isTrackingActive(tracking) {
312
318
  return tracking?.enabled !== false;
@@ -361,6 +367,7 @@ async function emitCourseStartedPipelineOnly(opts) {
361
367
  event: opts.event,
362
368
  xapi: opts.xapi,
363
369
  lxpackBridge: opts.lxpackBridge,
370
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
364
371
  extraSinks: opts.extraSinks,
365
372
  skipXapi: opts.skipXapi
366
373
  });
@@ -413,6 +420,7 @@ async function emitCourseStartedToTrackingOnly(opts) {
413
420
  event,
414
421
  xapi: null,
415
422
  lxpackBridge: opts.lxpackBridge,
423
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
416
424
  extraSinks: opts.extraSinks,
417
425
  skipXapi: true
418
426
  });
@@ -466,6 +474,35 @@ function assertTrackingSinkConfig(tracking) {
466
474
  "[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
467
475
  );
468
476
  }
477
+
478
+ // src/runtime/telemetry.ts
479
+ var import_core7 = require("@lessonkit/core");
480
+ function createTrackingClientFromConfig(config) {
481
+ if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
482
+ if (config.tracking?.createClient) return config.tracking.createClient();
483
+ return (0, import_core7.createTrackingClient)({
484
+ sink: config.tracking?.sink,
485
+ batchSink: config.tracking?.batchSink,
486
+ batch: config.tracking?.batch
487
+ });
488
+ }
489
+ async function disposeTrackingClient(client) {
490
+ try {
491
+ await client?.flush?.();
492
+ } catch {
493
+ }
494
+ try {
495
+ await client?.dispose?.();
496
+ } catch {
497
+ }
498
+ }
499
+
500
+ // src/provider/useLessonkitProviderRuntime.ts
501
+ var useIsoLayoutEffect = (
502
+ /* v8 ignore next -- SSR uses useEffect when window is unavailable */
503
+ typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect
504
+ );
505
+ var defaultStorage = (0, import_core3.createSessionStoragePort)();
469
506
  function useLessonkitProviderRuntime(config) {
470
507
  const normalizedCourseId = (0, import_react.useMemo)(
471
508
  () => (0, import_core8.assertValidId)(config.courseId, "courseId"),
@@ -476,6 +513,14 @@ function useLessonkitProviderRuntime(config) {
476
513
  [config, normalizedCourseId]
477
514
  );
478
515
  const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
516
+ (0, import_react.useEffect)(() => {
517
+ if (useV2Runtime) return;
518
+ const g = globalThis;
519
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
520
+ console.warn(
521
+ '[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
522
+ );
523
+ }, [useV2Runtime]);
479
524
  const extraSinksRef = (0, import_react.useRef)(normalizedConfig.sinks);
480
525
  extraSinksRef.current = normalizedConfig.sinks;
481
526
  const headlessRef = (0, import_react.useRef)(null);
@@ -494,7 +539,16 @@ function useLessonkitProviderRuntime(config) {
494
539
  courseIdRef.current = normalizedCourseId;
495
540
  const lxpackBridgeModeRef = (0, import_react.useRef)(normalizedConfig.lxpack?.bridge ?? "auto");
496
541
  lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
497
- const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
542
+ const observabilityRef = (0, import_react.useRef)(normalizedConfig.observability);
543
+ observabilityRef.current = normalizedConfig.observability;
544
+ const onLxpackBridgeMiss = (0, import_react.useCallback)((event) => {
545
+ observabilityRef.current?.onLxpackBridgeMiss?.(event);
546
+ }, []);
547
+ const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
548
+ const pluginHost = (0, import_react.useMemo)(
549
+ () => createReactPluginHost(normalizedConfig.plugins),
550
+ [pluginsFingerprint]
551
+ );
498
552
  const pluginHostRef = (0, import_react.useRef)(pluginHost);
499
553
  pluginHostRef.current = pluginHost;
500
554
  const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
@@ -510,7 +564,8 @@ function useLessonkitProviderRuntime(config) {
510
564
  headlessRef.current = (0, import_core8.createLessonkitRuntime)({
511
565
  courseId: normalizedCourseId,
512
566
  runtimeVersion: "v2",
513
- session: normalizedConfig.session
567
+ session: normalizedConfig.session,
568
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
514
569
  });
515
570
  progressRef.current = headlessRef.current.progress;
516
571
  } else {
@@ -524,7 +579,8 @@ function useLessonkitProviderRuntime(config) {
524
579
  headlessRef.current = (0, import_core8.createLessonkitRuntime)({
525
580
  courseId: normalizedCourseId,
526
581
  runtimeVersion: "v2",
527
- session: normalizedConfig.session
582
+ session: normalizedConfig.session,
583
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
528
584
  });
529
585
  }
530
586
  if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
@@ -548,7 +604,7 @@ function useLessonkitProviderRuntime(config) {
548
604
  }, []);
549
605
  const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
550
606
  activeLessonIdRef.current = progress.activeLessonId;
551
- const xapiQueueRef = (0, import_react.useRef)((0, import_xapi4.createInMemoryXAPIQueue)());
607
+ const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(normalizedConfig.observability));
552
608
  const xapiRef = (0, import_react.useRef)(null);
553
609
  const [xapi, setXapi] = (0, import_react.useState)(null);
554
610
  const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
@@ -569,7 +625,7 @@ function useLessonkitProviderRuntime(config) {
569
625
  }
570
626
  void xapiRef.current?.flush();
571
627
  }
572
- xapiQueueRef.current = (0, import_xapi4.createInMemoryXAPIQueue)();
628
+ xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
573
629
  prevXapiCourseIdRef.current = courseId;
574
630
  xapiCourseStartedSentOnClientRef.current = false;
575
631
  }
@@ -648,7 +704,7 @@ function useLessonkitProviderRuntime(config) {
648
704
  );
649
705
  useIsoLayoutEffect(() => {
650
706
  const prev = trackingRef.current;
651
- const baseSink = normalizedConfig.tracking?.sink;
707
+ const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
652
708
  const userBatchSink = normalizedConfig.tracking?.batchSink;
653
709
  assertTrackingSinkConfig(normalizedConfig.tracking);
654
710
  const sink = pluginHostRef.current && baseSink ? (
@@ -698,6 +754,7 @@ function useLessonkitProviderRuntime(config) {
698
754
  attemptId: attemptIdRef.current,
699
755
  user: userRef.current,
700
756
  lxpackBridge: lxpackBridgeModeRef.current,
757
+ onLxpackBridgeMiss,
701
758
  extraSinks: extraSinksRef.current,
702
759
  skipXapi: xapiCourseStartedSentOnClientRef.current,
703
760
  onXapiStatementSent: () => {
@@ -739,9 +796,10 @@ function useLessonkitProviderRuntime(config) {
739
796
  user: userRef.current
740
797
  }),
741
798
  lxpackBridge: lxpackBridgeModeRef.current,
799
+ onLxpackBridgeMiss,
742
800
  extraSinks: extraSinksRef.current
743
801
  });
744
- }, []);
802
+ }, [onLxpackBridgeMiss]);
745
803
  const emitLifecycleEvent = (0, import_react.useCallback)(
746
804
  (name, data, lessonId) => {
747
805
  const event = (0, import_core2.tryBuildTelemetryEvent)({
@@ -797,12 +855,13 @@ function useLessonkitProviderRuntime(config) {
797
855
  attemptId: attemptIdRef.current,
798
856
  user: userRef.current,
799
857
  lxpackBridge: lxpackBridgeModeRef.current,
858
+ onLxpackBridgeMiss,
800
859
  extraSinks: extraSinksRef.current
801
860
  });
802
861
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
803
862
  }
804
863
  })();
805
- }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
864
+ }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
806
865
  const emitLessonCompleted = (0, import_react.useCallback)(
807
866
  (lessonId, durationMs) => {
808
867
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -851,6 +910,22 @@ function useLessonkitProviderRuntime(config) {
851
910
  })();
852
911
  };
853
912
  }, []);
913
+ (0, import_react.useEffect)(() => {
914
+ if (typeof document === "undefined") return;
915
+ const flushOnExit = () => {
916
+ void xapiRef.current?.flush();
917
+ void trackingRef.current?.flush?.();
918
+ };
919
+ const onVisibilityChange = () => {
920
+ if (document.visibilityState === "hidden") flushOnExit();
921
+ };
922
+ document.addEventListener("visibilitychange", onVisibilityChange);
923
+ window.addEventListener("pagehide", flushOnExit);
924
+ return () => {
925
+ document.removeEventListener("visibilitychange", onVisibilityChange);
926
+ window.removeEventListener("pagehide", flushOnExit);
927
+ };
928
+ }, []);
854
929
  const setActiveLesson = (0, import_react.useCallback)(
855
930
  (lessonId) => {
856
931
  if (useV2Runtime && headlessRef.current) {
@@ -914,20 +989,34 @@ function useLessonkitProviderRuntime(config) {
914
989
  session: normalizedConfig.session
915
990
  });
916
991
  }
917
- }, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
992
+ }, [
993
+ useV2Runtime,
994
+ normalizedCourseId,
995
+ sessionAttemptId,
996
+ sessionConfiguredId,
997
+ sessionUserKey,
998
+ normalizedConfig.session
999
+ ]);
1000
+ (0, import_react.useEffect)(() => {
1001
+ if (!useV2Runtime || !headlessRef.current) return;
1002
+ headlessRef.current.updateConfig({
1003
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
1004
+ });
1005
+ }, [useV2Runtime, pluginHost]);
918
1006
  (0, import_react.useEffect)(() => {
919
- if (!pluginHost) return;
1007
+ const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
1008
+ if (!host) return;
920
1009
  const ctx = buildPluginContext({
921
1010
  courseId: courseIdRef.current,
922
1011
  sessionId: sessionIdRef.current,
923
1012
  attemptId: attemptIdRef.current,
924
1013
  user: userRef.current
925
1014
  });
926
- pluginHost.setupAll(ctx);
1015
+ host.setupAll(ctx);
927
1016
  return () => {
928
- pluginHost.disposeAll();
1017
+ host.disposeAll();
929
1018
  };
930
- }, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
1019
+ }, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
931
1020
  (0, import_react.useEffect)(() => {
932
1021
  const nextConfigured = normalizedConfig.session?.sessionId;
933
1022
  const prevConfigured = prevConfiguredSessionIdRef.current;
@@ -1090,195 +1179,347 @@ function getLessonMountCount(lessonId) {
1090
1179
  return mountCounts.get(lessonId) ?? 0;
1091
1180
  }
1092
1181
 
1093
- // src/components.tsx
1182
+ // src/components/Quiz.tsx
1183
+ var import_react10 = require("react");
1184
+ var import_accessibility = require("@lessonkit/accessibility");
1185
+
1186
+ // src/assessment/AssessmentLessonGuard.tsx
1187
+ var import_react6 = require("react");
1094
1188
  var import_jsx_runtime2 = require("react/jsx-runtime");
1095
- var warnedQuizOutsideLesson = false;
1096
- function resetQuizWarningsForTests() {
1097
- warnedQuizOutsideLesson = false;
1098
- }
1099
- function Course(props) {
1100
- const courseId = (0, import_react6.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1101
- const providerConfig = (0, import_react6.useMemo)(
1102
- () => ({ ...props.config, courseId }),
1103
- [props.config, courseId]
1104
- );
1105
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": props.title, children: [
1106
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { children: props.title }),
1107
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
1108
- ] }) });
1109
- }
1110
- function Lesson(props) {
1111
- const lessonId = (0, import_react6.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1112
- const autoComplete = props.autoCompleteOnUnmount !== false;
1113
- const { setActiveLesson, config } = useLessonkit();
1114
- const { completeLesson } = useCompletion();
1115
- const lessonMountGenerationRef = (0, import_react6.useRef)(0);
1116
- const liveCourseIdRef = (0, import_react6.useRef)(config.courseId);
1117
- liveCourseIdRef.current = config.courseId;
1118
- (0, import_react6.useEffect)(() => {
1119
- const unregister = registerLessonMount(lessonId);
1120
- const generation = ++lessonMountGenerationRef.current;
1121
- const mountedCourseId = config.courseId;
1122
- let effectSurvivedTick = false;
1123
- queueMicrotask(() => {
1124
- queueMicrotask(() => {
1125
- effectSurvivedTick = true;
1126
- });
1127
- });
1128
- setActiveLesson(lessonId);
1129
- return () => {
1130
- unregister();
1131
- if (getLessonMountCount(lessonId) > 0) {
1132
- return;
1133
- }
1134
- if (!autoComplete) return;
1135
- queueMicrotask(() => {
1136
- if (!effectSurvivedTick) return;
1137
- if (lessonMountGenerationRef.current !== generation) return;
1138
- if (liveCourseIdRef.current !== mountedCourseId) return;
1139
- completeLesson(lessonId, { courseId: mountedCourseId });
1140
- });
1141
- };
1142
- }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1143
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
1144
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: props.title }),
1145
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
1146
- ] }) });
1147
- }
1148
- function Scenario(props) {
1149
- const blockId = (0, import_react6.useMemo)(
1150
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1151
- [props.blockId]
1152
- );
1153
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1154
- }
1155
- function Reflection(props) {
1156
- const blockId = (0, import_react6.useMemo)(
1157
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1158
- [props.blockId]
1159
- );
1160
- const promptId = (0, import_react6.useId)();
1161
- const hintId = (0, import_react6.useId)();
1162
- const [internalValue, setInternalValue] = (0, import_react6.useState)("");
1163
- const isControlled = props.value !== void 0;
1164
- const value = isControlled ? props.value : internalValue;
1165
- const handleChange = (event) => {
1166
- if (!isControlled) setInternalValue(event.target.value);
1167
- props.onChange?.(event.target.value);
1168
- };
1169
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1170
- props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: promptId, children: props.prompt }) : null,
1171
- props.hint ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: hintId, style: import_accessibility.visuallyHiddenStyle, children: props.hint }) : null,
1172
- props.children,
1173
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1174
- "textarea",
1175
- {
1176
- value,
1177
- onChange: handleChange,
1178
- "aria-labelledby": props.prompt ? promptId : void 0,
1179
- "aria-describedby": props.hint ? hintId : void 0,
1180
- "aria-label": props.prompt ? void 0 : "Reflection response"
1181
- }
1182
- )
1183
- ] });
1184
- }
1185
- function KnowledgeCheck(props) {
1186
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1187
- Quiz,
1188
- {
1189
- checkId: props.checkId,
1190
- question: props.question,
1191
- choices: props.choices,
1192
- answer: props.answer,
1193
- passingScore: props.passingScore
1194
- }
1195
- );
1189
+ var warnedAssessmentOutsideLesson = false;
1190
+ function resetAssessmentWarningsForTests() {
1191
+ warnedAssessmentOutsideLesson = false;
1196
1192
  }
1197
- function Quiz(props) {
1193
+ function AssessmentLessonGuard(props) {
1198
1194
  const enclosingLessonId = useEnclosingLessonId();
1199
1195
  const missingLesson = enclosingLessonId === void 0;
1200
1196
  (0, import_react6.useEffect)(() => {
1201
1197
  if (!missingLesson || isDevEnvironment4()) return;
1202
- if (!warnedQuizOutsideLesson) {
1203
- warnedQuizOutsideLesson = true;
1198
+ if (!warnedAssessmentOutsideLesson) {
1199
+ warnedAssessmentOutsideLesson = true;
1204
1200
  console.error(
1205
- "[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
1201
+ `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1206
1202
  );
1207
1203
  }
1208
- }, [missingLesson]);
1204
+ }, [missingLesson, props.blockLabel]);
1209
1205
  if (missingLesson && isDevEnvironment4()) {
1210
- throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1206
+ throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1211
1207
  }
1212
1208
  if (missingLesson) {
1213
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
1209
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
1210
+ props.blockLabel,
1211
+ " must be placed inside a Lesson."
1212
+ ] }) });
1214
1213
  }
1215
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(QuizInner, { ...props, enclosingLessonId });
1214
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.children(enclosingLessonId) });
1216
1215
  }
1217
- function QuizInner(props) {
1218
- const { enclosingLessonId } = props;
1219
- const checkId = (0, import_react6.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1220
- const quiz = useQuizState(enclosingLessonId);
1221
- const { plugins, config, session } = useLessonkit();
1222
- const [selected, setSelected] = (0, import_react6.useState)(null);
1223
- const [selectionCorrect, setSelectionCorrect] = (0, import_react6.useState)(null);
1224
- const [quizPassed, setQuizPassed] = (0, import_react6.useState)(false);
1225
- const completedRef = (0, import_react6.useRef)(false);
1226
- const questionId = (0, import_react6.useId)();
1227
- const choicesKey = props.choices.join("\0");
1228
- (0, import_react6.useEffect)(() => {
1229
- completedRef.current = false;
1230
- setQuizPassed(false);
1231
- setSelected(null);
1232
- setSelectionCorrect(null);
1233
- }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
1234
- const isChoiceCorrect = (choice, custom) => {
1235
- if (!custom) return choice === props.answer;
1236
- if (custom.passed !== void 0) return custom.passed;
1237
- if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1238
- return meetsPassingThreshold(custom.score, custom.maxScore, props.passingScore);
1239
- }
1240
- return choice === props.answer;
1216
+
1217
+ // src/assessment/internal/buildAssessmentHandle.ts
1218
+ function buildAssessmentHandle(opts) {
1219
+ return {
1220
+ getScore: opts.getScore,
1221
+ getMaxScore: opts.getMaxScore,
1222
+ getAnswerGiven: opts.getAnswerGiven,
1223
+ resetTask: opts.resetTask,
1224
+ showSolutions: opts.showSolutions,
1225
+ getXAPIData: opts.getXAPIData,
1226
+ ...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
1227
+ ...opts.resume ? { resume: opts.resume } : {}
1241
1228
  };
1242
- const passed = quizPassed;
1243
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1244
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
1245
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1246
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
1247
- props.choices.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: { display: "block" }, children: [
1248
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1249
- "input",
1250
- {
1251
- type: "radio",
1252
- name: questionId,
1253
- value: c,
1254
- checked: selected === c,
1255
- disabled: passed,
1256
- "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1257
- onChange: () => {
1258
- if (passed) return;
1259
- setSelected(c);
1260
- const pluginCtx = buildPluginContext({
1261
- courseId: config.courseId,
1262
- sessionId: session.sessionId,
1263
- attemptId: session.attemptId,
1264
- user: session.user
1265
- });
1266
- const custom = plugins?.scoreAssessment(
1267
- {
1268
- checkId,
1269
- lessonId: enclosingLessonId,
1270
- response: c
1271
- },
1272
- pluginCtx
1273
- ) ?? null;
1274
- const correct = isChoiceCorrect(c, custom);
1275
- setSelectionCorrect(correct);
1276
- quiz.answer({
1277
- checkId,
1278
- question: props.question,
1279
- choice: c,
1280
- correct
1281
- });
1229
+ }
1230
+
1231
+ // src/assessment/internal/resumeState.ts
1232
+ function readBooleanField(state, key) {
1233
+ const value = state[key];
1234
+ if (value === true || value === false || value === null) return value;
1235
+ return void 0;
1236
+ }
1237
+ function readStringField(state, key) {
1238
+ const value = state[key];
1239
+ if (typeof value === "string" || value === null) return value;
1240
+ return void 0;
1241
+ }
1242
+ function readBooleanStateField(state, key, apply) {
1243
+ const value = state[key];
1244
+ if (typeof value === "boolean") apply(value);
1245
+ }
1246
+
1247
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1248
+ var import_react8 = require("react");
1249
+
1250
+ // src/compound/CompoundProvider.tsx
1251
+ var import_react7 = __toESM(require("react"), 1);
1252
+ var import_core10 = require("@lessonkit/core");
1253
+
1254
+ // src/compound/aggregateScores.ts
1255
+ function aggregateAssessmentScores(handles) {
1256
+ let score = 0;
1257
+ let maxScore = 0;
1258
+ let allAnswered = true;
1259
+ for (const handle of handles) {
1260
+ score += handle.getScore();
1261
+ maxScore += handle.getMaxScore();
1262
+ if (!handle.getAnswerGiven()) allAnswered = false;
1263
+ }
1264
+ return { score, maxScore, allAnswered };
1265
+ }
1266
+
1267
+ // src/compound/resumeChildHandles.ts
1268
+ function resumeChildHandles(handles, childStates, opts) {
1269
+ if (opts?.waitForHandles && handles.size === 0 && Object.keys(childStates).length > 0) {
1270
+ return false;
1271
+ }
1272
+ for (const [checkId, handle] of handles) {
1273
+ const child = childStates[checkId];
1274
+ if (child && handle.resume) handle.resume(child);
1275
+ }
1276
+ return true;
1277
+ }
1278
+
1279
+ // src/compound/CompoundProvider.tsx
1280
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1281
+ var CompoundRegistryContext = (0, import_react7.createContext)(null);
1282
+ var CompoundHandlesVersionContext = (0, import_react7.createContext)(0);
1283
+ function CompoundProvider({
1284
+ children,
1285
+ activePageIndex: _activePageIndex,
1286
+ onActivePageIndexChange: _onActivePageIndexChange
1287
+ }) {
1288
+ const registryRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
1289
+ const [handlesVersion, setHandlesVersion] = (0, import_react7.useState)(0);
1290
+ const register = (0, import_react7.useCallback)((checkId, handle) => {
1291
+ const prev = registryRef.current.get(checkId);
1292
+ registryRef.current.set(checkId, handle);
1293
+ if (prev !== handle) {
1294
+ setHandlesVersion((v) => v + 1);
1295
+ }
1296
+ return () => {
1297
+ if (registryRef.current.get(checkId) === handle) {
1298
+ registryRef.current.delete(checkId);
1299
+ setHandlesVersion((v) => v + 1);
1300
+ }
1301
+ };
1302
+ }, []);
1303
+ const registryValue = (0, import_react7.useMemo)(
1304
+ () => ({
1305
+ register,
1306
+ getHandles: () => registryRef.current
1307
+ }),
1308
+ [register]
1309
+ );
1310
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) });
1311
+ }
1312
+ function useCompoundRegistry() {
1313
+ const registry = (0, import_react7.useContext)(CompoundRegistryContext);
1314
+ const handlesVersion = (0, import_react7.useContext)(CompoundHandlesVersionContext);
1315
+ if (!registry) return null;
1316
+ return { ...registry, handlesVersion };
1317
+ }
1318
+ function useCompoundHandlesVersion() {
1319
+ return (0, import_react7.useContext)(CompoundHandlesVersionContext);
1320
+ }
1321
+ function useRegisterAssessmentHandle(checkId, handle) {
1322
+ const registry = (0, import_react7.useContext)(CompoundRegistryContext);
1323
+ import_react7.default.useEffect(() => {
1324
+ if (!registry || !handle) return;
1325
+ return registry.register(checkId, handle);
1326
+ }, [registry, checkId, handle]);
1327
+ }
1328
+ function useCompoundHandleRef(ref, opts) {
1329
+ const { activePageIndex, setActivePageIndex, getHandles, pageCount } = opts;
1330
+ const setIndexClamped = (0, import_react7.useCallback)(
1331
+ (index) => {
1332
+ const next = pageCount !== void 0 ? (0, import_core10.clampCompoundPageIndex)(index, pageCount) : Math.max(0, Math.floor(index));
1333
+ setActivePageIndex(next);
1334
+ },
1335
+ [pageCount, setActivePageIndex]
1336
+ );
1337
+ (0, import_react7.useImperativeHandle)(
1338
+ ref,
1339
+ () => ({
1340
+ getScore: () => aggregateAssessmentScores(getHandles().values()).score,
1341
+ getMaxScore: () => aggregateAssessmentScores(getHandles().values()).maxScore,
1342
+ getAnswerGiven: () => aggregateAssessmentScores(getHandles().values()).allAnswered,
1343
+ resetTask: () => {
1344
+ for (const handle of getHandles().values()) handle.resetTask();
1345
+ },
1346
+ showSolutions: () => {
1347
+ if (!opts.enableSolutionsButton) return;
1348
+ for (const handle of getHandles().values()) handle.showSolutions();
1349
+ },
1350
+ getCurrentState: () => {
1351
+ const childStates = {};
1352
+ for (const [checkId, handle] of getHandles()) {
1353
+ if (handle.getCurrentState) {
1354
+ childStates[checkId] = handle.getCurrentState();
1355
+ }
1356
+ }
1357
+ return (0, import_core10.createCompoundResumeState)({ activePageIndex, childStates });
1358
+ },
1359
+ resume: (state) => {
1360
+ setIndexClamped(state.activePageIndex);
1361
+ resumeChildHandles(getHandles(), state.childStates);
1362
+ }
1363
+ }),
1364
+ [activePageIndex, setIndexClamped, getHandles, opts.enableSolutionsButton]
1365
+ );
1366
+ }
1367
+
1368
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1369
+ function useAssessmentHandleRegistration(checkId, handle, ref) {
1370
+ (0, import_react8.useImperativeHandle)(ref, () => handle, [handle]);
1371
+ useRegisterAssessmentHandle(checkId, handle);
1372
+ }
1373
+
1374
+ // src/assessment/internal/usePluginScoring.ts
1375
+ var import_react9 = require("react");
1376
+
1377
+ // src/assessment/scoring.ts
1378
+ function resolvePassingThreshold(passingScore, maxScore) {
1379
+ return passingScore ?? maxScore;
1380
+ }
1381
+ function meetsPassingThreshold(score, maxScore, passingScore) {
1382
+ const threshold = resolvePassingThreshold(passingScore, maxScore);
1383
+ return score >= threshold;
1384
+ }
1385
+ function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
1386
+ const maxScore = custom?.maxScore ?? fallbackMax;
1387
+ if (custom?.passed !== void 0) {
1388
+ const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
1389
+ return { score: score2, maxScore, passed: custom.passed };
1390
+ }
1391
+ if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1392
+ const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1393
+ return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
1394
+ }
1395
+ const score = fallbackCorrect ? maxScore : 0;
1396
+ const passed = meetsPassingThreshold(score, maxScore, passingScore);
1397
+ return { score, maxScore, passed };
1398
+ }
1399
+
1400
+ // src/assessment/internal/usePluginScoring.ts
1401
+ function usePluginScoring(checkId, lessonId) {
1402
+ const { plugins, config, session } = useLessonkit();
1403
+ const getPluginScore = (0, import_react9.useCallback)(
1404
+ (response) => {
1405
+ const pluginCtx = buildPluginContext({
1406
+ courseId: config.courseId,
1407
+ sessionId: session.sessionId,
1408
+ attemptId: session.attemptId,
1409
+ user: session.user
1410
+ });
1411
+ return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
1412
+ },
1413
+ [checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
1414
+ );
1415
+ const scoreResponse = (0, import_react9.useCallback)(
1416
+ (response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
1417
+ [getPluginScore]
1418
+ );
1419
+ const isChoiceCorrect = (0, import_react9.useCallback)(
1420
+ (choice, answer, custom, passingScore) => {
1421
+ if (!custom) return choice === answer;
1422
+ if (custom.passed !== void 0) return custom.passed;
1423
+ if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1424
+ return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1425
+ }
1426
+ return choice === answer;
1427
+ },
1428
+ []
1429
+ );
1430
+ return { getPluginScore, scoreResponse, isChoiceCorrect };
1431
+ }
1432
+
1433
+ // src/components/Quiz.tsx
1434
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1435
+ function QuizInner(props, ref) {
1436
+ const { enclosingLessonId } = props;
1437
+ const checkId = (0, import_react10.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1438
+ const quiz = useQuizState(enclosingLessonId);
1439
+ const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
1440
+ const [selected, setSelected] = (0, import_react10.useState)(null);
1441
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react10.useState)(null);
1442
+ const [quizPassed, setQuizPassed] = (0, import_react10.useState)(false);
1443
+ const completedRef = (0, import_react10.useRef)(false);
1444
+ const questionId = (0, import_react10.useId)();
1445
+ const choicesKey = props.choices.join("\0");
1446
+ (0, import_react10.useEffect)(() => {
1447
+ completedRef.current = false;
1448
+ setQuizPassed(false);
1449
+ setSelected(null);
1450
+ setSelectionCorrect(null);
1451
+ }, [checkId, props.answer, props.question, choicesKey]);
1452
+ const passed = quizPassed;
1453
+ const handle = (0, import_react10.useMemo)(
1454
+ () => buildAssessmentHandle({
1455
+ checkId,
1456
+ getScore: () => {
1457
+ const maxScore = 1;
1458
+ if (quizPassed && selected !== null) return maxScore;
1459
+ if (selected === null) return 0;
1460
+ return selectionCorrect ? maxScore : 0;
1461
+ },
1462
+ getMaxScore: () => 1,
1463
+ getAnswerGiven: () => selected !== null,
1464
+ resetTask: () => {
1465
+ completedRef.current = false;
1466
+ setQuizPassed(false);
1467
+ setSelected(null);
1468
+ setSelectionCorrect(null);
1469
+ },
1470
+ showSolutions: () => {
1471
+ },
1472
+ getXAPIData: () => ({
1473
+ checkId,
1474
+ interactionType: "mcq",
1475
+ response: selected ?? void 0,
1476
+ correct: selectionCorrect ?? void 0,
1477
+ score: quizPassed && selected !== null ? 1 : selected === null ? 0 : selectionCorrect ? 1 : 0,
1478
+ maxScore: 1
1479
+ }),
1480
+ getCurrentState: () => ({ selected, selectionCorrect, quizPassed }),
1481
+ resume: (state) => {
1482
+ const nextSelected = readStringField(state, "selected");
1483
+ if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
1484
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1485
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1486
+ setSelectionCorrect(nextCorrect);
1487
+ }
1488
+ readBooleanStateField(state, "quizPassed", (value) => {
1489
+ setQuizPassed(value);
1490
+ completedRef.current = value;
1491
+ });
1492
+ }
1493
+ }),
1494
+ [checkId, quizPassed, selected, selectionCorrect]
1495
+ );
1496
+ useAssessmentHandleRegistration(checkId, handle, ref);
1497
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1498
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { id: questionId, children: props.question }),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1500
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
1501
+ props.choices.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "block" }, children: [
1502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1503
+ "input",
1504
+ {
1505
+ type: "radio",
1506
+ name: questionId,
1507
+ value: c,
1508
+ checked: selected === c,
1509
+ disabled: passed,
1510
+ "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1511
+ onChange: () => {
1512
+ if (passed) return;
1513
+ setSelected(c);
1514
+ const custom = getPluginScore(c);
1515
+ const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
1516
+ setSelectionCorrect(correct);
1517
+ quiz.answer({
1518
+ checkId,
1519
+ question: props.question,
1520
+ choice: c,
1521
+ correct
1522
+ });
1282
1523
  if (correct && !completedRef.current) {
1283
1524
  completedRef.current = true;
1284
1525
  setQuizPassed(true);
@@ -1296,7 +1537,115 @@ function QuizInner(props) {
1296
1537
  c
1297
1538
  ] }, `${questionId}-${i}`))
1298
1539
  ] }),
1299
- selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1540
+ selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1541
+ ] });
1542
+ }
1543
+ var QuizInnerForwarded = (0, import_react10.forwardRef)(QuizInner);
1544
+ var Quiz = (0, import_react10.forwardRef)(function Quiz2(props, ref) {
1545
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1546
+ });
1547
+ function KnowledgeCheck(props) {
1548
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1549
+ Quiz,
1550
+ {
1551
+ checkId: props.checkId,
1552
+ question: props.question,
1553
+ choices: props.choices,
1554
+ answer: props.answer,
1555
+ passingScore: props.passingScore
1556
+ }
1557
+ );
1558
+ }
1559
+ function resetQuizWarningsForTests() {
1560
+ resetAssessmentWarningsForTests();
1561
+ }
1562
+
1563
+ // src/components.tsx
1564
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1565
+ function Course(props) {
1566
+ const courseId = (0, import_react11.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1567
+ const providerConfig = (0, import_react11.useMemo)(
1568
+ () => ({ ...props.config, courseId }),
1569
+ [props.config, courseId]
1570
+ );
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": props.title, children: [
1572
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { children: props.title }),
1573
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: props.children })
1574
+ ] }) });
1575
+ }
1576
+ function Lesson(props) {
1577
+ const lessonId = (0, import_react11.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1578
+ const autoComplete = props.autoCompleteOnUnmount !== false;
1579
+ const { setActiveLesson, config } = useLessonkit();
1580
+ const { completeLesson } = useCompletion();
1581
+ const lessonMountGenerationRef = (0, import_react11.useRef)(0);
1582
+ const liveCourseIdRef = (0, import_react11.useRef)(config.courseId);
1583
+ liveCourseIdRef.current = config.courseId;
1584
+ (0, import_react11.useEffect)(() => {
1585
+ const unregister = registerLessonMount(lessonId);
1586
+ const generation = ++lessonMountGenerationRef.current;
1587
+ const mountedCourseId = config.courseId;
1588
+ let effectSurvivedTick = false;
1589
+ queueMicrotask(() => {
1590
+ queueMicrotask(() => {
1591
+ effectSurvivedTick = true;
1592
+ });
1593
+ });
1594
+ setActiveLesson(lessonId);
1595
+ return () => {
1596
+ unregister();
1597
+ if (getLessonMountCount(lessonId) > 0) {
1598
+ return;
1599
+ }
1600
+ if (!autoComplete) return;
1601
+ queueMicrotask(() => {
1602
+ if (!effectSurvivedTick) return;
1603
+ if (lessonMountGenerationRef.current !== generation) return;
1604
+ if (liveCourseIdRef.current !== mountedCourseId) return;
1605
+ completeLesson(lessonId, { courseId: mountedCourseId });
1606
+ });
1607
+ };
1608
+ }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1609
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("article", { "aria-label": props.title, children: [
1610
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { children: props.title }),
1611
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: props.children })
1612
+ ] }) });
1613
+ }
1614
+ function Scenario(props) {
1615
+ const blockId = (0, import_react11.useMemo)(
1616
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1617
+ [props.blockId]
1618
+ );
1619
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1620
+ }
1621
+ function Reflection(props) {
1622
+ const blockId = (0, import_react11.useMemo)(
1623
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1624
+ [props.blockId]
1625
+ );
1626
+ const promptId = (0, import_react11.useId)();
1627
+ const hintId = (0, import_react11.useId)();
1628
+ const [internalValue, setInternalValue] = (0, import_react11.useState)("");
1629
+ const isControlled = props.value !== void 0;
1630
+ const value = isControlled ? props.value : internalValue;
1631
+ const handleChange = (event) => {
1632
+ if (!isControlled) setInternalValue(event.target.value);
1633
+ props.onChange?.(event.target.value);
1634
+ };
1635
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1636
+ props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: promptId, children: props.prompt }) : null,
1637
+ props.hint ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: hintId, style: import_accessibility2.visuallyHiddenStyle, children: props.hint }) : null,
1638
+ props.children,
1639
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1640
+ "textarea",
1641
+ {
1642
+ value,
1643
+ onChange: handleChange,
1644
+ "aria-labelledby": props.prompt ? promptId : void 0,
1645
+ "aria-describedby": props.hint ? hintId : void 0,
1646
+ "aria-label": props.prompt ? void 0 : "Reflection response"
1647
+ }
1648
+ )
1300
1649
  ] });
1301
1650
  }
1302
1651
  function ProgressTracker(props) {
@@ -1305,7 +1654,7 @@ function ProgressTracker(props) {
1305
1654
  if (props.totalLessons != null) {
1306
1655
  const total = props.totalLessons;
1307
1656
  const displayed = Math.min(completed, total);
1308
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1657
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1309
1658
  "div",
1310
1659
  {
1311
1660
  role: "progressbar",
@@ -1313,7 +1662,7 @@ function ProgressTracker(props) {
1313
1662
  "aria-valuemax": total,
1314
1663
  "aria-valuenow": displayed,
1315
1664
  "aria-label": "Lessons completed",
1316
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
1665
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
1317
1666
  "Lessons completed: ",
1318
1667
  displayed,
1319
1668
  " of ",
@@ -1322,92 +1671,28 @@ function ProgressTracker(props) {
1322
1671
  }
1323
1672
  ) });
1324
1673
  }
1325
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
1674
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
1326
1675
  "Lessons completed: ",
1327
1676
  completed
1328
1677
  ] }) });
1329
1678
  }
1330
1679
 
1331
1680
  // src/blocks/TrueFalse.tsx
1332
- var import_react9 = __toESM(require("react"), 1);
1333
-
1334
- // src/assessment/AssessmentLessonGuard.tsx
1335
- var import_react7 = require("react");
1336
- var import_jsx_runtime3 = require("react/jsx-runtime");
1337
- var warnedAssessmentOutsideLesson = false;
1338
- function resetAssessmentWarningsForTests() {
1339
- warnedAssessmentOutsideLesson = false;
1340
- }
1341
- function AssessmentLessonGuard(props) {
1342
- const enclosingLessonId = useEnclosingLessonId();
1343
- const missingLesson = enclosingLessonId === void 0;
1344
- (0, import_react7.useEffect)(() => {
1345
- if (!missingLesson || isDevEnvironment4()) return;
1346
- if (!warnedAssessmentOutsideLesson) {
1347
- warnedAssessmentOutsideLesson = true;
1348
- console.error(
1349
- `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1350
- );
1351
- }
1352
- }, [missingLesson, props.blockLabel]);
1353
- if (missingLesson && isDevEnvironment4()) {
1354
- throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1355
- }
1356
- if (missingLesson) {
1357
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { children: [
1358
- props.blockLabel,
1359
- " must be placed inside a Lesson."
1360
- ] }) });
1361
- }
1362
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: props.children(enclosingLessonId) });
1363
- }
1364
-
1365
- // src/assessment/AssessmentSequenceContext.tsx
1366
- var import_react8 = __toESM(require("react"), 1);
1367
- var import_jsx_runtime4 = require("react/jsx-runtime");
1368
- var AssessmentSequenceContext = (0, import_react8.createContext)(null);
1369
- function AssessmentSequenceProvider({ children }) {
1370
- const registryRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
1371
- const register = (0, import_react8.useCallback)((checkId, handle) => {
1372
- registryRef.current.set(checkId, handle);
1373
- return () => {
1374
- registryRef.current.delete(checkId);
1375
- };
1376
- }, []);
1377
- const value = (0, import_react8.useMemo)(
1378
- () => ({
1379
- register,
1380
- getHandles: () => registryRef.current
1381
- }),
1382
- [register]
1383
- );
1384
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AssessmentSequenceContext.Provider, { value, children });
1385
- }
1386
- function useAssessmentSequenceRegistry() {
1387
- return (0, import_react8.useContext)(AssessmentSequenceContext);
1388
- }
1389
- function useRegisterAssessmentHandle(checkId, handle) {
1390
- const ctx = useAssessmentSequenceRegistry();
1391
- import_react8.default.useEffect(() => {
1392
- if (!ctx || !handle) return;
1393
- return ctx.register(checkId, handle);
1394
- }, [ctx, checkId, handle]);
1395
- }
1396
-
1397
- // src/blocks/TrueFalse.tsx
1398
- var import_jsx_runtime5 = require("react/jsx-runtime");
1681
+ var import_react12 = __toESM(require("react"), 1);
1682
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1399
1683
  var INTERACTION = "trueFalse";
1400
1684
  function TrueFalseInner(props, ref) {
1401
1685
  const { enclosingLessonId } = props;
1402
- const checkId = (0, import_react9.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1686
+ const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1403
1687
  const assessment = useAssessmentState(enclosingLessonId);
1404
- const { plugins, config, session } = useLessonkit();
1405
- const [selected, setSelected] = (0, import_react9.useState)(null);
1406
- const [selectionCorrect, setSelectionCorrect] = (0, import_react9.useState)(null);
1407
- const [showSolutions, setShowSolutions] = (0, import_react9.useState)(false);
1408
- const [passed, setPassed] = (0, import_react9.useState)(false);
1409
- const completedRef = (0, import_react9.useRef)(false);
1410
- const questionId = import_react9.default.useId();
1688
+ const { config } = useLessonkit();
1689
+ const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
1690
+ const [selected, setSelected] = (0, import_react12.useState)(null);
1691
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react12.useState)(null);
1692
+ const [showSolutions, setShowSolutions] = (0, import_react12.useState)(false);
1693
+ const [passed, setPassed] = (0, import_react12.useState)(false);
1694
+ const completedRef = (0, import_react12.useRef)(false);
1695
+ const questionId = import_react12.default.useId();
1411
1696
  const reset = () => {
1412
1697
  completedRef.current = false;
1413
1698
  setPassed(false);
@@ -1415,15 +1700,17 @@ function TrueFalseInner(props, ref) {
1415
1700
  setSelectionCorrect(null);
1416
1701
  setShowSolutions(false);
1417
1702
  };
1418
- (0, import_react9.useEffect)(() => {
1703
+ (0, import_react12.useEffect)(() => {
1419
1704
  reset();
1420
1705
  }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1421
- const handle = (0, import_react9.useMemo)(() => {
1422
- const maxScore = 1;
1423
- const score = passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
1424
- return {
1425
- getScore: () => score,
1426
- getMaxScore: () => maxScore,
1706
+ const handle = (0, import_react12.useMemo)(
1707
+ () => buildAssessmentHandle({
1708
+ checkId,
1709
+ getScore: () => {
1710
+ const maxScore = 1;
1711
+ return passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
1712
+ },
1713
+ getMaxScore: () => 1,
1427
1714
  getAnswerGiven: () => selected !== null,
1428
1715
  resetTask: reset,
1429
1716
  showSolutions: () => setShowSolutions(true),
@@ -1432,28 +1719,34 @@ function TrueFalseInner(props, ref) {
1432
1719
  interactionType: INTERACTION,
1433
1720
  response: selected ?? void 0,
1434
1721
  correct: selected === props.answer,
1435
- score,
1436
- maxScore
1437
- })
1438
- };
1439
- }, [checkId, passed, props.answer, selected]);
1440
- (0, import_react9.useImperativeHandle)(ref, () => handle, [handle]);
1441
- useRegisterAssessmentHandle(checkId, handle);
1722
+ score: passed ? 1 : selected === null ? 0 : selected === props.answer ? 1 : 0,
1723
+ maxScore: 1
1724
+ }),
1725
+ getCurrentState: () => ({ selected, selectionCorrect, passed, showSolutions }),
1726
+ resume: (state) => {
1727
+ const nextSelected = readBooleanField(state, "selected");
1728
+ if (nextSelected === true || nextSelected === false || nextSelected === null) {
1729
+ setSelected(nextSelected);
1730
+ }
1731
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1732
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1733
+ setSelectionCorrect(nextCorrect);
1734
+ }
1735
+ readBooleanStateField(state, "passed", (value) => {
1736
+ setPassed(value);
1737
+ completedRef.current = value;
1738
+ });
1739
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
1740
+ }
1741
+ }),
1742
+ [checkId, passed, props.answer, selected, selectionCorrect, showSolutions]
1743
+ );
1744
+ useAssessmentHandleRegistration(checkId, handle, ref);
1442
1745
  const submit = (value) => {
1443
1746
  if (passed && !props.enableRetry) return;
1444
1747
  setSelected(value);
1445
- const pluginCtx = buildPluginContext({
1446
- courseId: config.courseId,
1447
- sessionId: session.sessionId,
1448
- attemptId: session.attemptId,
1449
- user: session.user
1450
- });
1451
- const custom = plugins?.scoreAssessment(
1452
- { checkId, lessonId: enclosingLessonId, response: value },
1453
- pluginCtx
1454
- ) ?? null;
1455
1748
  const correct = value === props.answer;
1456
- const scored = scoreFromCustom(custom, correct, 1, props.passingScore);
1749
+ const scored = scoreResponse(value, correct, 1, props.passingScore);
1457
1750
  setSelectionCorrect(scored.passed);
1458
1751
  assessment.answer({
1459
1752
  checkId,
@@ -1475,12 +1768,12 @@ function TrueFalseInner(props, ref) {
1475
1768
  }
1476
1769
  };
1477
1770
  const reveal = showSolutions || passed && props.enableSolutionsButton;
1478
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1479
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: questionId, children: props.question }),
1480
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1481
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
1482
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
1483
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1771
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1772
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: questionId, children: props.question }),
1773
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1774
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
1775
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
1776
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1484
1777
  "input",
1485
1778
  {
1486
1779
  type: "radio",
@@ -1492,8 +1785,8 @@ function TrueFalseInner(props, ref) {
1492
1785
  ),
1493
1786
  "True"
1494
1787
  ] }),
1495
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "block" }, children: [
1496
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1788
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { style: { display: "block" }, children: [
1789
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1497
1790
  "input",
1498
1791
  {
1499
1792
  type: "radio",
@@ -1506,49 +1799,49 @@ function TrueFalseInner(props, ref) {
1506
1799
  "False"
1507
1800
  ] })
1508
1801
  ] }),
1509
- reveal ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
1802
+ reveal ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { children: [
1510
1803
  "Correct answer: ",
1511
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: props.answer ? "True" : "False" })
1804
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: props.answer ? "True" : "False" })
1512
1805
  ] }) : null,
1513
- selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1514
- props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1515
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1806
+ selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1807
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1808
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1516
1809
  ] });
1517
1810
  }
1518
- var TrueFalseInnerForwarded = (0, import_react9.forwardRef)(TrueFalseInner);
1519
- var TrueFalse = (0, import_react9.forwardRef)(function TrueFalse2(props, ref) {
1520
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1811
+ var TrueFalseInnerForwarded = (0, import_react12.forwardRef)(TrueFalseInner);
1812
+ var TrueFalse = (0, import_react12.forwardRef)(function TrueFalse2(props, ref) {
1813
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1521
1814
  });
1522
1815
 
1523
1816
  // src/blocks/MarkTheWords.tsx
1524
- var import_react10 = __toESM(require("react"), 1);
1525
- var import_jsx_runtime6 = require("react/jsx-runtime");
1817
+ var import_react13 = __toESM(require("react"), 1);
1818
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1526
1819
  var INTERACTION2 = "markTheWords";
1527
1820
  function tokenize(text) {
1528
1821
  return text.split(/(\s+)/).filter((t) => t.length > 0);
1529
1822
  }
1530
1823
  function MarkTheWordsInner(props, ref) {
1531
- const checkId = (0, import_react10.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1824
+ const checkId = (0, import_react13.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1532
1825
  const assessment = useAssessmentState(props.enclosingLessonId);
1533
- const tokens = (0, import_react10.useMemo)(() => tokenize(props.text), [props.text]);
1534
- const correctSet = (0, import_react10.useMemo)(
1826
+ const tokens = (0, import_react13.useMemo)(() => tokenize(props.text), [props.text]);
1827
+ const correctSet = (0, import_react13.useMemo)(
1535
1828
  () => new Set(props.correctWords.map((w) => w.toLowerCase())),
1536
1829
  [props.correctWords]
1537
1830
  );
1538
- const [marked, setMarked] = (0, import_react10.useState)(() => /* @__PURE__ */ new Set());
1539
- const [passed, setPassed] = (0, import_react10.useState)(false);
1540
- const [showSolutions, setShowSolutions] = (0, import_react10.useState)(false);
1541
- const completedRef = (0, import_react10.useRef)(false);
1831
+ const [marked, setMarked] = (0, import_react13.useState)(() => /* @__PURE__ */ new Set());
1832
+ const [passed, setPassed] = (0, import_react13.useState)(false);
1833
+ const [showSolutions, setShowSolutions] = (0, import_react13.useState)(false);
1834
+ const completedRef = (0, import_react13.useRef)(false);
1542
1835
  const reset = () => {
1543
1836
  completedRef.current = false;
1544
1837
  setPassed(false);
1545
1838
  setMarked(/* @__PURE__ */ new Set());
1546
1839
  setShowSolutions(false);
1547
1840
  };
1548
- (0, import_react10.useEffect)(() => {
1841
+ (0, import_react13.useEffect)(() => {
1549
1842
  reset();
1550
1843
  }, [checkId, props.text, props.correctWords.join("\0")]);
1551
- const selectableIndices = (0, import_react10.useMemo)(() => {
1844
+ const selectableIndices = (0, import_react13.useMemo)(() => {
1552
1845
  const indices = [];
1553
1846
  tokens.forEach((t, i) => {
1554
1847
  if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
@@ -1560,11 +1853,11 @@ function MarkTheWordsInner(props, ref) {
1560
1853
  const maxScore = selectableIndices.length;
1561
1854
  const score = allMarked ? maxScore : marked.size;
1562
1855
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1563
- const handle = (0, import_react10.useMemo)(() => {
1564
- const handleMax = maxScore || 1;
1565
- return {
1856
+ const handle = (0, import_react13.useMemo)(
1857
+ () => buildAssessmentHandle({
1858
+ checkId,
1566
1859
  getScore: () => score,
1567
- getMaxScore: () => handleMax,
1860
+ getMaxScore: () => maxScore || 1,
1568
1861
  getAnswerGiven: () => marked.size > 0,
1569
1862
  resetTask: reset,
1570
1863
  showSolutions: () => setShowSolutions(true),
@@ -1574,12 +1867,22 @@ function MarkTheWordsInner(props, ref) {
1574
1867
  response: [...marked].map((i) => tokens[i]),
1575
1868
  correct: passedThreshold,
1576
1869
  score,
1577
- maxScore: handleMax
1578
- })
1579
- };
1580
- }, [checkId, marked, maxScore, passedThreshold, score, tokens]);
1581
- (0, import_react10.useImperativeHandle)(ref, () => handle, [handle]);
1582
- useRegisterAssessmentHandle(checkId, handle);
1870
+ maxScore: maxScore || 1
1871
+ }),
1872
+ getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
1873
+ resume: (state) => {
1874
+ const raw = state.marked;
1875
+ if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
1876
+ readBooleanStateField(state, "passed", (value) => {
1877
+ setPassed(value);
1878
+ completedRef.current = value;
1879
+ });
1880
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
1881
+ }
1882
+ }),
1883
+ [checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
1884
+ );
1885
+ useAssessmentHandleRegistration(checkId, handle, ref);
1583
1886
  const toggle = (index) => {
1584
1887
  if (passed && !props.enableRetry) return;
1585
1888
  setMarked((prev) => {
@@ -1589,7 +1892,7 @@ function MarkTheWordsInner(props, ref) {
1589
1892
  return next;
1590
1893
  });
1591
1894
  };
1592
- (0, import_react10.useEffect)(() => {
1895
+ (0, import_react13.useEffect)(() => {
1593
1896
  if (!hasTargets) {
1594
1897
  if (isDevEnvironment4()) {
1595
1898
  console.warn(
@@ -1629,20 +1932,20 @@ function MarkTheWordsInner(props, ref) {
1629
1932
  score,
1630
1933
  tokens
1631
1934
  ]);
1632
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1633
- !hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { role: "alert", children: [
1935
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1936
+ !hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { role: "alert", children: [
1634
1937
  "No words in this sentence match ",
1635
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { children: "correctWords" }),
1938
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { children: "correctWords" }),
1636
1939
  ". Check spelling and capitalization in the source text."
1637
1940
  ] }) : null,
1638
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
1639
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1941
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
1942
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1640
1943
  const isWord = !/^\s+$/.test(token);
1641
1944
  const isTarget = isWord && correctSet.has(token.toLowerCase());
1642
- if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react10.default.Fragment, { children: token }, i);
1945
+ if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react13.default.Fragment, { children: token }, i);
1643
1946
  const selected = marked.has(i);
1644
1947
  const solution = showSolutions || passed && props.enableSolutionsButton;
1645
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1948
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1646
1949
  "button",
1647
1950
  {
1648
1951
  type: "button",
@@ -1660,49 +1963,59 @@ function MarkTheWordsInner(props, ref) {
1660
1963
  i
1661
1964
  );
1662
1965
  }) }),
1663
- allMarked ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
1664
- props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1665
- props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1966
+ allMarked ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
1967
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1968
+ props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1666
1969
  ] });
1667
1970
  }
1668
- var MarkTheWordsInnerForwarded = (0, import_react10.forwardRef)(MarkTheWordsInner);
1669
- var MarkTheWords = (0, import_react10.forwardRef)(function MarkTheWords2(props, ref) {
1670
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1971
+ var MarkTheWordsInnerForwarded = (0, import_react13.forwardRef)(MarkTheWordsInner);
1972
+ var MarkTheWords = (0, import_react13.forwardRef)(function MarkTheWords2(props, ref) {
1973
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1671
1974
  });
1672
1975
 
1673
1976
  // src/blocks/FillInTheBlanks.tsx
1674
- var import_react11 = __toESM(require("react"), 1);
1675
- var import_jsx_runtime7 = require("react/jsx-runtime");
1676
- var INTERACTION3 = "fillInBlanks";
1677
- function parseTemplate(template) {
1977
+ var import_react14 = __toESM(require("react"), 1);
1978
+
1979
+ // src/assessment/internal/parseStarDelimitedTemplate.ts
1980
+ function parseStarDelimitedTemplate(template, idPrefix) {
1678
1981
  const parts = [];
1679
- const blanks = [];
1982
+ const values = [];
1680
1983
  const re = /\*([^*]+)\*/g;
1681
1984
  let last = 0;
1682
1985
  let match;
1683
1986
  let n = 0;
1684
1987
  while ((match = re.exec(template)) !== null) {
1685
1988
  parts.push(template.slice(last, match.index));
1686
- const id = `blank-${n++}`;
1687
- blanks.push({ id, answer: match[1].trim() });
1688
- parts.push(id);
1989
+ values.push(match[1].trim());
1990
+ parts.push(`${idPrefix}-${n++}`);
1689
1991
  last = match.index + match[0].length;
1690
1992
  }
1691
1993
  parts.push(template.slice(last));
1692
- return { parts, blanks };
1994
+ return { parts, values };
1995
+ }
1996
+
1997
+ // src/blocks/FillInTheBlanks.tsx
1998
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1999
+ var INTERACTION3 = "fillInBlanks";
2000
+ function parseTemplate(template) {
2001
+ const { parts, values } = parseStarDelimitedTemplate(template, "blank");
2002
+ return {
2003
+ parts,
2004
+ blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
2005
+ };
1693
2006
  }
1694
2007
  function FillInTheBlanksInner(props, ref) {
1695
- const checkId = (0, import_react11.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2008
+ const checkId = (0, import_react14.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1696
2009
  const assessment = useAssessmentState(props.enclosingLessonId);
1697
- const parsed = (0, import_react11.useMemo)(() => parseTemplate(props.template), [props.template]);
2010
+ const parsed = (0, import_react14.useMemo)(() => parseTemplate(props.template), [props.template]);
1698
2011
  const blanks = props.blanks ?? parsed.blanks;
1699
- const [values, setValues] = (0, import_react11.useState)(
2012
+ const [values, setValues] = (0, import_react14.useState)(
1700
2013
  () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
1701
2014
  );
1702
- const [passed, setPassed] = (0, import_react11.useState)(false);
1703
- const [showSolutions, setShowSolutions] = (0, import_react11.useState)(false);
1704
- const completedRef = (0, import_react11.useRef)(false);
1705
- const answeredRef = (0, import_react11.useRef)(false);
2015
+ const [passed, setPassed] = (0, import_react14.useState)(false);
2016
+ const [showSolutions, setShowSolutions] = (0, import_react14.useState)(false);
2017
+ const completedRef = (0, import_react14.useRef)(false);
2018
+ const answeredRef = (0, import_react14.useRef)(false);
1706
2019
  const reset = () => {
1707
2020
  completedRef.current = false;
1708
2021
  answeredRef.current = false;
@@ -1710,7 +2023,7 @@ function FillInTheBlanksInner(props, ref) {
1710
2023
  setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
1711
2024
  setShowSolutions(false);
1712
2025
  };
1713
- (0, import_react11.useEffect)(() => {
2026
+ (0, import_react14.useEffect)(() => {
1714
2027
  reset();
1715
2028
  }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
1716
2029
  const hasBlanks = blanks.length > 0;
@@ -1721,11 +2034,11 @@ function FillInTheBlanksInner(props, ref) {
1721
2034
  });
1722
2035
  const maxScore = blanks.length;
1723
2036
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1724
- const handle = (0, import_react11.useMemo)(() => {
1725
- const handleMax = maxScore || 1;
1726
- return {
2037
+ const handle = (0, import_react14.useMemo)(
2038
+ () => buildAssessmentHandle({
2039
+ checkId,
1727
2040
  getScore: () => score,
1728
- getMaxScore: () => handleMax,
2041
+ getMaxScore: () => maxScore || 1,
1729
2042
  getAnswerGiven: () => allFilled,
1730
2043
  resetTask: reset,
1731
2044
  showSolutions: () => setShowSolutions(true),
@@ -1735,12 +2048,23 @@ function FillInTheBlanksInner(props, ref) {
1735
2048
  response: values,
1736
2049
  correct: passedThreshold,
1737
2050
  score,
1738
- maxScore: handleMax
1739
- })
1740
- };
1741
- }, [allFilled, blanks.length, checkId, maxScore, passedThreshold, score, values]);
1742
- (0, import_react11.useImperativeHandle)(ref, () => handle, [handle]);
1743
- useRegisterAssessmentHandle(checkId, handle);
2051
+ maxScore: maxScore || 1
2052
+ }),
2053
+ getCurrentState: () => ({ values, passed, showSolutions }),
2054
+ resume: (state) => {
2055
+ const raw = state.values;
2056
+ if (raw && typeof raw === "object") setValues({ ...raw });
2057
+ readBooleanStateField(state, "passed", (value) => {
2058
+ setPassed(value);
2059
+ completedRef.current = value;
2060
+ answeredRef.current = value;
2061
+ });
2062
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
2063
+ }
2064
+ }),
2065
+ [allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, values]
2066
+ );
2067
+ useAssessmentHandleRegistration(checkId, handle, ref);
1744
2068
  const check = () => {
1745
2069
  if (!hasBlanks) {
1746
2070
  if (isDevEnvironment4()) {
@@ -1771,20 +2095,20 @@ function FillInTheBlanksInner(props, ref) {
1771
2095
  });
1772
2096
  }
1773
2097
  };
1774
- (0, import_react11.useEffect)(() => {
2098
+ (0, import_react14.useEffect)(() => {
1775
2099
  if (!allFilled) answeredRef.current = false;
1776
2100
  }, [allFilled]);
1777
- (0, import_react11.useEffect)(() => {
2101
+ (0, import_react14.useEffect)(() => {
1778
2102
  if (props.autoCheck && allFilled) check();
1779
2103
  }, [allFilled, props.autoCheck, values, passedThreshold]);
1780
2104
  const reveal = showSolutions || passed && props.enableSolutionsButton;
1781
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
1782
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: parsed.parts.map((part, i) => {
2105
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2106
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: parsed.parts.map((part, i) => {
1783
2107
  const blank = blanks.find((b) => b.id === part);
1784
- if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react11.default.Fragment, { children: part }, i);
1785
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
1786
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
1787
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2108
+ if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react14.default.Fragment, { children: part }, i);
2109
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
2110
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
2111
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1788
2112
  "input",
1789
2113
  {
1790
2114
  type: "text",
@@ -1800,52 +2124,40 @@ function FillInTheBlanksInner(props, ref) {
1800
2124
  )
1801
2125
  ] }, blank.id);
1802
2126
  }) }),
1803
- !props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
1804
- !hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
1805
- allFilled ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
1806
- props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1807
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2127
+ !props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2128
+ !hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2129
+ allFilled ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2130
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2131
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1808
2132
  ] });
1809
2133
  }
1810
- var FillInTheBlanksInnerForwarded = (0, import_react11.forwardRef)(FillInTheBlanksInner);
1811
- var FillInTheBlanks = (0, import_react11.forwardRef)(
2134
+ var FillInTheBlanksInnerForwarded = (0, import_react14.forwardRef)(FillInTheBlanksInner);
2135
+ var FillInTheBlanks = (0, import_react14.forwardRef)(
1812
2136
  function FillInTheBlanks2(props, ref) {
1813
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2137
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1814
2138
  }
1815
2139
  );
1816
2140
 
1817
2141
  // src/blocks/DragTheWords.tsx
1818
- var import_react12 = __toESM(require("react"), 1);
1819
- var import_jsx_runtime8 = require("react/jsx-runtime");
2142
+ var import_react15 = __toESM(require("react"), 1);
2143
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1820
2144
  var INTERACTION4 = "dragTheWords";
1821
2145
  function parseZones(template) {
1822
- const parts = [];
1823
- const answers = [];
1824
- const re = /\*([^*]+)\*/g;
1825
- let last = 0;
1826
- let match;
1827
- let n = 0;
1828
- while ((match = re.exec(template)) !== null) {
1829
- parts.push(template.slice(last, match.index));
1830
- answers.push(match[1].trim());
1831
- parts.push(`zone-${n++}`);
1832
- last = match.index + match[0].length;
1833
- }
1834
- parts.push(template.slice(last));
1835
- return { parts, answers };
2146
+ const { parts, values } = parseStarDelimitedTemplate(template, "zone");
2147
+ return { parts, answers: values };
1836
2148
  }
1837
2149
  function DragTheWordsInner(props, ref) {
1838
- const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2150
+ const checkId = (0, import_react15.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1839
2151
  const assessment = useAssessmentState(props.enclosingLessonId);
1840
- const { parts, answers } = (0, import_react12.useMemo)(() => parseZones(props.template), [props.template]);
1841
- const [zones, setZones] = (0, import_react12.useState)(
2152
+ const { parts, answers } = (0, import_react15.useMemo)(() => parseZones(props.template), [props.template]);
2153
+ const [zones, setZones] = (0, import_react15.useState)(
1842
2154
  () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
1843
2155
  );
1844
- const [pool, setPool] = (0, import_react12.useState)(() => [...props.words]);
1845
- const [keyboardWord, setKeyboardWord] = (0, import_react12.useState)(null);
1846
- const [passed, setPassed] = (0, import_react12.useState)(false);
1847
- const completedRef = (0, import_react12.useRef)(false);
1848
- const answeredRef = (0, import_react12.useRef)(false);
2156
+ const [pool, setPool] = (0, import_react15.useState)(() => [...props.words]);
2157
+ const [keyboardWord, setKeyboardWord] = (0, import_react15.useState)(null);
2158
+ const [passed, setPassed] = (0, import_react15.useState)(false);
2159
+ const completedRef = (0, import_react15.useRef)(false);
2160
+ const answeredRef = (0, import_react15.useRef)(false);
1849
2161
  const reset = () => {
1850
2162
  completedRef.current = false;
1851
2163
  answeredRef.current = false;
@@ -1854,7 +2166,7 @@ function DragTheWordsInner(props, ref) {
1854
2166
  setPool([...props.words]);
1855
2167
  setKeyboardWord(null);
1856
2168
  };
1857
- (0, import_react12.useEffect)(() => {
2169
+ (0, import_react15.useEffect)(() => {
1858
2170
  reset();
1859
2171
  }, [checkId, props.template, props.words.join("\0")]);
1860
2172
  const hasZones = answers.length > 0;
@@ -1865,11 +2177,11 @@ function DragTheWordsInner(props, ref) {
1865
2177
  });
1866
2178
  const maxScore = answers.length;
1867
2179
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1868
- const handle = (0, import_react12.useMemo)(() => {
1869
- const handleMax = maxScore || 1;
1870
- return {
2180
+ const handle = (0, import_react15.useMemo)(
2181
+ () => buildAssessmentHandle({
2182
+ checkId,
1871
2183
  getScore: () => score,
1872
- getMaxScore: () => handleMax,
2184
+ getMaxScore: () => maxScore || 1,
1873
2185
  getAnswerGiven: () => allFilled,
1874
2186
  resetTask: reset,
1875
2187
  showSolutions: () => {
@@ -1880,12 +2192,25 @@ function DragTheWordsInner(props, ref) {
1880
2192
  response: zones,
1881
2193
  correct: passedThreshold,
1882
2194
  score,
1883
- maxScore: handleMax
1884
- })
1885
- };
1886
- }, [allFilled, answers.length, checkId, maxScore, passedThreshold, score, zones]);
1887
- (0, import_react12.useImperativeHandle)(ref, () => handle, [handle]);
1888
- useRegisterAssessmentHandle(checkId, handle);
2195
+ maxScore: maxScore || 1
2196
+ }),
2197
+ getCurrentState: () => ({ zones, pool, passed, keyboardWord }),
2198
+ resume: (state) => {
2199
+ const rawZones = state.zones;
2200
+ if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
2201
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2202
+ readBooleanStateField(state, "passed", (value) => {
2203
+ setPassed(value);
2204
+ completedRef.current = value;
2205
+ answeredRef.current = value;
2206
+ });
2207
+ const kw = state.keyboardWord;
2208
+ if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2209
+ }
2210
+ }),
2211
+ [allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, zones]
2212
+ );
2213
+ useAssessmentHandleRegistration(checkId, handle, ref);
1889
2214
  const placeInZone = (zoneId, word) => {
1890
2215
  if (passed && !props.enableRetry) return;
1891
2216
  const prev = zones[zoneId];
@@ -1935,15 +2260,15 @@ function DragTheWordsInner(props, ref) {
1935
2260
  });
1936
2261
  }
1937
2262
  };
1938
- (0, import_react12.useEffect)(() => {
2263
+ (0, import_react15.useEffect)(() => {
1939
2264
  if (!allFilled) answeredRef.current = false;
1940
2265
  }, [allFilled]);
1941
- (0, import_react12.useEffect)(() => {
2266
+ (0, import_react15.useEffect)(() => {
1942
2267
  if (props.autoCheck && allFilled) check();
1943
2268
  }, [allFilled, props.autoCheck, zones, passedThreshold]);
1944
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
1945
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
1946
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2269
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2270
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2271
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1947
2272
  "button",
1948
2273
  {
1949
2274
  type: "button",
@@ -1957,9 +2282,9 @@ function DragTheWordsInner(props, ref) {
1957
2282
  },
1958
2283
  word
1959
2284
  )) }),
1960
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: parts.map((part, i) => {
1961
- if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react12.default.Fragment, { children: part }, i);
1962
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2285
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: parts.map((part, i) => {
2286
+ if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react15.default.Fragment, { children: part }, i);
2287
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1963
2288
  "span",
1964
2289
  {
1965
2290
  role: "button",
@@ -1983,209 +2308,1166 @@ function DragTheWordsInner(props, ref) {
1983
2308
  part
1984
2309
  );
1985
2310
  }) }),
1986
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
1987
- !hasZones ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
1988
- allFilled ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2311
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2312
+ !hasZones ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2313
+ allFilled ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2314
+ ] });
2315
+ }
2316
+ var DragTheWordsInnerForwarded = (0, import_react15.forwardRef)(DragTheWordsInner);
2317
+ var DragTheWords = (0, import_react15.forwardRef)(function DragTheWords2(props, ref) {
2318
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2319
+ });
2320
+
2321
+ // src/blocks/DragAndDrop.tsx
2322
+ var import_react16 = require("react");
2323
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2324
+ var INTERACTION5 = "dragAndDrop";
2325
+ function DragAndDropInner(props, ref) {
2326
+ const checkId = (0, import_react16.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2327
+ const assessment = useAssessmentState(props.enclosingLessonId);
2328
+ const [assignments, setAssignments] = (0, import_react16.useState)(
2329
+ () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2330
+ );
2331
+ const [pool, setPool] = (0, import_react16.useState)(() => props.items.map((i) => i.id));
2332
+ const [keyboardItem, setKeyboardItem] = (0, import_react16.useState)(null);
2333
+ const [passed, setPassed] = (0, import_react16.useState)(false);
2334
+ const completedRef = (0, import_react16.useRef)(false);
2335
+ const reset = () => {
2336
+ completedRef.current = false;
2337
+ setPassed(false);
2338
+ setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2339
+ setPool(props.items.map((i) => i.id));
2340
+ setKeyboardItem(null);
2341
+ };
2342
+ (0, import_react16.useEffect)(() => {
2343
+ reset();
2344
+ }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2345
+ const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2346
+ const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
2347
+ const handle = (0, import_react16.useMemo)(() => {
2348
+ const maxScore = props.targets.length || 1;
2349
+ let score = 0;
2350
+ props.targets.forEach((t) => {
2351
+ if (assignments[t.id] === t.accepts) score += 1;
2352
+ });
2353
+ return buildAssessmentHandle({
2354
+ checkId,
2355
+ getScore: () => score,
2356
+ getMaxScore: () => maxScore,
2357
+ getAnswerGiven: () => allFilled,
2358
+ resetTask: reset,
2359
+ showSolutions: () => {
2360
+ },
2361
+ getXAPIData: () => ({
2362
+ checkId,
2363
+ interactionType: INTERACTION5,
2364
+ response: assignments,
2365
+ correct: allCorrect,
2366
+ score,
2367
+ maxScore
2368
+ }),
2369
+ getCurrentState: () => ({ assignments, pool, passed, keyboardItem }),
2370
+ resume: (state) => {
2371
+ const rawAssignments = state.assignments;
2372
+ if (rawAssignments && typeof rawAssignments === "object") {
2373
+ setAssignments({ ...rawAssignments });
2374
+ }
2375
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2376
+ readBooleanStateField(state, "passed", (value) => {
2377
+ setPassed(value);
2378
+ completedRef.current = value;
2379
+ });
2380
+ const item = state.keyboardItem;
2381
+ if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
2382
+ }
2383
+ });
2384
+ }, [allCorrect, allFilled, assignments, checkId, keyboardItem, passed, pool, props.targets]);
2385
+ useAssessmentHandleRegistration(checkId, handle, ref);
2386
+ const place = (targetId, itemId) => {
2387
+ if (passed && !props.enableRetry) return;
2388
+ const prev = assignments[targetId];
2389
+ setAssignments((a) => ({ ...a, [targetId]: itemId }));
2390
+ setPool((p) => {
2391
+ const next = p.filter((id) => id !== itemId);
2392
+ if (prev) next.push(prev);
2393
+ return next;
2394
+ });
2395
+ setKeyboardItem(null);
2396
+ };
2397
+ const check = () => {
2398
+ if (!allFilled) return;
2399
+ assessment.answer({
2400
+ checkId,
2401
+ interactionType: INTERACTION5,
2402
+ response: assignments,
2403
+ correct: allCorrect
2404
+ });
2405
+ if (allCorrect && !completedRef.current) {
2406
+ completedRef.current = true;
2407
+ setPassed(true);
2408
+ assessment.complete({
2409
+ checkId,
2410
+ interactionType: INTERACTION5,
2411
+ score: props.targets.length,
2412
+ maxScore: props.targets.length,
2413
+ passingScore: props.passingScore ?? props.targets.length
2414
+ });
2415
+ }
2416
+ };
2417
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2418
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2419
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
2420
+ const item = props.items.find((i) => i.id === id);
2421
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2422
+ "button",
2423
+ {
2424
+ type: "button",
2425
+ draggable: true,
2426
+ "data-testid": `drag-item-${id}`,
2427
+ "aria-pressed": keyboardItem === id,
2428
+ onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2429
+ onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2430
+ style: { margin: "0.25rem" },
2431
+ children: item.label
2432
+ },
2433
+ id
2434
+ );
2435
+ }) }),
2436
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { children: props.targets.map((target) => {
2437
+ const assigned = assignments[target.id];
2438
+ const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2439
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { children: [
2440
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: target.label }),
2441
+ " ",
2442
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2443
+ "span",
2444
+ {
2445
+ role: "button",
2446
+ tabIndex: 0,
2447
+ "data-testid": `drop-${target.id}`,
2448
+ onDragOver: (e) => e.preventDefault(),
2449
+ onDrop: (e) => {
2450
+ e.preventDefault();
2451
+ const id = e.dataTransfer.getData("text/plain");
2452
+ if (id) place(target.id, id);
2453
+ },
2454
+ onClick: () => keyboardItem && place(target.id, keyboardItem),
2455
+ onKeyDown: (e) => {
2456
+ if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2457
+ },
2458
+ style: {
2459
+ display: "inline-block",
2460
+ minWidth: "8em",
2461
+ border: "1px dashed currentColor",
2462
+ padding: "0.25em"
2463
+ },
2464
+ children: label
2465
+ }
2466
+ )
2467
+ ] }, target.id);
2468
+ }) }),
2469
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2470
+ allFilled ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
2471
+ ] });
2472
+ }
2473
+ var DragAndDropInnerForwarded = (0, import_react16.forwardRef)(DragAndDropInner);
2474
+ var DragAndDrop = (0, import_react16.forwardRef)(function DragAndDrop2(props, ref) {
2475
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2476
+ });
2477
+
2478
+ // src/blocks/AssessmentSequence.tsx
2479
+ var import_react22 = __toESM(require("react"), 1);
2480
+
2481
+ // src/compound/useCompoundShell.ts
2482
+ var import_react20 = require("react");
2483
+ var import_core14 = require("@lessonkit/core");
2484
+
2485
+ // src/compound/useCompoundNavigation.ts
2486
+ var import_react17 = require("react");
2487
+ function useCompoundNavigation(pageCount, index, setIndex) {
2488
+ const goNext = (0, import_react17.useCallback)(() => {
2489
+ if (pageCount < 1) return;
2490
+ setIndex((i) => Math.min(i + 1, pageCount - 1));
2491
+ }, [pageCount, setIndex]);
2492
+ const goPrev = (0, import_react17.useCallback)(() => {
2493
+ setIndex((i) => Math.max(i - 1, 0));
2494
+ }, [setIndex]);
2495
+ const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
2496
+ return {
2497
+ index: clampedIndex,
2498
+ setIndex,
2499
+ goNext,
2500
+ goPrev,
2501
+ progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
2502
+ };
2503
+ }
2504
+
2505
+ // src/compound/useCompoundPersistence.ts
2506
+ var import_react19 = require("react");
2507
+ var import_core13 = require("@lessonkit/core");
2508
+
2509
+ // src/compound/useCompoundResume.ts
2510
+ var import_react18 = require("react");
2511
+ var import_core11 = require("@lessonkit/core");
2512
+ var import_core12 = require("@lessonkit/core");
2513
+ function useCompoundResume(opts) {
2514
+ const storageRef = (0, import_react18.useRef)(opts.storage ?? (0, import_core12.createSessionStoragePort)());
2515
+ const resumedRef = (0, import_react18.useRef)(false);
2516
+ (0, import_react18.useEffect)(() => {
2517
+ if (!opts.enabled || !opts.courseId || resumedRef.current) return;
2518
+ const saved = (0, import_core11.loadCompoundState)(storageRef.current, opts.courseId, opts.compoundId);
2519
+ if (saved) {
2520
+ resumedRef.current = true;
2521
+ opts.onResume?.(saved);
2522
+ }
2523
+ }, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
2524
+ return (0, import_react18.useCallback)(
2525
+ (state) => {
2526
+ if (!opts.enabled || !opts.courseId) return;
2527
+ (0, import_core11.saveCompoundState)(storageRef.current, opts.courseId, opts.compoundId, state);
2528
+ },
2529
+ [opts.enabled, opts.courseId, opts.compoundId]
2530
+ );
2531
+ }
2532
+
2533
+ // src/compound/useCompoundPersistence.ts
2534
+ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = (0, import_core13.createSessionStoragePort)()) {
2535
+ if (!enabled || !courseId || pageCount < 1) return 0;
2536
+ const saved = (0, import_core13.loadCompoundState)(storage, courseId, compoundId);
2537
+ if (!saved) return 0;
2538
+ return (0, import_core13.clampCompoundPageIndex)(saved.activePageIndex, pageCount);
2539
+ }
2540
+ function useCompoundPersistence(opts) {
2541
+ const storage = opts.storage ?? (0, import_core13.createSessionStoragePort)();
2542
+ const ctx = useCompoundRegistry();
2543
+ const handlesVersion = useCompoundHandlesVersion();
2544
+ const pendingChildResumeRef = (0, import_react19.useRef)(null);
2545
+ const loadedChildStatesRef = (0, import_react19.useRef)({});
2546
+ const skipSaveUntilHydratedRef = (0, import_react19.useRef)(false);
2547
+ const buildState = (0, import_react19.useCallback)(() => {
2548
+ const childStates = {
2549
+ ...loadedChildStatesRef.current
2550
+ };
2551
+ if (ctx) {
2552
+ for (const [checkId, handle] of ctx.getHandles()) {
2553
+ if (handle.getCurrentState) {
2554
+ childStates[checkId] = handle.getCurrentState();
2555
+ delete loadedChildStatesRef.current[checkId];
2556
+ }
2557
+ }
2558
+ }
2559
+ return (0, import_core13.createCompoundResumeState)({
2560
+ activePageIndex: (0, import_core13.clampCompoundPageIndex)(opts.index, opts.pageCount),
2561
+ childStates
2562
+ });
2563
+ }, [ctx, opts.index, opts.pageCount]);
2564
+ const applyPendingChildResume = (0, import_react19.useCallback)(() => {
2565
+ const pending = pendingChildResumeRef.current;
2566
+ if (!pending || !ctx) return;
2567
+ const applied = resumeChildHandles(ctx.getHandles(), pending.childStates, { waitForHandles: true });
2568
+ if (!applied) return;
2569
+ pendingChildResumeRef.current = null;
2570
+ skipSaveUntilHydratedRef.current = false;
2571
+ }, [ctx]);
2572
+ const saveResume = useCompoundResume({
2573
+ courseId: opts.courseId,
2574
+ compoundId: opts.compoundId,
2575
+ enabled: opts.enabled,
2576
+ storage,
2577
+ onResume: (state) => {
2578
+ const clamped = (0, import_core13.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
2579
+ loadedChildStatesRef.current = { ...state.childStates };
2580
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2581
+ opts.setIndex(clamped);
2582
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped };
2583
+ queueMicrotask(() => applyPendingChildResume());
2584
+ }
2585
+ });
2586
+ (0, import_react19.useEffect)(() => {
2587
+ if (!opts.enabled || !opts.courseId) return;
2588
+ if (skipSaveUntilHydratedRef.current) return;
2589
+ saveResume(buildState());
2590
+ }, [
2591
+ opts.enabled,
2592
+ opts.courseId,
2593
+ opts.index,
2594
+ opts.pageCount,
2595
+ handlesVersion,
2596
+ saveResume,
2597
+ buildState
2598
+ ]);
2599
+ (0, import_react19.useEffect)(() => {
2600
+ applyPendingChildResume();
2601
+ }, [opts.index, handlesVersion, applyPendingChildResume]);
2602
+ }
2603
+
2604
+ // src/compound/useCompoundShell.ts
2605
+ function useCompoundShell(opts) {
2606
+ const ctx = useCompoundRegistry();
2607
+ useCompoundPersistence({
2608
+ courseId: opts.courseId,
2609
+ compoundId: opts.compoundId,
2610
+ pageCount: opts.pageCount,
2611
+ index: opts.index,
2612
+ setIndex: opts.setIndex,
2613
+ enabled: opts.persistEnabled,
2614
+ storage: opts.storage
2615
+ });
2616
+ const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
2617
+ const visibleIndex = (0, import_core14.clampCompoundPageIndex)(opts.index, opts.pageCount);
2618
+ useCompoundHandleRef(opts.ref, {
2619
+ activePageIndex: visibleIndex,
2620
+ setActivePageIndex: opts.setIndex,
2621
+ getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
2622
+ pageCount: opts.pageCount,
2623
+ enableSolutionsButton: opts.enableSolutionsButton
2624
+ });
2625
+ return { visibleIndex, goNext, goPrev, progress, ctx };
2626
+ }
2627
+ function useCompoundInitialIndex(opts) {
2628
+ return (0, import_react20.useMemo)(
2629
+ () => readCompoundInitialIndex(
2630
+ opts.courseId,
2631
+ opts.compoundId,
2632
+ opts.pageCount,
2633
+ opts.persistEnabled,
2634
+ opts.storage
2635
+ ),
2636
+ [opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
2637
+ );
2638
+ }
2639
+
2640
+ // src/compound/validateChildren.ts
2641
+ var import_react21 = __toESM(require("react"), 1);
2642
+ var import_core15 = require("@lessonkit/core");
2643
+
2644
+ // src/compound/blockType.ts
2645
+ var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
2646
+ function setLessonkitBlockType(component, blockType) {
2647
+ component[LESSONKIT_BLOCK_TYPE] = blockType;
2648
+ if (!component.displayName) {
2649
+ component.displayName = blockType;
2650
+ }
2651
+ return component;
2652
+ }
2653
+ function getLessonkitBlockType(component) {
2654
+ if (!component || typeof component !== "object" && typeof component !== "function") {
2655
+ return void 0;
2656
+ }
2657
+ const typed = component;
2658
+ return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
2659
+ }
2660
+
2661
+ // src/compound/validateChildren.ts
2662
+ var warnedPairs = /* @__PURE__ */ new Set();
2663
+ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
2664
+ "Page",
2665
+ "InteractiveBook",
2666
+ "AssessmentSequence"
2667
+ ]);
2668
+ function warnOrThrow(msg, strict) {
2669
+ if (strict) throw new Error(msg);
2670
+ if (!warnedPairs.has(msg)) {
2671
+ warnedPairs.add(msg);
2672
+ console.warn(msg);
2673
+ }
2674
+ }
2675
+ function validateNode(parent, node, depth, strict) {
2676
+ import_react21.default.Children.forEach(node, (child) => {
2677
+ if (!import_react21.default.isValidElement(child)) return;
2678
+ const blockType = getLessonkitBlockType(child.type);
2679
+ if (!blockType) {
2680
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
2681
+ validateNode(parent, child.props.children, depth, strict);
2682
+ }
2683
+ return;
2684
+ }
2685
+ if (!(0, import_core15.isChildTypeAllowed)(parent, blockType)) {
2686
+ const key = `${parent}:${blockType}`;
2687
+ if (!warnedPairs.has(key)) {
2688
+ warnedPairs.add(key);
2689
+ const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
2690
+ if (strict) throw new Error(msg);
2691
+ console.warn(msg);
2692
+ }
2693
+ }
2694
+ if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
2695
+ const maxDepth = import_core15.COMPOUND_MAX_NESTING_DEPTH[parent];
2696
+ if (depth >= maxDepth) {
2697
+ warnOrThrow(
2698
+ `[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
2699
+ strict
2700
+ );
2701
+ }
2702
+ const nestedParent = blockType;
2703
+ validateNode(nestedParent, child.props.children, depth + 1, strict);
2704
+ } else if (blockType === "Accordion") {
2705
+ const sections = child.props.sections;
2706
+ if (sections) validateAccordionSections(sections, strict);
2707
+ } else if (child.props && typeof child.props === "object" && "children" in child.props) {
2708
+ validateSubtreeForForbidden(
2709
+ child.props.children,
2710
+ import_core15.ACCORDION_FORBIDDEN_CHILD_TYPES,
2711
+ strict
2712
+ );
2713
+ }
2714
+ });
2715
+ }
2716
+ function validateSubtreeForForbidden(node, forbidden, strict) {
2717
+ import_react21.default.Children.forEach(node, (child) => {
2718
+ if (!import_react21.default.isValidElement(child)) return;
2719
+ const blockType = getLessonkitBlockType(child.type);
2720
+ if (blockType && forbidden.includes(blockType)) {
2721
+ warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
2722
+ }
2723
+ if (blockType === "Accordion") {
2724
+ const sections = child.props.sections;
2725
+ if (sections) validateAccordionSections(sections, strict);
2726
+ return;
2727
+ }
2728
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
2729
+ validateSubtreeForForbidden(
2730
+ child.props.children,
2731
+ forbidden,
2732
+ strict
2733
+ );
2734
+ }
2735
+ });
2736
+ }
2737
+ function validateAccordionSections(sections, strict) {
2738
+ if (!isDevEnvironment4() && !strict) return;
2739
+ for (const section of sections) {
2740
+ validateSubtreeForForbidden(section.content, import_core15.ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
2741
+ }
2742
+ }
2743
+ function validateCompoundChildren(parent, children, strict) {
2744
+ if (!isDevEnvironment4() && !strict) return;
2745
+ validateNode(parent, children, 0, strict);
2746
+ }
2747
+
2748
+ // src/compound/warnPersistence.ts
2749
+ var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
2750
+ function warnSharedCompoundStorageKey(opts) {
2751
+ if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
2752
+ console.warn(
2753
+ `[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
2754
+ );
2755
+ }
2756
+
2757
+ // src/blocks/AssessmentSequence.tsx
2758
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2759
+ var AssessmentSequenceInner = (0, import_react22.forwardRef)(
2760
+ function AssessmentSequenceInner2(props, ref) {
2761
+ const { compoundId, childArray, index, setIndex, persistEnabled } = props;
2762
+ const sequential = props.sequential !== false;
2763
+ const { config } = useLessonkit();
2764
+ const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
2765
+ courseId: config.courseId,
2766
+ compoundId,
2767
+ pageCount: childArray.length,
2768
+ index,
2769
+ setIndex,
2770
+ persistEnabled,
2771
+ ref,
2772
+ enableSolutionsButton: props.enableSolutionsButton
2773
+ });
2774
+ validateCompoundChildren("AssessmentSequence", props.children);
2775
+ if (!sequential) {
2776
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
2777
+ }
2778
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
2779
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { children: [
2780
+ "Question ",
2781
+ progress.current,
2782
+ " of ",
2783
+ progress.total
2784
+ ] }),
2785
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { hidden: i !== visibleIndex, children: child }, child.key ?? i)) }),
2786
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
2787
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2788
+ "button",
2789
+ {
2790
+ type: "button",
2791
+ "data-testid": "sequence-prev",
2792
+ disabled: visibleIndex === 0 || childArray.length === 0,
2793
+ onClick: goPrev,
2794
+ children: "Previous"
2795
+ }
2796
+ ),
2797
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2798
+ "button",
2799
+ {
2800
+ type: "button",
2801
+ "data-testid": "sequence-next",
2802
+ disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
2803
+ onClick: goNext,
2804
+ children: "Next"
2805
+ }
2806
+ )
2807
+ ] })
2808
+ ] });
2809
+ }
2810
+ );
2811
+ var AssessmentSequence = (0, import_react22.forwardRef)(
2812
+ function AssessmentSequence2(props, ref) {
2813
+ const compoundId = (0, import_react22.useMemo)(
2814
+ () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
2815
+ [props.blockId]
2816
+ );
2817
+ const childArray = import_react22.default.Children.toArray(props.children).filter(
2818
+ import_react22.default.isValidElement
2819
+ );
2820
+ const { config } = useLessonkit();
2821
+ const persistEnabled = config.session?.persistCompoundState !== false;
2822
+ (0, import_react22.useEffect)(() => {
2823
+ warnSharedCompoundStorageKey({
2824
+ persistEnabled,
2825
+ hasExplicitBlockId: Boolean(props.blockId),
2826
+ componentName: "AssessmentSequence"
2827
+ });
2828
+ }, [persistEnabled, props.blockId]);
2829
+ const initialIndex = useCompoundInitialIndex({
2830
+ courseId: config.courseId,
2831
+ compoundId,
2832
+ pageCount: childArray.length,
2833
+ persistEnabled
2834
+ });
2835
+ const [index, setIndex] = (0, import_react22.useState)(initialIndex);
2836
+ const setIndexStable = (0, import_react22.useCallback)((i) => setIndex(i), []);
2837
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2838
+ AssessmentSequenceInner,
2839
+ {
2840
+ ...props,
2841
+ ref,
2842
+ compoundId,
2843
+ childArray,
2844
+ index,
2845
+ setIndex,
2846
+ persistEnabled
2847
+ }
2848
+ ) });
2849
+ }
2850
+ );
2851
+ setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
2852
+
2853
+ // src/blocks/Text.tsx
2854
+ var import_react23 = require("react");
2855
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2856
+ function Text(props) {
2857
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
2858
+ }
2859
+ setLessonkitBlockType(Text, "Text");
2860
+
2861
+ // src/blocks/Heading.tsx
2862
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2863
+ function Heading(props) {
2864
+ const Tag = `h${props.level}`;
2865
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
2866
+ }
2867
+ setLessonkitBlockType(Heading, "Heading");
2868
+
2869
+ // src/blocks/Image.tsx
2870
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2871
+ function Image(props) {
2872
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2873
+ "img",
2874
+ {
2875
+ src: props.src,
2876
+ alt: props.alt,
2877
+ "data-lk-block-id": props.blockId,
2878
+ "data-testid": props.blockId ? `image-${props.blockId}` : "image",
2879
+ style: { maxWidth: "100%", height: "auto" }
2880
+ }
2881
+ );
2882
+ }
2883
+ setLessonkitBlockType(Image, "Image");
2884
+
2885
+ // src/blocks/Page.tsx
2886
+ var import_react24 = require("react");
2887
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2888
+ function Page(props) {
2889
+ validateCompoundChildren("Page", props.children);
2890
+ const { track } = useLessonkit();
2891
+ const lessonId = useEnclosingLessonId();
2892
+ (0, import_react24.useEffect)(() => {
2893
+ if (props.hidden || !lessonId) return;
2894
+ track(
2895
+ "compound_page_viewed",
2896
+ {
2897
+ blockId: props.blockId,
2898
+ pageIndex: props.pageIndex ?? 0,
2899
+ parentType: props.parentType
2900
+ },
2901
+ { lessonId }
2902
+ );
2903
+ }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
2904
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2905
+ "section",
2906
+ {
2907
+ "aria-label": props.title ?? "Page",
2908
+ "data-lk-block-id": props.blockId,
2909
+ "data-testid": `page-${props.blockId}`,
2910
+ hidden: props.hidden ? true : void 0,
2911
+ children: [
2912
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { children: props.title }) : null,
2913
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: props.children })
2914
+ ]
2915
+ }
2916
+ );
2917
+ }
2918
+ setLessonkitBlockType(Page, "Page");
2919
+
2920
+ // src/blocks/InteractiveBook.tsx
2921
+ var import_react25 = __toESM(require("react"), 1);
2922
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2923
+ var InteractiveBookInner = (0, import_react25.forwardRef)(
2924
+ function InteractiveBookInner2(props, ref) {
2925
+ const { blockId, pages, index, setIndex, persistEnabled } = props;
2926
+ validateCompoundChildren("InteractiveBook", pages);
2927
+ const { config, track } = useLessonkit();
2928
+ const lessonId = useEnclosingLessonId();
2929
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
2930
+ courseId: config.courseId,
2931
+ compoundId: blockId,
2932
+ pageCount: pages.length,
2933
+ index,
2934
+ setIndex,
2935
+ persistEnabled,
2936
+ ref
2937
+ });
2938
+ const pageTitles = (0, import_react25.useMemo)(
2939
+ () => pages.map((page) => page.props.title),
2940
+ [pages]
2941
+ );
2942
+ (0, import_react25.useEffect)(() => {
2943
+ if (!lessonId || pages.length === 0) return;
2944
+ track(
2945
+ "book_page_viewed",
2946
+ {
2947
+ blockId,
2948
+ pageIndex: visibleIndex,
2949
+ pageTitle: pageTitles[visibleIndex]
2950
+ },
2951
+ { lessonId }
2952
+ );
2953
+ }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
2954
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
2955
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { children: props.title }),
2956
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { children: [
2957
+ "Page ",
2958
+ progress.current,
2959
+ " of ",
2960
+ progress.total
2961
+ ] }),
2962
+ props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { "data-testid": "book-score", children: [
2963
+ "Score: ",
2964
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
2965
+ " /",
2966
+ " ",
2967
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
2968
+ ] }) : null,
2969
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
2970
+ (page, i) => import_react25.default.cloneElement(page, {
2971
+ key: page.key ?? page.props.blockId,
2972
+ hidden: i !== visibleIndex,
2973
+ pageIndex: i,
2974
+ parentType: "InteractiveBook"
2975
+ })
2976
+ ) }),
2977
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("nav", { "aria-label": "Book navigation", children: [
2978
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2979
+ "button",
2980
+ {
2981
+ type: "button",
2982
+ "data-testid": "book-prev",
2983
+ disabled: visibleIndex === 0 || pages.length === 0,
2984
+ onClick: goPrev,
2985
+ children: "Previous"
2986
+ }
2987
+ ),
2988
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2989
+ "button",
2990
+ {
2991
+ type: "button",
2992
+ "data-testid": "book-next",
2993
+ disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
2994
+ onClick: goNext,
2995
+ children: "Next"
2996
+ }
2997
+ )
2998
+ ] })
2999
+ ] });
3000
+ }
3001
+ );
3002
+ var InteractiveBook = (0, import_react25.forwardRef)(function InteractiveBook2(props, ref) {
3003
+ const blockId = (0, import_react25.useMemo)(
3004
+ () => normalizeComponentId(props.blockId, "blockId"),
3005
+ [props.blockId]
3006
+ );
3007
+ const pages = import_react25.default.Children.toArray(props.children).filter(
3008
+ import_react25.default.isValidElement
3009
+ );
3010
+ const { config } = useLessonkit();
3011
+ const persistEnabled = config.session?.persistCompoundState !== false;
3012
+ const initialIndex = useCompoundInitialIndex({
3013
+ courseId: config.courseId,
3014
+ compoundId: blockId,
3015
+ pageCount: pages.length,
3016
+ persistEnabled
3017
+ });
3018
+ const [index, setIndex] = (0, import_react25.useState)(initialIndex);
3019
+ const setIndexStable = (0, import_react25.useCallback)((i) => setIndex(i), []);
3020
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3021
+ InteractiveBookInner,
3022
+ {
3023
+ ...props,
3024
+ ref,
3025
+ blockId,
3026
+ pages,
3027
+ index,
3028
+ setIndex,
3029
+ persistEnabled
3030
+ }
3031
+ ) });
3032
+ });
3033
+ setLessonkitBlockType(InteractiveBook, "InteractiveBook");
3034
+
3035
+ // src/blocks/Accordion.tsx
3036
+ var import_react26 = require("react");
3037
+ var import_jsx_runtime17 = require("react/jsx-runtime");
3038
+ function Accordion(props) {
3039
+ if (isDevEnvironment4()) {
3040
+ validateAccordionSections(props.sections);
3041
+ }
3042
+ const [open, setOpen] = (0, import_react26.useState)(/* @__PURE__ */ new Set());
3043
+ const { track } = useLessonkit();
3044
+ const lessonId = useEnclosingLessonId();
3045
+ const baseId = (0, import_react26.useId)();
3046
+ const toggle = (sectionId) => {
3047
+ setOpen((prev) => {
3048
+ const next = new Set(prev);
3049
+ const expanded = !next.has(sectionId);
3050
+ if (expanded) next.add(sectionId);
3051
+ else next.delete(sectionId);
3052
+ track(
3053
+ "accordion_section_toggled",
3054
+ { blockId: props.blockId, sectionId, expanded },
3055
+ lessonId ? { lessonId } : void 0
3056
+ );
3057
+ return next;
3058
+ });
3059
+ };
3060
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3061
+ const expanded = open.has(section.id);
3062
+ const panelId = `${baseId}-${section.id}`;
3063
+ const triggerId = `${baseId}-trigger-${section.id}`;
3064
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
3065
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3066
+ "button",
3067
+ {
3068
+ id: triggerId,
3069
+ type: "button",
3070
+ "aria-expanded": expanded,
3071
+ "aria-controls": panelId,
3072
+ "data-testid": `accordion-trigger-${section.id}`,
3073
+ onClick: () => toggle(section.id),
3074
+ children: section.title
3075
+ }
3076
+ ) }),
3077
+ expanded ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3078
+ ] }, section.id);
3079
+ }) });
3080
+ }
3081
+ setLessonkitBlockType(Accordion, "Accordion");
3082
+
3083
+ // src/blocks/DialogCards.tsx
3084
+ var import_react27 = require("react");
3085
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3086
+ function DialogCards(props) {
3087
+ const [index, setIndex] = (0, import_react27.useState)(0);
3088
+ const [flipped, setFlipped] = (0, import_react27.useState)(false);
3089
+ const card = props.cards[index];
3090
+ if (!card) return null;
3091
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3092
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { children: [
3093
+ "Card ",
3094
+ index + 1,
3095
+ " of ",
3096
+ props.cards.length
3097
+ ] }),
3098
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3099
+ "button",
3100
+ {
3101
+ type: "button",
3102
+ "data-testid": "dialog-card-flip",
3103
+ "aria-pressed": flipped,
3104
+ onClick: () => setFlipped((f) => !f),
3105
+ style: { minHeight: "6rem", width: "100%" },
3106
+ children: flipped ? card.back : card.front
3107
+ }
3108
+ ),
3109
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("nav", { "aria-label": "Card navigation", children: [
3110
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3111
+ "button",
3112
+ {
3113
+ type: "button",
3114
+ "data-testid": "dialog-prev",
3115
+ disabled: index === 0,
3116
+ onClick: () => {
3117
+ setIndex((i) => i - 1);
3118
+ setFlipped(false);
3119
+ },
3120
+ children: "Previous"
3121
+ }
3122
+ ),
3123
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3124
+ "button",
3125
+ {
3126
+ type: "button",
3127
+ "data-testid": "dialog-next",
3128
+ disabled: index >= props.cards.length - 1,
3129
+ onClick: () => {
3130
+ setIndex((i) => i + 1);
3131
+ setFlipped(false);
3132
+ },
3133
+ children: "Next"
3134
+ }
3135
+ )
3136
+ ] })
3137
+ ] });
3138
+ }
3139
+ setLessonkitBlockType(DialogCards, "DialogCards");
3140
+
3141
+ // src/blocks/Flashcards.tsx
3142
+ var import_react28 = require("react");
3143
+ var import_jsx_runtime19 = require("react/jsx-runtime");
3144
+ function Flashcards(props) {
3145
+ const [index, setIndex] = (0, import_react28.useState)(0);
3146
+ const [face, setFace] = (0, import_react28.useState)("front");
3147
+ const { track } = useLessonkit();
3148
+ const lessonId = useEnclosingLessonId();
3149
+ const card = props.cards[index];
3150
+ if (!card) return null;
3151
+ const flip = () => {
3152
+ const next = face === "front" ? "back" : "front";
3153
+ setFace(next);
3154
+ track(
3155
+ "flashcard_flipped",
3156
+ { blockId: props.blockId, cardIndex: index, face: next },
3157
+ lessonId ? { lessonId } : void 0
3158
+ );
3159
+ };
3160
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3161
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3162
+ props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3163
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3164
+ "button",
3165
+ {
3166
+ type: "button",
3167
+ "data-testid": "flashcard-next",
3168
+ disabled: index >= props.cards.length - 1,
3169
+ onClick: () => {
3170
+ setIndex((i) => i + 1);
3171
+ setFace("front");
3172
+ },
3173
+ children: "Next card"
3174
+ }
3175
+ )
3176
+ ] });
3177
+ }
3178
+ setLessonkitBlockType(Flashcards, "Flashcards");
3179
+
3180
+ // src/blocks/ImageHotspots.tsx
3181
+ var import_react29 = require("react");
3182
+ var import_jsx_runtime20 = require("react/jsx-runtime");
3183
+ function ImageHotspots(props) {
3184
+ const [active, setActive] = (0, import_react29.useState)(null);
3185
+ const { track } = useLessonkit();
3186
+ const lessonId = useEnclosingLessonId();
3187
+ const open = (hotspotId) => {
3188
+ setActive(hotspotId);
3189
+ track(
3190
+ "hotspot_opened",
3191
+ { blockId: props.blockId, hotspotId },
3192
+ lessonId ? { lessonId } : void 0
3193
+ );
3194
+ };
3195
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3196
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3197
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3198
+ props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3199
+ "button",
3200
+ {
3201
+ type: "button",
3202
+ "aria-expanded": active === h.id,
3203
+ "aria-label": h.label,
3204
+ "data-testid": `hotspot-${h.id}`,
3205
+ style: {
3206
+ position: "absolute",
3207
+ left: `${h.x}%`,
3208
+ top: `${h.y}%`,
3209
+ transform: "translate(-50%, -50%)"
3210
+ },
3211
+ onClick: () => open(h.id),
3212
+ children: "+"
3213
+ },
3214
+ h.id
3215
+ ))
3216
+ ] }),
3217
+ active ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3218
+ props.hotspots.find((h) => h.id === active)?.content,
3219
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3220
+ ] }) : null
3221
+ ] });
3222
+ }
3223
+ setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3224
+
3225
+ // src/blocks/ImageSlider.tsx
3226
+ var import_react30 = require("react");
3227
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3228
+ function ImageSlider(props) {
3229
+ const [index, setIndex] = (0, import_react30.useState)(0);
3230
+ const { track } = useLessonkit();
3231
+ const lessonId = useEnclosingLessonId();
3232
+ const slide = props.slides[index];
3233
+ if (!slide) return null;
3234
+ const goTo = (next) => {
3235
+ setIndex(next);
3236
+ track(
3237
+ "image_slider_changed",
3238
+ { blockId: props.blockId, slideIndex: next },
3239
+ lessonId ? { lessonId } : void 0
3240
+ );
3241
+ };
3242
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
3243
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
3244
+ slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { children: slide.caption }) : null,
3245
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3246
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3247
+ "button",
3248
+ {
3249
+ type: "button",
3250
+ "data-testid": "slider-prev",
3251
+ disabled: index === 0,
3252
+ onClick: () => goTo(index - 1),
3253
+ children: "Previous"
3254
+ }
3255
+ ),
3256
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { children: [
3257
+ index + 1,
3258
+ " / ",
3259
+ props.slides.length
3260
+ ] }),
3261
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3262
+ "button",
3263
+ {
3264
+ type: "button",
3265
+ "data-testid": "slider-next",
3266
+ disabled: index >= props.slides.length - 1,
3267
+ onClick: () => goTo(index + 1),
3268
+ children: "Next"
3269
+ }
3270
+ )
3271
+ ] })
1989
3272
  ] });
1990
3273
  }
1991
- var DragTheWordsInnerForwarded = (0, import_react12.forwardRef)(DragTheWordsInner);
1992
- var DragTheWords = (0, import_react12.forwardRef)(function DragTheWords2(props, ref) {
1993
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1994
- });
3274
+ setLessonkitBlockType(ImageSlider, "ImageSlider");
1995
3275
 
1996
- // src/blocks/DragAndDrop.tsx
1997
- var import_react13 = require("react");
1998
- var import_jsx_runtime9 = require("react/jsx-runtime");
1999
- var INTERACTION5 = "dragAndDrop";
2000
- function DragAndDropInner(props, ref) {
2001
- const checkId = (0, import_react13.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3276
+ // src/blocks/FindHotspot.tsx
3277
+ var import_react31 = require("react");
3278
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3279
+ var INTERACTION6 = "findHotspot";
3280
+ function FindHotspotInner(props, ref) {
3281
+ const checkId = (0, import_react31.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3282
+ const [selected, setSelected] = (0, import_react31.useState)(null);
3283
+ const [checked, setChecked] = (0, import_react31.useState)(false);
2002
3284
  const assessment = useAssessmentState(props.enclosingLessonId);
2003
- const [assignments, setAssignments] = (0, import_react13.useState)(
2004
- () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2005
- );
2006
- const [pool, setPool] = (0, import_react13.useState)(() => props.items.map((i) => i.id));
2007
- const [keyboardItem, setKeyboardItem] = (0, import_react13.useState)(null);
2008
- const [passed, setPassed] = (0, import_react13.useState)(false);
2009
- const completedRef = (0, import_react13.useRef)(false);
2010
- const reset = () => {
2011
- completedRef.current = false;
2012
- setPassed(false);
2013
- setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2014
- setPool(props.items.map((i) => i.id));
2015
- setKeyboardItem(null);
2016
- };
2017
- (0, import_react13.useEffect)(() => {
2018
- reset();
2019
- }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2020
- const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2021
- const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
2022
- const handle = (0, import_react13.useMemo)(() => {
2023
- const maxScore = props.targets.length || 1;
2024
- let score = 0;
2025
- props.targets.forEach((t) => {
2026
- if (assignments[t.id] === t.accepts) score += 1;
2027
- });
2028
- return {
2029
- getScore: () => score,
2030
- getMaxScore: () => maxScore,
2031
- getAnswerGiven: () => allFilled,
2032
- resetTask: reset,
2033
- showSolutions: () => {
3285
+ const correct = selected === props.correctTargetId;
3286
+ const handle = (0, import_react31.useMemo)(
3287
+ () => buildAssessmentHandle({
3288
+ checkId,
3289
+ getScore: () => checked && correct ? 1 : 0,
3290
+ getMaxScore: () => 1,
3291
+ getAnswerGiven: () => selected !== null,
3292
+ resetTask: () => {
3293
+ setSelected(null);
3294
+ setChecked(false);
2034
3295
  },
3296
+ showSolutions: () => setSelected(props.correctTargetId),
2035
3297
  getXAPIData: () => ({
2036
3298
  checkId,
2037
- interactionType: INTERACTION5,
2038
- response: assignments,
2039
- correct: allCorrect,
2040
- score,
2041
- maxScore
2042
- })
2043
- };
2044
- }, [allCorrect, allFilled, assignments, checkId, props.targets]);
2045
- (0, import_react13.useImperativeHandle)(ref, () => handle, [handle]);
2046
- useRegisterAssessmentHandle(checkId, handle);
2047
- const place = (targetId, itemId) => {
2048
- if (passed && !props.enableRetry) return;
2049
- const prev = assignments[targetId];
2050
- setAssignments((a) => ({ ...a, [targetId]: itemId }));
2051
- setPool((p) => {
2052
- const next = p.filter((id) => id !== itemId);
2053
- if (prev) next.push(prev);
2054
- return next;
2055
- });
2056
- setKeyboardItem(null);
2057
- };
2058
- const check = () => {
2059
- if (!allFilled) return;
3299
+ interactionType: INTERACTION6,
3300
+ response: selected ?? void 0,
3301
+ correct: checked ? correct : void 0,
3302
+ score: checked && correct ? 1 : 0,
3303
+ maxScore: 1
3304
+ }),
3305
+ getCurrentState: () => ({ selected, checked }),
3306
+ resume: (state) => {
3307
+ const nextSelected = readStringField(state, "selected");
3308
+ if (typeof nextSelected === "string") setSelected(nextSelected);
3309
+ readBooleanStateField(state, "checked", setChecked);
3310
+ }
3311
+ }),
3312
+ [checkId, selected, checked, correct, props.correctTargetId]
3313
+ );
3314
+ useAssessmentHandleRegistration(checkId, handle, ref);
3315
+ const submit = () => {
3316
+ if (!selected) return;
3317
+ setChecked(true);
2060
3318
  assessment.answer({
2061
3319
  checkId,
2062
- interactionType: INTERACTION5,
2063
- response: assignments,
2064
- correct: allCorrect
3320
+ interactionType: INTERACTION6,
3321
+ response: selected,
3322
+ correct
2065
3323
  });
2066
- if (allCorrect && !completedRef.current) {
2067
- completedRef.current = true;
2068
- setPassed(true);
3324
+ if (correct) {
2069
3325
  assessment.complete({
2070
3326
  checkId,
2071
- interactionType: INTERACTION5,
2072
- score: props.targets.length,
2073
- maxScore: props.targets.length,
2074
- passingScore: props.passingScore ?? props.targets.length
3327
+ interactionType: INTERACTION6,
3328
+ score: 1,
3329
+ maxScore: 1,
3330
+ passingScore: props.passingScore
2075
3331
  });
2076
3332
  }
2077
3333
  };
2078
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2079
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2080
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
2081
- const item = props.items.find((i) => i.id === id);
2082
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3334
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
3335
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3336
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3337
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2083
3338
  "button",
2084
3339
  {
2085
3340
  type: "button",
2086
- draggable: true,
2087
- "data-testid": `drag-item-${id}`,
2088
- "aria-pressed": keyboardItem === id,
2089
- onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2090
- onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2091
- style: { margin: "0.25rem" },
2092
- children: item.label
3341
+ "aria-label": t.label,
3342
+ "aria-pressed": selected === t.id,
3343
+ "data-testid": `target-${t.id}`,
3344
+ style: {
3345
+ position: "absolute",
3346
+ left: `${t.x}%`,
3347
+ top: `${t.y}%`,
3348
+ transform: "translate(-50%, -50%)"
3349
+ },
3350
+ onClick: () => setSelected(t.id),
3351
+ children: t.label
2093
3352
  },
2094
- id
2095
- );
2096
- }) }),
2097
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { children: props.targets.map((target) => {
2098
- const assigned = assignments[target.id];
2099
- const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2100
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("li", { children: [
2101
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: target.label }),
2102
- " ",
2103
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2104
- "span",
2105
- {
2106
- role: "button",
2107
- tabIndex: 0,
2108
- "data-testid": `drop-${target.id}`,
2109
- onDragOver: (e) => e.preventDefault(),
2110
- onDrop: (e) => {
2111
- e.preventDefault();
2112
- const id = e.dataTransfer.getData("text/plain");
2113
- if (id) place(target.id, id);
2114
- },
2115
- onClick: () => keyboardItem && place(target.id, keyboardItem),
2116
- onKeyDown: (e) => {
2117
- if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2118
- },
2119
- style: {
2120
- display: "inline-block",
2121
- minWidth: "8em",
2122
- border: "1px dashed currentColor",
2123
- padding: "0.25em"
2124
- },
2125
- children: label
2126
- }
2127
- )
2128
- ] }, target.id);
2129
- }) }),
2130
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2131
- allFilled ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
3353
+ t.id
3354
+ ))
3355
+ ] }),
3356
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
3357
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
2132
3358
  ] });
2133
3359
  }
2134
- var DragAndDropInnerForwarded = (0, import_react13.forwardRef)(DragAndDropInner);
2135
- var DragAndDrop = (0, import_react13.forwardRef)(function DragAndDrop2(props, ref) {
2136
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
3360
+ var FindHotspotInnerForwarded = (0, import_react31.forwardRef)(FindHotspotInner);
3361
+ var FindHotspot = (0, import_react31.forwardRef)(function FindHotspot2(props, ref) {
3362
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
2137
3363
  });
3364
+ setLessonkitBlockType(FindHotspot, "FindHotspot");
2138
3365
 
2139
- // src/blocks/AssessmentSequence.tsx
2140
- var import_react14 = __toESM(require("react"), 1);
2141
- var import_jsx_runtime10 = require("react/jsx-runtime");
2142
- function AssessmentSequence(props) {
2143
- const sequential = props.sequential !== false;
2144
- const childArray = import_react14.default.Children.toArray(props.children).filter(import_react14.default.isValidElement);
2145
- const [index, setIndex] = (0, import_react14.useState)(0);
2146
- const current = childArray[index] ?? null;
2147
- const goNext = (0, import_react14.useCallback)(() => {
2148
- setIndex((i) => Math.min(i + 1, childArray.length - 1));
2149
- }, [childArray.length]);
2150
- const goPrev = (0, import_react14.useCallback)(() => {
2151
- setIndex((i) => Math.max(i - 1, 0));
2152
- }, []);
2153
- const progress = (0, import_react14.useMemo)(
2154
- () => ({ current: index + 1, total: childArray.length }),
2155
- [index, childArray.length]
3366
+ // src/blocks/FindMultipleHotspots.tsx
3367
+ var import_react32 = require("react");
3368
+ var import_jsx_runtime23 = require("react/jsx-runtime");
3369
+ var INTERACTION7 = "findMultipleHotspots";
3370
+ function FindMultipleHotspotsInner(props, ref) {
3371
+ const checkId = (0, import_react32.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3372
+ const [selected, setSelected] = (0, import_react32.useState)(/* @__PURE__ */ new Set());
3373
+ const [checked, setChecked] = (0, import_react32.useState)(false);
3374
+ const assessment = useAssessmentState(props.enclosingLessonId);
3375
+ const toggle = (id) => {
3376
+ setSelected((prev) => {
3377
+ const next = new Set(prev);
3378
+ if (next.has(id)) next.delete(id);
3379
+ else next.add(id);
3380
+ return next;
3381
+ });
3382
+ };
3383
+ const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
3384
+ const handle = (0, import_react32.useMemo)(
3385
+ () => buildAssessmentHandle({
3386
+ checkId,
3387
+ getScore: () => checked && correct ? 1 : 0,
3388
+ getMaxScore: () => 1,
3389
+ getAnswerGiven: () => selected.size > 0,
3390
+ resetTask: () => {
3391
+ setSelected(/* @__PURE__ */ new Set());
3392
+ setChecked(false);
3393
+ },
3394
+ showSolutions: () => setSelected(new Set(props.correctTargetIds)),
3395
+ getXAPIData: () => ({
3396
+ checkId,
3397
+ interactionType: INTERACTION7,
3398
+ response: [...selected],
3399
+ correct: checked ? correct : void 0,
3400
+ score: checked && correct ? 1 : 0,
3401
+ maxScore: 1
3402
+ }),
3403
+ getCurrentState: () => ({ selected: [...selected], checked }),
3404
+ resume: (state) => {
3405
+ const raw = state.selected;
3406
+ if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
3407
+ readBooleanStateField(state, "checked", setChecked);
3408
+ }
3409
+ }),
3410
+ [checkId, selected, checked, correct, props.correctTargetIds]
2156
3411
  );
2157
- if (!sequential) {
2158
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentSequenceProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("section", { "aria-label": "Assessment sequence", children: props.children }) });
2159
- }
2160
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentSequenceProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
2161
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { children: [
2162
- "Question ",
2163
- progress.current,
2164
- " of ",
2165
- progress.total
2166
- ] }),
2167
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { "data-testid": "assessment-sequence-step", children: current }),
2168
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
2169
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "sequence-prev", disabled: index === 0, onClick: goPrev, children: "Previous" }),
2170
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3412
+ useAssessmentHandleRegistration(checkId, handle, ref);
3413
+ const submit = () => {
3414
+ if (selected.size === 0) return;
3415
+ setChecked(true);
3416
+ assessment.answer({
3417
+ checkId,
3418
+ interactionType: INTERACTION7,
3419
+ response: [...selected],
3420
+ correct
3421
+ });
3422
+ if (correct) {
3423
+ assessment.complete({
3424
+ checkId,
3425
+ interactionType: INTERACTION7,
3426
+ score: 1,
3427
+ maxScore: 1,
3428
+ passingScore: props.passingScore
3429
+ });
3430
+ }
3431
+ };
3432
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
3433
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3434
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3435
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2171
3436
  "button",
2172
3437
  {
2173
3438
  type: "button",
2174
- "data-testid": "sequence-next",
2175
- disabled: index >= childArray.length - 1,
2176
- onClick: goNext,
2177
- children: "Next"
2178
- }
2179
- )
2180
- ] })
2181
- ] }) });
3439
+ "aria-label": t.label,
3440
+ "aria-pressed": selected.has(t.id),
3441
+ "data-testid": `target-${t.id}`,
3442
+ style: {
3443
+ position: "absolute",
3444
+ left: `${t.x}%`,
3445
+ top: `${t.y}%`,
3446
+ transform: "translate(-50%, -50%)"
3447
+ },
3448
+ onClick: () => toggle(t.id),
3449
+ children: t.label
3450
+ },
3451
+ t.id
3452
+ ))
3453
+ ] }),
3454
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
3455
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
3456
+ ] });
2182
3457
  }
3458
+ var FindMultipleHotspotsInnerForwarded = (0, import_react32.forwardRef)(FindMultipleHotspotsInner);
3459
+ var FindMultipleHotspots = (0, import_react32.forwardRef)(
3460
+ function FindMultipleHotspots2(props, ref) {
3461
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
3462
+ }
3463
+ );
3464
+ setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
2183
3465
 
2184
3466
  // src/index.tsx
2185
- var import_core10 = require("@lessonkit/core");
3467
+ var import_core17 = require("@lessonkit/core");
2186
3468
 
2187
3469
  // src/theme/ThemeProvider.tsx
2188
- var import_react15 = __toESM(require("react"), 1);
3470
+ var import_react33 = __toESM(require("react"), 1);
2189
3471
  var import_themes = require("@lessonkit/themes");
2190
3472
 
2191
3473
  // src/theme/applyCssVariables.ts
@@ -2204,11 +3486,11 @@ function applyCssVariables(target, vars, previousKeys) {
2204
3486
  }
2205
3487
 
2206
3488
  // src/theme/ThemeProvider.tsx
2207
- var import_jsx_runtime11 = require("react/jsx-runtime");
2208
- var ThemeContext = (0, import_react15.createContext)(null);
3489
+ var import_jsx_runtime24 = require("react/jsx-runtime");
3490
+ var ThemeContext = (0, import_react33.createContext)(null);
2209
3491
  var useIsoLayoutEffect2 = (
2210
3492
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
2211
- typeof window !== "undefined" ? import_react15.useLayoutEffect : import_react15.default.useEffect
3493
+ typeof window !== "undefined" ? import_react33.useLayoutEffect : import_react33.default.useEffect
2212
3494
  );
2213
3495
  function getSystemMode() {
2214
3496
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -2227,7 +3509,7 @@ function ThemeProvider(props) {
2227
3509
  const preset = props.preset ?? "default";
2228
3510
  const mode = props.mode ?? "light";
2229
3511
  const targetKind = props.target ?? "document";
2230
- const [resolvedMode, setResolvedMode] = (0, import_react15.useState)(
3512
+ const [resolvedMode, setResolvedMode] = (0, import_react33.useState)(
2231
3513
  () => mode === "system" ? getSystemMode() : mode
2232
3514
  );
2233
3515
  useIsoLayoutEffect2(() => {
@@ -2243,20 +3525,20 @@ function ThemeProvider(props) {
2243
3525
  return () => mq.removeEventListener("change", onChange);
2244
3526
  }, [mode]);
2245
3527
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
2246
- const effectiveTheme = (0, import_react15.useMemo)(() => {
3528
+ const effectiveTheme = (0, import_react33.useMemo)(() => {
2247
3529
  const modeBase = resolveModeBase(mode, dataTheme);
2248
3530
  const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
2249
3531
  return (0, import_themes.mergeThemes)(base, props.theme ?? {});
2250
3532
  }, [preset, mode, dataTheme, props.theme]);
2251
- const hostRef = (0, import_react15.useRef)(null);
2252
- const appliedKeysRef = (0, import_react15.useRef)(/* @__PURE__ */ new Set());
3533
+ const hostRef = (0, import_react33.useRef)(null);
3534
+ const appliedKeysRef = (0, import_react33.useRef)(/* @__PURE__ */ new Set());
2253
3535
  useIsoLayoutEffect2(() => {
2254
3536
  if (targetKind === "document" && typeof document !== "undefined") {
2255
3537
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
2256
3538
  return () => document.documentElement.removeAttribute("data-lk-theme");
2257
3539
  }
2258
3540
  }, [targetKind, dataTheme]);
2259
- const inject = (0, import_react15.useCallback)(() => {
3541
+ const inject = (0, import_react33.useCallback)(() => {
2260
3542
  const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
2261
3543
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
2262
3544
  if (!el) return;
@@ -2273,7 +3555,7 @@ function ThemeProvider(props) {
2273
3555
  appliedKeysRef.current = /* @__PURE__ */ new Set();
2274
3556
  };
2275
3557
  }, [inject, targetKind]);
2276
- const value = (0, import_react15.useMemo)(
3558
+ const value = (0, import_react33.useMemo)(
2277
3559
  () => ({
2278
3560
  theme: effectiveTheme,
2279
3561
  preset,
@@ -2283,21 +3565,270 @@ function ThemeProvider(props) {
2283
3565
  [effectiveTheme, preset, mode, dataTheme]
2284
3566
  );
2285
3567
  if (targetKind === "document") {
2286
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
3568
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
2287
3569
  }
2288
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
3570
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
2289
3571
  }
2290
3572
  function useTheme() {
2291
- const ctx = (0, import_react15.useContext)(ThemeContext);
3573
+ const ctx = (0, import_react33.useContext)(ThemeContext);
2292
3574
  if (!ctx) {
2293
3575
  throw new Error("useTheme must be used within a ThemeProvider");
2294
3576
  }
2295
3577
  return ctx;
2296
3578
  }
2297
3579
 
3580
+ // src/catalogV3Entries.ts
3581
+ var import_core16 = require("@lessonkit/core");
3582
+ var COMPOUND_PARENTS = ["Lesson", "Page", "InteractiveBook", "AssessmentSequence"];
3583
+ function extendParents(entry) {
3584
+ if (!entry.parentConstraints?.length) return entry;
3585
+ const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
3586
+ return { ...entry, parentConstraints: [...merged] };
3587
+ }
3588
+ var assessmentBehaviourProps = [
3589
+ { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
3590
+ { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
3591
+ { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
3592
+ { name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
3593
+ ];
3594
+ var v3CompoundAndContentEntries = [
3595
+ {
3596
+ type: "Text",
3597
+ category: "content",
3598
+ description: "Paragraph text content.",
3599
+ props: [
3600
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
3601
+ { name: "children", type: "ReactNode", required: true, description: "Text body." }
3602
+ ],
3603
+ requiredIds: [],
3604
+ parentConstraints: [...COMPOUND_PARENTS],
3605
+ a11y: { element: "p", ariaLabel: "Text", keyboard: "N/A", notes: "Semantic paragraph." },
3606
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
3607
+ telemetry: { emits: [] }
3608
+ },
3609
+ {
3610
+ type: "Heading",
3611
+ category: "content",
3612
+ description: "Heading levels 1\u20133.",
3613
+ props: [
3614
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
3615
+ { name: "level", type: "1 | 2 | 3", required: true, description: "Heading level." },
3616
+ { name: "children", type: "ReactNode", required: true, description: "Heading text." }
3617
+ ],
3618
+ requiredIds: [],
3619
+ parentConstraints: [...COMPOUND_PARENTS],
3620
+ a11y: { element: "h1-h3", ariaLabel: "Heading", keyboard: "N/A", notes: "Use one level per outline." },
3621
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
3622
+ telemetry: { emits: [] }
3623
+ },
3624
+ {
3625
+ type: "Image",
3626
+ category: "content",
3627
+ description: "Image with required alt text.",
3628
+ props: [
3629
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
3630
+ { name: "src", type: "string", required: true, description: "Image URL." },
3631
+ { name: "alt", type: "string", required: true, description: "Alt text." }
3632
+ ],
3633
+ requiredIds: [],
3634
+ parentConstraints: [...COMPOUND_PARENTS],
3635
+ a11y: { element: "img", ariaLabel: "Image", keyboard: "N/A", notes: "Requires alt." },
3636
+ theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
3637
+ telemetry: { emits: [] }
3638
+ },
3639
+ {
3640
+ type: "Page",
3641
+ category: "container",
3642
+ compoundContract: true,
3643
+ h5pMachineName: "H5P.Column",
3644
+ h5pAlias: "Column",
3645
+ description: "Column layout container (H5P Column / Page).",
3646
+ allowedChildTypes: [...import_core16.PAGE_ALLOWED_CHILD_TYPES],
3647
+ maxNestingDepth: import_core16.COMPOUND_MAX_NESTING_DEPTH.Page,
3648
+ props: [
3649
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3650
+ { name: "title", type: "string", required: false, description: "Page title." },
3651
+ { name: "children", type: "ReactNode", required: true, description: "Page content." }
3652
+ ],
3653
+ requiredIds: [],
3654
+ optionalIds: ["blockId"],
3655
+ parentConstraints: ["Lesson", "InteractiveBook"],
3656
+ a11y: { element: "section", ariaLabel: "Page", keyboard: "N/A", notes: "H5P Column equivalent." },
3657
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
3658
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
3659
+ },
3660
+ {
3661
+ type: "InteractiveBook",
3662
+ category: "container",
3663
+ compoundContract: true,
3664
+ h5pMachineName: "H5P.InteractiveBook",
3665
+ h5pAlias: "Interactive Book",
3666
+ description: "Multi-page book with chapter navigation.",
3667
+ allowedChildTypes: [...import_core16.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
3668
+ maxNestingDepth: import_core16.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
3669
+ props: [
3670
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3671
+ { name: "title", type: "string", required: true, description: "Book title." },
3672
+ { name: "showBookScore", type: "boolean", required: false, description: "Show aggregate score." },
3673
+ { name: "children", type: "Page[]", required: true, description: "Page chapters." }
3674
+ ],
3675
+ requiredIds: ["blockId"],
3676
+ parentConstraints: ["Lesson"],
3677
+ a11y: {
3678
+ element: "section",
3679
+ ariaLabel: "Interactive book",
3680
+ keyboard: "Previous/Next chapter navigation.",
3681
+ notes: "H5P Interactive Book equivalent."
3682
+ },
3683
+ theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
3684
+ telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
3685
+ },
3686
+ {
3687
+ type: "Accordion",
3688
+ category: "content",
3689
+ h5pMachineName: "H5P.Accordion",
3690
+ h5pAlias: "Accordion",
3691
+ description: "Expandable sections.",
3692
+ props: [
3693
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3694
+ { name: "sections", type: "AccordionSection[]", required: true, description: "Sections." }
3695
+ ],
3696
+ requiredIds: ["blockId"],
3697
+ parentConstraints: [...COMPOUND_PARENTS],
3698
+ a11y: { element: "section", ariaLabel: "Accordion", keyboard: "Button toggles sections.", notes: "No nested accordions." },
3699
+ theming: { surface: "global-inherit", stylingNotes: "Disclosure pattern." },
3700
+ telemetry: { emits: ["accordion_section_toggled"] }
3701
+ },
3702
+ {
3703
+ type: "DialogCards",
3704
+ category: "content",
3705
+ h5pMachineName: "H5P.Dialogcards",
3706
+ h5pAlias: "Dialog Cards",
3707
+ description: "Flip cards with front/back text.",
3708
+ props: [
3709
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3710
+ { name: "cards", type: "DialogCard[]", required: true, description: "Cards." }
3711
+ ],
3712
+ requiredIds: ["blockId"],
3713
+ parentConstraints: [...COMPOUND_PARENTS],
3714
+ a11y: { element: "section", ariaLabel: "Dialog cards", keyboard: "Flip and navigate cards.", notes: "Reduced motion safe." },
3715
+ theming: { surface: "global-inherit", stylingNotes: "Card flip." },
3716
+ telemetry: { emits: [] }
3717
+ },
3718
+ {
3719
+ type: "Flashcards",
3720
+ category: "content",
3721
+ h5pMachineName: "H5P.Flashcards",
3722
+ h5pAlias: "Flashcards",
3723
+ description: "Study flashcards with optional self-score.",
3724
+ props: [
3725
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3726
+ { name: "cards", type: "Flashcard[]", required: true, description: "Cards." },
3727
+ { name: "selfScore", type: "boolean", required: false, description: "Self-score mode." }
3728
+ ],
3729
+ requiredIds: ["blockId"],
3730
+ parentConstraints: [...COMPOUND_PARENTS],
3731
+ a11y: { element: "section", ariaLabel: "Flashcards", keyboard: "Flip and next.", notes: "Not LMS-scored by default." },
3732
+ theming: { surface: "global-inherit", stylingNotes: "Study mode." },
3733
+ telemetry: { emits: ["flashcard_flipped"] }
3734
+ },
3735
+ {
3736
+ type: "ImageHotspots",
3737
+ category: "content",
3738
+ h5pMachineName: "H5P.ImageHotspots",
3739
+ h5pAlias: "Image Hotspots",
3740
+ description: "Image with clickable hotspot popovers.",
3741
+ props: [
3742
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3743
+ { name: "src", type: "string", required: true, description: "Image URL." },
3744
+ { name: "alt", type: "string", required: true, description: "Alt text." },
3745
+ { name: "hotspots", type: "HotspotSpec[]", required: true, description: "Hotspots." }
3746
+ ],
3747
+ requiredIds: ["blockId"],
3748
+ parentConstraints: [...COMPOUND_PARENTS],
3749
+ a11y: { element: "section", ariaLabel: "Image hotspots", keyboard: "Buttons on image.", notes: "Popover dialog." },
3750
+ theming: { surface: "global-inherit", stylingNotes: "Positioned hotspots." },
3751
+ telemetry: { emits: ["hotspot_opened"] }
3752
+ },
3753
+ {
3754
+ type: "ImageSlider",
3755
+ category: "content",
3756
+ h5pMachineName: "H5P.ImageSlider",
3757
+ h5pAlias: "Image Slider",
3758
+ description: "Carousel of images.",
3759
+ props: [
3760
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
3761
+ { name: "slides", type: "ImageSlide[]", required: true, description: "Slides." }
3762
+ ],
3763
+ requiredIds: ["blockId"],
3764
+ parentConstraints: [...COMPOUND_PARENTS],
3765
+ a11y: { element: "section", ariaLabel: "Image slider", keyboard: "Previous/next slide.", notes: "Carousel." },
3766
+ theming: { surface: "global-inherit", stylingNotes: "Slider." },
3767
+ telemetry: { emits: ["image_slider_changed"] }
3768
+ },
3769
+ {
3770
+ type: "FindHotspot",
3771
+ category: "assessment",
3772
+ assessmentContract: true,
3773
+ h5pMachineName: "H5P.ImageHotspotQuestion",
3774
+ h5pAlias: "Find the Hotspot",
3775
+ description: "Select the correct region on an image.",
3776
+ props: [
3777
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
3778
+ { name: "src", type: "string", required: true, description: "Image URL." },
3779
+ { name: "alt", type: "string", required: true, description: "Alt text." },
3780
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
3781
+ { name: "correctTargetId", type: "string", required: true, description: "Correct target id." },
3782
+ ...assessmentBehaviourProps
3783
+ ],
3784
+ requiredIds: ["checkId"],
3785
+ parentConstraints: [...COMPOUND_PARENTS],
3786
+ a11y: { element: "section", ariaLabel: "Find the hotspot", keyboard: "Select target buttons.", notes: "Scored." },
3787
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
3788
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
3789
+ },
3790
+ {
3791
+ type: "FindMultipleHotspots",
3792
+ category: "assessment",
3793
+ assessmentContract: true,
3794
+ h5pMachineName: "H5P.ImageMultipleHotspotQuestion",
3795
+ h5pAlias: "Find Multiple Hotspots",
3796
+ description: "Select all correct regions on an image.",
3797
+ props: [
3798
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
3799
+ { name: "src", type: "string", required: true, description: "Image URL." },
3800
+ { name: "alt", type: "string", required: true, description: "Alt text." },
3801
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
3802
+ { name: "correctTargetIds", type: "string[]", required: true, description: "Correct target ids." },
3803
+ ...assessmentBehaviourProps
3804
+ ],
3805
+ requiredIds: ["checkId"],
3806
+ parentConstraints: [...COMPOUND_PARENTS],
3807
+ a11y: { element: "section", ariaLabel: "Find multiple hotspots", keyboard: "Toggle targets.", notes: "Scored." },
3808
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
3809
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
3810
+ }
3811
+ ];
3812
+ function buildV3CatalogFromV2(v2) {
3813
+ const patched = v2.map((entry) => {
3814
+ const base = extendParents(entry);
3815
+ if (entry.type === "AssessmentSequence") {
3816
+ return {
3817
+ ...base,
3818
+ compoundContract: true,
3819
+ allowedChildTypes: [...import_core16.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
3820
+ maxNestingDepth: import_core16.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
3821
+ };
3822
+ }
3823
+ return base;
3824
+ });
3825
+ return [...patched, ...v3CompoundAndContentEntries];
3826
+ }
3827
+
2298
3828
  // src/blockCatalog.ts
2299
3829
  var blockCatalogVersion = 1;
2300
3830
  var blockCatalogV2Version = 2;
3831
+ var blockCatalogV3Version = 3;
2301
3832
  var BLOCK_CATALOG = [
2302
3833
  {
2303
3834
  type: "Course",
@@ -2484,7 +4015,7 @@ var BLOCK_CATALOG = [
2484
4015
  }
2485
4016
  }
2486
4017
  ];
2487
- var assessmentBehaviourProps = [
4018
+ var assessmentBehaviourProps2 = [
2488
4019
  { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
2489
4020
  { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
2490
4021
  { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
@@ -2502,7 +4033,7 @@ var v2AssessmentEntries = [
2502
4033
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2503
4034
  { name: "question", type: "string", required: true, description: "Question text." },
2504
4035
  { name: "answer", type: "boolean", required: true, description: "Correct answer." },
2505
- ...assessmentBehaviourProps
4036
+ ...assessmentBehaviourProps2
2506
4037
  ],
2507
4038
  requiredIds: ["checkId"],
2508
4039
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2527,7 +4058,7 @@ var v2AssessmentEntries = [
2527
4058
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2528
4059
  { name: "template", type: "string", required: true, description: "Text with *blank* markers." },
2529
4060
  { name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
2530
- ...assessmentBehaviourProps
4061
+ ...assessmentBehaviourProps2
2531
4062
  ],
2532
4063
  requiredIds: ["checkId"],
2533
4064
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2551,7 +4082,7 @@ var v2AssessmentEntries = [
2551
4082
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2552
4083
  { name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
2553
4084
  { name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
2554
- ...assessmentBehaviourProps
4085
+ ...assessmentBehaviourProps2
2555
4086
  ],
2556
4087
  requiredIds: ["checkId"],
2557
4088
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2575,7 +4106,7 @@ var v2AssessmentEntries = [
2575
4106
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2576
4107
  { name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
2577
4108
  { name: "words", type: "string[]", required: true, description: "Draggable word bank." },
2578
- ...assessmentBehaviourProps
4109
+ ...assessmentBehaviourProps2
2579
4110
  ],
2580
4111
  requiredIds: ["checkId"],
2581
4112
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2599,7 +4130,7 @@ var v2AssessmentEntries = [
2599
4130
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2600
4131
  { name: "text", type: "string", required: true, description: "Source text." },
2601
4132
  { name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
2602
- ...assessmentBehaviourProps
4133
+ ...assessmentBehaviourProps2
2603
4134
  ],
2604
4135
  requiredIds: ["checkId"],
2605
4136
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2621,7 +4152,7 @@ var v2AssessmentEntries = [
2621
4152
  props: [
2622
4153
  { name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
2623
4154
  { name: "sequential", type: "boolean", required: false, description: "One question at a time." },
2624
- ...assessmentBehaviourProps.filter((p) => p.name !== "passingScore")
4155
+ ...assessmentBehaviourProps2.filter((p) => p.name !== "passingScore")
2625
4156
  ],
2626
4157
  requiredIds: [],
2627
4158
  parentConstraints: ["Lesson"],
@@ -2639,6 +4170,7 @@ var BLOCK_CATALOG_V2 = [
2639
4170
  ...BLOCK_CATALOG,
2640
4171
  ...v2AssessmentEntries
2641
4172
  ];
4173
+ var BLOCK_CATALOG_V3 = buildV3CatalogFromV2(BLOCK_CATALOG_V2);
2642
4174
  function cloneCatalogEntry(entry) {
2643
4175
  return {
2644
4176
  ...entry,
@@ -2646,6 +4178,7 @@ function cloneCatalogEntry(entry) {
2646
4178
  aliases: entry.aliases ? [...entry.aliases] : void 0,
2647
4179
  optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
2648
4180
  parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
4181
+ allowedChildTypes: entry.allowedChildTypes ? [...entry.allowedChildTypes] : void 0,
2649
4182
  a11y: { ...entry.a11y },
2650
4183
  theming: {
2651
4184
  ...entry.theming,
@@ -2658,35 +4191,49 @@ function cloneCatalogEntry(entry) {
2658
4191
  };
2659
4192
  }
2660
4193
  function buildBlockCatalog(opts) {
2661
- const version = opts?.version ?? 2;
2662
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4194
+ const version = opts?.version ?? 3;
4195
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2663
4196
  return source.map((entry) => cloneCatalogEntry(entry));
2664
4197
  }
2665
4198
  function getBlockCatalogEntry(type, opts) {
2666
- const version = opts?.version ?? 2;
2667
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4199
+ const version = opts?.version ?? 3;
4200
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2668
4201
  return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
2669
4202
  }
2670
4203
  // Annotate the CommonJS export names for ESM import in node:
2671
4204
  0 && (module.exports = {
4205
+ Accordion,
2672
4206
  AssessmentSequence,
2673
4207
  BLOCK_CATALOG,
2674
4208
  BLOCK_CATALOG_V2,
4209
+ BLOCK_CATALOG_V3,
2675
4210
  Course,
4211
+ DialogCards,
2676
4212
  DragAndDrop,
2677
4213
  DragTheWords,
2678
4214
  FillInTheBlanks,
4215
+ FindHotspot,
4216
+ FindMultipleHotspots,
4217
+ Flashcards,
4218
+ Heading,
4219
+ Image,
4220
+ ImageHotspots,
4221
+ ImageSlider,
4222
+ InteractiveBook,
2679
4223
  KnowledgeCheck,
2680
4224
  Lesson,
2681
4225
  LessonkitProvider,
2682
4226
  MarkTheWords,
4227
+ Page,
2683
4228
  ProgressTracker,
2684
4229
  Quiz,
2685
4230
  Reflection,
2686
4231
  Scenario,
4232
+ Text,
2687
4233
  ThemeProvider,
2688
4234
  TrueFalse,
2689
4235
  blockCatalogV2Version,
4236
+ blockCatalogV3Version,
2690
4237
  blockCatalogVersion,
2691
4238
  buildBlockCatalog,
2692
4239
  buildTelemetryEvent,