@lessonkit/react 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,33 +30,49 @@ 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
+ Slide: () => Slide,
61
+ SlideDeck: () => SlideDeck,
62
+ Text: () => Text,
48
63
  ThemeProvider: () => ThemeProvider,
49
64
  TrueFalse: () => TrueFalse,
50
65
  blockCatalogV2Version: () => blockCatalogV2Version,
66
+ blockCatalogV3Version: () => blockCatalogV3Version,
51
67
  blockCatalogVersion: () => blockCatalogVersion,
52
68
  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,
69
+ buildTelemetryEvent: () => import_core19.buildTelemetryEvent,
70
+ createLessonkitRuntime: () => import_core19.createLessonkitRuntime,
71
+ createPluginRegistry: () => import_core19.createPluginRegistry,
72
+ createTelemetryPipeline: () => import_core19.createTelemetryPipeline,
73
+ defineAssessmentPlugin: () => import_core19.defineAssessmentPlugin,
74
+ defineLifecyclePlugin: () => import_core19.defineLifecyclePlugin,
75
+ defineTelemetryPlugin: () => import_core19.defineTelemetryPlugin,
60
76
  getBlockCatalogEntry: () => getBlockCatalogEntry,
61
77
  resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
62
78
  resetQuizWarningsForTests: () => resetQuizWarningsForTests,
@@ -71,31 +87,8 @@ __export(index_exports, {
71
87
  module.exports = __toCommonJS(index_exports);
72
88
 
73
89
  // 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
- }
90
+ var import_react13 = require("react");
91
+ var import_accessibility2 = require("@lessonkit/accessibility");
99
92
 
100
93
  // src/context.tsx
101
94
  var import_react2 = require("react");
@@ -103,7 +96,39 @@ var import_react2 = require("react");
103
96
  // src/provider/useLessonkitProviderRuntime.ts
104
97
  var import_react = require("react");
105
98
  var import_core8 = require("@lessonkit/core");
106
- var import_xapi4 = require("@lessonkit/xapi");
99
+
100
+ // src/runtime/observability.ts
101
+ var import_xapi = require("@lessonkit/xapi");
102
+ function createXapiQueueFromObservability(observability) {
103
+ const opts = {};
104
+ if (observability?.onXapiQueueDepth) {
105
+ opts.onDepth = observability.onXapiQueueDepth;
106
+ }
107
+ if (observability?.onXapiQueueCap) {
108
+ opts.onCap = observability.onXapiQueueCap;
109
+ }
110
+ return (0, import_xapi.createInMemoryXAPIQueue)(opts);
111
+ }
112
+ function wrapTrackingSink(sink, observability) {
113
+ if (!sink || !observability?.onTelemetrySinkError) return sink;
114
+ const onError = observability.onTelemetrySinkError;
115
+ return ((event) => {
116
+ try {
117
+ const result = sink(event);
118
+ if (result != null && typeof result.catch === "function") {
119
+ return result.catch((err) => {
120
+ onError(err, { sinkId: "tracking" });
121
+ });
122
+ }
123
+ return result;
124
+ } catch (err) {
125
+ onError(err, { sinkId: "tracking" });
126
+ return void 0;
127
+ }
128
+ });
129
+ }
130
+
131
+ // src/provider/useLessonkitProviderRuntime.ts
107
132
  var import_xapi5 = require("@lessonkit/xapi");
108
133
 
109
134
  // src/runtime/emitTelemetry.ts
@@ -111,11 +136,20 @@ var import_core2 = require("@lessonkit/core");
111
136
 
112
137
  // src/runtime/telemetryPipeline.ts
113
138
  var import_core = require("@lessonkit/core");
114
- var import_xapi = require("@lessonkit/xapi");
139
+ var import_xapi2 = require("@lessonkit/xapi");
115
140
 
116
141
  // src/runtime/lxpackBridge.ts
117
142
  var import_bridge = require("@lessonkit/lxpack/bridge");
118
- function forwardTelemetryToLxpack(event, mode = "auto") {
143
+ var BRIDGE_MISS_EVENT_NAMES = /* @__PURE__ */ new Set([
144
+ "course_completed",
145
+ "lesson_completed",
146
+ "assessment_completed",
147
+ "quiz_completed"
148
+ ]);
149
+ function forwardTelemetryToLxpack(event, mode = "auto", opts) {
150
+ if (mode === "auto" && opts?.onBridgeMiss && BRIDGE_MISS_EVENT_NAMES.has(event.name) && !(0, import_bridge.getLxpackBridge)()) {
151
+ opts.onBridgeMiss(event);
152
+ }
119
153
  (0, import_bridge.forwardTelemetryToBridge)(event, mode);
120
154
  }
121
155
 
@@ -131,7 +165,7 @@ function createLegacyPipeline(opts, extraSinks = []) {
131
165
  id: "xapi",
132
166
  emit(event) {
133
167
  try {
134
- const statement = (0, import_xapi.telemetryEventToXAPIStatement)(event);
168
+ const statement = (0, import_xapi2.telemetryEventToXAPIStatement)(event);
135
169
  if (statement) opts.xapi?.send(statement);
136
170
  } catch (err) {
137
171
  if (isDevEnvironment()) {
@@ -146,7 +180,9 @@ function createLegacyPipeline(opts, extraSinks = []) {
146
180
  {
147
181
  id: "lxpack-bridge",
148
182
  emit(event) {
149
- forwardTelemetryToLxpack(event, opts.lxpackBridge);
183
+ forwardTelemetryToLxpack(event, opts.lxpackBridge, {
184
+ onBridgeMiss: opts.onLxpackBridgeMiss
185
+ });
150
186
  }
151
187
  },
152
188
  ...extraSinks
@@ -173,7 +209,8 @@ function emitTelemetry(tracking, xapi, event, opts) {
173
209
  const legacy = {
174
210
  tracking,
175
211
  xapi,
176
- lxpackBridge: opts?.lxpackBridge ?? "auto"
212
+ lxpackBridge: opts?.lxpackBridge ?? "auto",
213
+ onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
177
214
  };
178
215
  emitThroughPipeline(event, legacy, opts?.extraSinks);
179
216
  }
@@ -181,18 +218,21 @@ function emitTelemetry(tracking, xapi, event, opts) {
181
218
  // src/runtime/ports.ts
182
219
  var import_core3 = require("@lessonkit/core");
183
220
 
221
+ // src/provider/useLessonkitProviderRuntime.ts
222
+ var import_core9 = require("@lessonkit/core");
223
+
184
224
  // src/runtime/progress.ts
185
225
  var import_core4 = require("@lessonkit/core");
186
226
 
187
227
  // src/runtime/xapi.ts
188
- var import_xapi2 = require("@lessonkit/xapi");
228
+ var import_xapi3 = require("@lessonkit/xapi");
189
229
  function createXapiClientFromConfig(config, queue) {
190
230
  if (config.xapi?.enabled === false) return null;
191
231
  if (config.xapi?.client) return config.xapi.client;
192
232
  if (!config.courseId) return null;
193
233
  const hasTransport = typeof config.xapi?.transport === "function";
194
234
  if (!hasTransport && config.xapi?.enabled !== true) return null;
195
- return (0, import_xapi2.createXAPIClient)({
235
+ return (0, import_xapi3.createXAPIClient)({
196
236
  courseId: config.courseId,
197
237
  transport: config.xapi?.transport,
198
238
  queue
@@ -203,7 +243,7 @@ function createXapiClientFromConfig(config, queue) {
203
243
  var import_core5 = require("@lessonkit/core");
204
244
 
205
245
  // src/runtime/courseStartedPipeline.ts
206
- var import_xapi3 = require("@lessonkit/xapi");
246
+ var import_xapi4 = require("@lessonkit/xapi");
207
247
  function isDevEnvironment3() {
208
248
  const g = globalThis;
209
249
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
@@ -240,13 +280,15 @@ async function emitExtraSinks(sinks, event, emitCtx) {
240
280
  async function emitCourseStartedNonTrackingPipeline(opts) {
241
281
  let xapiStatementSent = false;
242
282
  if (!opts.skipXapi && opts.xapi) {
243
- const statement = (0, import_xapi3.telemetryEventToXAPIStatement)(opts.event);
283
+ const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(opts.event);
244
284
  if (statement) {
245
285
  opts.xapi.send(statement);
246
286
  xapiStatementSent = true;
247
287
  }
248
288
  }
249
- forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
289
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
290
+ onBridgeMiss: opts.onLxpackBridgeMiss
291
+ });
250
292
  const emitCtx = {
251
293
  courseId: opts.event.courseId,
252
294
  sessionId: opts.event.sessionId,
@@ -263,50 +305,19 @@ function createReactPluginHost(plugins) {
263
305
  return (0, import_core6.createPluginRegistry)(plugins);
264
306
  }
265
307
  function buildPluginContext(opts) {
266
- return {
267
- courseId: opts.courseId,
268
- sessionId: opts.sessionId,
269
- attemptId: opts.attemptId,
270
- user: opts.user
271
- };
308
+ return (0, import_core6.buildPluginContext)(opts);
272
309
  }
273
310
  function emitTelemetryWithPlugins(opts) {
274
311
  const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
275
312
  if (next === null) return;
276
313
  emitTelemetry(opts.tracking, opts.xapi, next, {
277
314
  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
315
+ extraSinks: opts.extraSinks,
316
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss
291
317
  });
292
318
  }
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
319
 
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)();
320
+ // src/provider/courseStarted/emit.ts
310
321
  var courseStartedTrackingFlightKey = null;
311
322
  function isTrackingActive(tracking) {
312
323
  return tracking?.enabled !== false;
@@ -342,9 +353,10 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
342
353
  try {
343
354
  if (shouldCommit && !shouldCommit()) return false;
344
355
  tracking.track(event);
345
- await tracking.flush?.();
346
- if (shouldCommit && !shouldCommit()) return false;
347
356
  (0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId);
357
+ const delivered = await tracking.flush?.();
358
+ if (delivered === false) return false;
359
+ if (shouldCommit && !shouldCommit()) return false;
348
360
  return true;
349
361
  } catch {
350
362
  return false;
@@ -361,6 +373,7 @@ async function emitCourseStartedPipelineOnly(opts) {
361
373
  event: opts.event,
362
374
  xapi: opts.xapi,
363
375
  lxpackBridge: opts.lxpackBridge,
376
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
364
377
  extraSinks: opts.extraSinks,
365
378
  skipXapi: opts.skipXapi
366
379
  });
@@ -413,6 +426,7 @@ async function emitCourseStartedToTrackingOnly(opts) {
413
426
  event,
414
427
  xapi: null,
415
428
  lxpackBridge: opts.lxpackBridge,
429
+ onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
416
430
  extraSinks: opts.extraSinks,
417
431
  skipXapi: true
418
432
  });
@@ -466,6 +480,36 @@ function assertTrackingSinkConfig(tracking) {
466
480
  "[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
467
481
  );
468
482
  }
483
+
484
+ // src/runtime/telemetry.ts
485
+ var import_core7 = require("@lessonkit/core");
486
+ function createTrackingClientFromConfig(config, observability) {
487
+ if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
488
+ if (config.tracking?.createClient) return config.tracking.createClient();
489
+ return (0, import_core7.createTrackingClient)({
490
+ sink: config.tracking?.sink,
491
+ batchSink: config.tracking?.batchSink,
492
+ batch: config.tracking?.batch,
493
+ onBufferDrop: observability?.onTelemetryBufferDrop
494
+ });
495
+ }
496
+ async function disposeTrackingClient(client) {
497
+ try {
498
+ await client?.flush?.();
499
+ } catch {
500
+ }
501
+ try {
502
+ await client?.dispose?.();
503
+ } catch {
504
+ }
505
+ }
506
+
507
+ // src/provider/useLessonkitProviderRuntime.ts
508
+ var useIsoLayoutEffect = (
509
+ /* v8 ignore next -- SSR uses useEffect when window is unavailable */
510
+ typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect
511
+ );
512
+ var defaultStorage = (0, import_core3.createSessionStoragePort)();
469
513
  function useLessonkitProviderRuntime(config) {
470
514
  const normalizedCourseId = (0, import_react.useMemo)(
471
515
  () => (0, import_core8.assertValidId)(config.courseId, "courseId"),
@@ -476,6 +520,14 @@ function useLessonkitProviderRuntime(config) {
476
520
  [config, normalizedCourseId]
477
521
  );
478
522
  const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
523
+ (0, import_react.useEffect)(() => {
524
+ if (useV2Runtime) return;
525
+ const g = globalThis;
526
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
527
+ console.warn(
528
+ '[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
529
+ );
530
+ }, [useV2Runtime]);
479
531
  const extraSinksRef = (0, import_react.useRef)(normalizedConfig.sinks);
480
532
  extraSinksRef.current = normalizedConfig.sinks;
481
533
  const headlessRef = (0, import_react.useRef)(null);
@@ -494,7 +546,16 @@ function useLessonkitProviderRuntime(config) {
494
546
  courseIdRef.current = normalizedCourseId;
495
547
  const lxpackBridgeModeRef = (0, import_react.useRef)(normalizedConfig.lxpack?.bridge ?? "auto");
496
548
  lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
497
- const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
549
+ const observabilityRef = (0, import_react.useRef)(normalizedConfig.observability);
550
+ observabilityRef.current = normalizedConfig.observability;
551
+ const onLxpackBridgeMiss = (0, import_react.useCallback)((event) => {
552
+ observabilityRef.current?.onLxpackBridgeMiss?.(event);
553
+ }, []);
554
+ const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
555
+ const pluginHost = (0, import_react.useMemo)(
556
+ () => createReactPluginHost(normalizedConfig.plugins),
557
+ [pluginsFingerprint]
558
+ );
498
559
  const pluginHostRef = (0, import_react.useRef)(pluginHost);
499
560
  pluginHostRef.current = pluginHost;
500
561
  const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
@@ -510,7 +571,9 @@ function useLessonkitProviderRuntime(config) {
510
571
  headlessRef.current = (0, import_core8.createLessonkitRuntime)({
511
572
  courseId: normalizedCourseId,
512
573
  runtimeVersion: "v2",
513
- session: normalizedConfig.session
574
+ session: normalizedConfig.session,
575
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
576
+ deferPluginSetup: true
514
577
  });
515
578
  progressRef.current = headlessRef.current.progress;
516
579
  } else {
@@ -524,7 +587,9 @@ function useLessonkitProviderRuntime(config) {
524
587
  headlessRef.current = (0, import_core8.createLessonkitRuntime)({
525
588
  courseId: normalizedCourseId,
526
589
  runtimeVersion: "v2",
527
- session: normalizedConfig.session
590
+ session: normalizedConfig.session,
591
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins,
592
+ deferPluginSetup: true
528
593
  });
529
594
  }
530
595
  if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
@@ -548,7 +613,7 @@ function useLessonkitProviderRuntime(config) {
548
613
  }, []);
549
614
  const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
550
615
  activeLessonIdRef.current = progress.activeLessonId;
551
- const xapiQueueRef = (0, import_react.useRef)((0, import_xapi4.createInMemoryXAPIQueue)());
616
+ const xapiQueueRef = (0, import_react.useRef)(createXapiQueueFromObservability(normalizedConfig.observability));
552
617
  const xapiRef = (0, import_react.useRef)(null);
553
618
  const [xapi, setXapi] = (0, import_react.useState)(null);
554
619
  const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
@@ -569,7 +634,7 @@ function useLessonkitProviderRuntime(config) {
569
634
  }
570
635
  void xapiRef.current?.flush();
571
636
  }
572
- xapiQueueRef.current = (0, import_xapi4.createInMemoryXAPIQueue)();
637
+ xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
573
638
  prevXapiCourseIdRef.current = courseId;
574
639
  xapiCourseStartedSentOnClientRef.current = false;
575
640
  }
@@ -648,7 +713,7 @@ function useLessonkitProviderRuntime(config) {
648
713
  );
649
714
  useIsoLayoutEffect(() => {
650
715
  const prev = trackingRef.current;
651
- const baseSink = normalizedConfig.tracking?.sink;
716
+ const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
652
717
  const userBatchSink = normalizedConfig.tracking?.batchSink;
653
718
  assertTrackingSinkConfig(normalizedConfig.tracking);
654
719
  const sink = pluginHostRef.current && baseSink ? (
@@ -669,9 +734,12 @@ function useLessonkitProviderRuntime(config) {
669
734
  }
670
735
  return userBatchSink(perEventForBatch);
671
736
  } : userBatchSink;
672
- const next = createTrackingClientFromConfig({
673
- tracking: { ...normalizedConfig.tracking, sink, batchSink }
674
- });
737
+ const next = createTrackingClientFromConfig(
738
+ {
739
+ tracking: { ...normalizedConfig.tracking, sink, batchSink }
740
+ },
741
+ observabilityRef.current
742
+ );
675
743
  trackingRef.current = next;
676
744
  trackingClientForUnmountRef.current = next;
677
745
  setTracking(next);
@@ -698,6 +766,7 @@ function useLessonkitProviderRuntime(config) {
698
766
  attemptId: attemptIdRef.current,
699
767
  user: userRef.current,
700
768
  lxpackBridge: lxpackBridgeModeRef.current,
769
+ onLxpackBridgeMiss,
701
770
  extraSinks: extraSinksRef.current,
702
771
  skipXapi: xapiCourseStartedSentOnClientRef.current,
703
772
  onXapiStatementSent: () => {
@@ -739,9 +808,10 @@ function useLessonkitProviderRuntime(config) {
739
808
  user: userRef.current
740
809
  }),
741
810
  lxpackBridge: lxpackBridgeModeRef.current,
811
+ onLxpackBridgeMiss,
742
812
  extraSinks: extraSinksRef.current
743
813
  });
744
- }, []);
814
+ }, [onLxpackBridgeMiss]);
745
815
  const emitLifecycleEvent = (0, import_react.useCallback)(
746
816
  (name, data, lessonId) => {
747
817
  const event = (0, import_core2.tryBuildTelemetryEvent)({
@@ -797,12 +867,13 @@ function useLessonkitProviderRuntime(config) {
797
867
  attemptId: attemptIdRef.current,
798
868
  user: userRef.current,
799
869
  lxpackBridge: lxpackBridgeModeRef.current,
870
+ onLxpackBridgeMiss,
800
871
  extraSinks: extraSinksRef.current
801
872
  });
802
873
  courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
803
874
  }
804
875
  })();
805
- }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
876
+ }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
806
877
  const emitLessonCompleted = (0, import_react.useCallback)(
807
878
  (lessonId, durationMs) => {
808
879
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -851,6 +922,22 @@ function useLessonkitProviderRuntime(config) {
851
922
  })();
852
923
  };
853
924
  }, []);
925
+ (0, import_react.useEffect)(() => {
926
+ if (typeof document === "undefined") return;
927
+ const flushOnExit = () => {
928
+ void xapiRef.current?.flush();
929
+ void trackingRef.current?.flush?.();
930
+ };
931
+ const onVisibilityChange = () => {
932
+ if (document.visibilityState === "hidden") flushOnExit();
933
+ };
934
+ document.addEventListener("visibilitychange", onVisibilityChange);
935
+ window.addEventListener("pagehide", flushOnExit);
936
+ return () => {
937
+ document.removeEventListener("visibilitychange", onVisibilityChange);
938
+ window.removeEventListener("pagehide", flushOnExit);
939
+ };
940
+ }, []);
854
941
  const setActiveLesson = (0, import_react.useCallback)(
855
942
  (lessonId) => {
856
943
  if (useV2Runtime && headlessRef.current) {
@@ -914,20 +1001,34 @@ function useLessonkitProviderRuntime(config) {
914
1001
  session: normalizedConfig.session
915
1002
  });
916
1003
  }
917
- }, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
1004
+ }, [
1005
+ useV2Runtime,
1006
+ normalizedCourseId,
1007
+ sessionAttemptId,
1008
+ sessionConfiguredId,
1009
+ sessionUserKey,
1010
+ normalizedConfig.session
1011
+ ]);
1012
+ (0, import_react.useEffect)(() => {
1013
+ if (!useV2Runtime || !headlessRef.current) return;
1014
+ headlessRef.current.updateConfig({
1015
+ plugins: pluginHostRef.current ?? normalizedConfig.plugins
1016
+ });
1017
+ }, [useV2Runtime, pluginHost]);
918
1018
  (0, import_react.useEffect)(() => {
919
- if (!pluginHost) return;
1019
+ const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
1020
+ if (!host) return;
920
1021
  const ctx = buildPluginContext({
921
1022
  courseId: courseIdRef.current,
922
1023
  sessionId: sessionIdRef.current,
923
1024
  attemptId: attemptIdRef.current,
924
1025
  user: userRef.current
925
1026
  });
926
- pluginHost.setupAll(ctx);
1027
+ host.setupAll(ctx);
927
1028
  return () => {
928
- pluginHost.disposeAll();
1029
+ host.disposeAll();
929
1030
  };
930
- }, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
1031
+ }, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
931
1032
  (0, import_react.useEffect)(() => {
932
1033
  const nextConfigured = normalizedConfig.session?.sessionId;
933
1034
  const prevConfigured = prevConfiguredSessionIdRef.current;
@@ -956,6 +1057,7 @@ function useLessonkitProviderRuntime(config) {
956
1057
  config: normalizedConfig,
957
1058
  tracking,
958
1059
  xapi,
1060
+ storage: defaultStorage,
959
1061
  session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
960
1062
  progress,
961
1063
  setActiveLesson,
@@ -1053,17 +1155,17 @@ function useEnclosingLessonId() {
1053
1155
  }
1054
1156
 
1055
1157
  // src/runtime/validateComponentId.ts
1056
- var import_core9 = require("@lessonkit/core");
1158
+ var import_core10 = require("@lessonkit/core");
1057
1159
  function isDevEnvironment4() {
1058
1160
  const g = globalThis;
1059
1161
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
1060
1162
  }
1061
1163
  function normalizeComponentId(id, path) {
1062
- if (path === "courseId") return (0, import_core9.assertValidId)(id, "courseId");
1063
- if (path === "lessonId") return (0, import_core9.assertValidId)(id, "lessonId");
1064
- if (path === "checkId") return (0, import_core9.assertValidId)(id, "checkId");
1065
- if (path === "blockId") return (0, import_core9.assertValidId)(id, "blockId");
1066
- return (0, import_core9.assertValidId)(id, path);
1164
+ if (path === "courseId") return (0, import_core10.assertValidId)(id, "courseId");
1165
+ if (path === "lessonId") return (0, import_core10.assertValidId)(id, "lessonId");
1166
+ if (path === "checkId") return (0, import_core10.assertValidId)(id, "checkId");
1167
+ if (path === "blockId") return (0, import_core10.assertValidId)(id, "blockId");
1168
+ return (0, import_core10.assertValidId)(id, path);
1067
1169
  }
1068
1170
 
1069
1171
  // src/runtime/lessonMountRegistry.ts
@@ -1090,188 +1192,451 @@ function getLessonMountCount(lessonId) {
1090
1192
  return mountCounts.get(lessonId) ?? 0;
1091
1193
  }
1092
1194
 
1093
- // src/components.tsx
1195
+ // src/components/Quiz.tsx
1196
+ var import_react12 = require("react");
1197
+ var import_accessibility = require("@lessonkit/accessibility");
1198
+
1199
+ // src/assessment/AssessmentLessonGuard.tsx
1200
+ var import_react6 = require("react");
1094
1201
  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
- );
1202
+ var warnedAssessmentOutsideLesson = false;
1203
+ function resetAssessmentWarningsForTests() {
1204
+ warnedAssessmentOutsideLesson = false;
1196
1205
  }
1197
- function Quiz(props) {
1206
+ function AssessmentLessonGuard(props) {
1198
1207
  const enclosingLessonId = useEnclosingLessonId();
1199
1208
  const missingLesson = enclosingLessonId === void 0;
1200
1209
  (0, import_react6.useEffect)(() => {
1201
1210
  if (!missingLesson || isDevEnvironment4()) return;
1202
- if (!warnedQuizOutsideLesson) {
1203
- warnedQuizOutsideLesson = true;
1211
+ if (!warnedAssessmentOutsideLesson) {
1212
+ warnedAssessmentOutsideLesson = true;
1204
1213
  console.error(
1205
- "[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
1214
+ `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1206
1215
  );
1207
1216
  }
1208
- }, [missingLesson]);
1217
+ }, [missingLesson, props.blockLabel]);
1209
1218
  if (missingLesson && isDevEnvironment4()) {
1210
- throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1219
+ throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1211
1220
  }
1212
1221
  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." }) });
1222
+ 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: [
1223
+ props.blockLabel,
1224
+ " must be placed inside a Lesson."
1225
+ ] }) });
1214
1226
  }
1215
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(QuizInner, { ...props, enclosingLessonId });
1227
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.children(enclosingLessonId) });
1216
1228
  }
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;
1229
+
1230
+ // src/assessment/internal/buildAssessmentHandle.ts
1231
+ function buildAssessmentHandle(opts) {
1232
+ return {
1233
+ getScore: opts.getScore,
1234
+ getMaxScore: opts.getMaxScore,
1235
+ getAnswerGiven: opts.getAnswerGiven,
1236
+ resetTask: opts.resetTask,
1237
+ showSolutions: opts.showSolutions,
1238
+ getXAPIData: opts.getXAPIData,
1239
+ ...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
1240
+ ...opts.resume ? { resume: opts.resume } : {}
1241
1241
  };
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);
1242
+ }
1243
+
1244
+ // src/assessment/internal/resumeState.ts
1245
+ function readBooleanField(state, key) {
1246
+ const value = state[key];
1247
+ if (value === true || value === false || value === null) return value;
1248
+ return void 0;
1249
+ }
1250
+ function readStringField(state, key) {
1251
+ const value = state[key];
1252
+ if (typeof value === "string" || value === null) return value;
1253
+ return void 0;
1254
+ }
1255
+ function readNumberField(state, key) {
1256
+ const value = state[key];
1257
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1258
+ if (value === null) return null;
1259
+ return void 0;
1260
+ }
1261
+ function readBooleanStateField(state, key, apply) {
1262
+ const value = state[key];
1263
+ if (typeof value === "boolean") apply(value);
1264
+ }
1265
+
1266
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1267
+ var import_react10 = require("react");
1268
+
1269
+ // src/compound/CompoundProvider.tsx
1270
+ var import_react9 = __toESM(require("react"), 1);
1271
+ var import_core11 = require("@lessonkit/core");
1272
+
1273
+ // src/compound/aggregateScores.ts
1274
+ function aggregateAssessmentScores(handles, opts) {
1275
+ let score = 0;
1276
+ let maxScore = 0;
1277
+ let allAnswered = true;
1278
+ for (const entry of handles) {
1279
+ const handle = "handle" in entry ? entry.handle : entry;
1280
+ const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
1281
+ score += handle.getScore();
1282
+ maxScore += handle.getMaxScore();
1283
+ const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
1284
+ if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
1285
+ }
1286
+ return { score, maxScore, allAnswered };
1287
+ }
1288
+
1289
+ // src/compound/CompoundHydrationBridge.tsx
1290
+ var import_react7 = require("react");
1291
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1292
+ var CompoundHydrationBridgeContext = (0, import_react7.createContext)(
1293
+ null
1294
+ );
1295
+ function CompoundHydrationBridgeProvider({ children }) {
1296
+ const bridgeRef = (0, import_react7.useRef)(null);
1297
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
1298
+ }
1299
+ function useCompoundHydrationBridgeRef() {
1300
+ return (0, import_react7.useContext)(CompoundHydrationBridgeContext);
1301
+ }
1302
+
1303
+ // src/compound/CompoundPageIndexContext.tsx
1304
+ var import_react8 = require("react");
1305
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1306
+ var CompoundPageIndexContext = (0, import_react8.createContext)(void 0);
1307
+ function CompoundPageIndexProvider({
1308
+ pageIndex,
1309
+ children
1310
+ }) {
1311
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CompoundPageIndexContext.Provider, { value: pageIndex, children });
1312
+ }
1313
+ function useCompoundPageIndex() {
1314
+ return (0, import_react8.useContext)(CompoundPageIndexContext);
1315
+ }
1316
+
1317
+ // src/compound/CompoundProvider.tsx
1318
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1319
+ var CompoundRegistryContext = (0, import_react9.createContext)(null);
1320
+ var CompoundHandlesVersionContext = (0, import_react9.createContext)(0);
1321
+ function CompoundProvider({
1322
+ children,
1323
+ activePageIndex: _activePageIndex,
1324
+ onActivePageIndexChange: _onActivePageIndexChange
1325
+ }) {
1326
+ const registryRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
1327
+ const [handlesVersion, setHandlesVersion] = (0, import_react9.useState)(0);
1328
+ const register = (0, import_react9.useCallback)((checkId, handle, pageIndex) => {
1329
+ const prev = registryRef.current.get(checkId);
1330
+ if (prev && prev.handle !== handle) {
1331
+ const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
1332
+ if (isDevEnvironment4()) {
1333
+ console.error(message);
1334
+ } else {
1335
+ console.warn(message);
1336
+ }
1337
+ }
1338
+ registryRef.current.set(checkId, { handle, pageIndex });
1339
+ if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
1340
+ setHandlesVersion((v) => v + 1);
1341
+ }
1342
+ return () => {
1343
+ const current = registryRef.current.get(checkId);
1344
+ if (current?.handle === handle) {
1345
+ registryRef.current.delete(checkId);
1346
+ setHandlesVersion((v) => v + 1);
1347
+ }
1348
+ };
1349
+ }, []);
1350
+ const registryValue = (0, import_react9.useMemo)(
1351
+ () => ({
1352
+ register,
1353
+ getHandles: () => {
1354
+ const handles = /* @__PURE__ */ new Map();
1355
+ for (const [checkId, entry] of registryRef.current) {
1356
+ handles.set(checkId, entry.handle);
1357
+ }
1358
+ return handles;
1359
+ },
1360
+ getRegisteredHandles: () => registryRef.current
1361
+ }),
1362
+ [register]
1363
+ );
1364
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
1365
+ }
1366
+ function useCompoundRegistry() {
1367
+ const registry = (0, import_react9.useContext)(CompoundRegistryContext);
1368
+ const handlesVersion = (0, import_react9.useContext)(CompoundHandlesVersionContext);
1369
+ if (!registry) return null;
1370
+ return { ...registry, handlesVersion };
1371
+ }
1372
+ function useCompoundHandlesVersion() {
1373
+ return (0, import_react9.useContext)(CompoundHandlesVersionContext);
1374
+ }
1375
+ function useRegisterAssessmentHandle(checkId, handle) {
1376
+ const registry = (0, import_react9.useContext)(CompoundRegistryContext);
1377
+ const pageIndex = useCompoundPageIndex();
1378
+ import_react9.default.useLayoutEffect(() => {
1379
+ if (!registry || !handle) return;
1380
+ return registry.register(checkId, handle, pageIndex);
1381
+ }, [registry, checkId, handle, pageIndex]);
1382
+ }
1383
+ function useCompoundHandleRef(ref, opts) {
1384
+ const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
1385
+ const bridgeRef = useCompoundHydrationBridgeRef();
1386
+ const setIndexClamped = (0, import_react9.useCallback)(
1387
+ (index) => {
1388
+ const next = pageCount !== void 0 ? (0, import_core11.clampCompoundPageIndex)(index, pageCount) : Math.max(0, Math.floor(index));
1389
+ setActivePageIndex(next);
1390
+ },
1391
+ [pageCount, setActivePageIndex]
1392
+ );
1393
+ (0, import_react9.useImperativeHandle)(
1394
+ ref,
1395
+ () => ({
1396
+ getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
1397
+ getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
1398
+ getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
1399
+ answerPageIndex: activePageIndex
1400
+ }).allAnswered,
1401
+ resetTask: () => {
1402
+ for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
1403
+ },
1404
+ showSolutions: () => {
1405
+ if (!opts.enableSolutionsButton) return;
1406
+ for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
1407
+ },
1408
+ getCurrentState: () => {
1409
+ const childStates = {};
1410
+ for (const [checkId, entry] of getRegisteredHandles()) {
1411
+ if (entry.handle.getCurrentState) {
1412
+ childStates[checkId] = entry.handle.getCurrentState();
1413
+ }
1414
+ }
1415
+ return (0, import_core11.createCompoundResumeState)({ activePageIndex, childStates });
1416
+ },
1417
+ resume: (state) => {
1418
+ bridgeRef?.current?.notifyImperativeResume(state);
1419
+ }
1420
+ }),
1421
+ [activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
1422
+ );
1423
+ }
1424
+
1425
+ // src/assessment/internal/useAssessmentHandleRegistration.ts
1426
+ function useAssessmentHandleRegistration(checkId, handle, ref) {
1427
+ (0, import_react10.useImperativeHandle)(ref, () => handle, [handle]);
1428
+ useRegisterAssessmentHandle(checkId, handle);
1429
+ }
1430
+
1431
+ // src/assessment/internal/usePluginScoring.ts
1432
+ var import_react11 = require("react");
1433
+
1434
+ // src/assessment/scoring.ts
1435
+ function resolvePassingThreshold(passingScore, maxScore) {
1436
+ return passingScore ?? maxScore;
1437
+ }
1438
+ function meetsPassingThreshold(score, maxScore, passingScore) {
1439
+ const threshold = resolvePassingThreshold(passingScore, maxScore);
1440
+ return score >= threshold;
1441
+ }
1442
+ function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
1443
+ const maxScore = custom?.maxScore ?? fallbackMax;
1444
+ if (custom?.passed !== void 0) {
1445
+ const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
1446
+ return { score: score2, maxScore, passed: custom.passed };
1447
+ }
1448
+ if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1449
+ const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1450
+ return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
1451
+ }
1452
+ const score = fallbackCorrect ? maxScore : 0;
1453
+ const passed = meetsPassingThreshold(score, maxScore, passingScore);
1454
+ return { score, maxScore, passed };
1455
+ }
1456
+
1457
+ // src/assessment/internal/usePluginScoring.ts
1458
+ function usePluginScoring(checkId, lessonId) {
1459
+ const { plugins, config, session } = useLessonkit();
1460
+ const getPluginScore = (0, import_react11.useCallback)(
1461
+ (response) => {
1462
+ const pluginCtx = buildPluginContext({
1463
+ courseId: config.courseId,
1464
+ sessionId: session.sessionId,
1465
+ attemptId: session.attemptId,
1466
+ user: session.user
1467
+ });
1468
+ return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
1469
+ },
1470
+ [checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
1471
+ );
1472
+ const scoreResponse = (0, import_react11.useCallback)(
1473
+ (response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
1474
+ [getPluginScore]
1475
+ );
1476
+ const isChoiceCorrect = (0, import_react11.useCallback)(
1477
+ (choice, answer, custom, passingScore) => {
1478
+ if (!custom) return choice === answer;
1479
+ if (custom.passed !== void 0) return custom.passed;
1480
+ if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1481
+ return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1482
+ }
1483
+ return choice === answer;
1484
+ },
1485
+ []
1486
+ );
1487
+ return { getPluginScore, scoreResponse, isChoiceCorrect };
1488
+ }
1489
+
1490
+ // src/components/Quiz.tsx
1491
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1492
+ function QuizInner(props, ref) {
1493
+ const { enclosingLessonId } = props;
1494
+ const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1495
+ const quiz = useQuizState(enclosingLessonId);
1496
+ const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
1497
+ const [selected, setSelected] = (0, import_react12.useState)(null);
1498
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react12.useState)(null);
1499
+ const [quizPassed, setQuizPassed] = (0, import_react12.useState)(false);
1500
+ const [completedScore, setCompletedScore] = (0, import_react12.useState)(null);
1501
+ const [completedMaxScore, setCompletedMaxScore] = (0, import_react12.useState)(null);
1502
+ const completedRef = (0, import_react12.useRef)(false);
1503
+ const telemetryReplayedRef = (0, import_react12.useRef)(false);
1504
+ const questionId = (0, import_react12.useId)();
1505
+ const choicesKey = props.choices.join("\0");
1506
+ (0, import_react12.useEffect)(() => {
1507
+ completedRef.current = false;
1508
+ telemetryReplayedRef.current = false;
1509
+ setQuizPassed(false);
1510
+ setSelected(null);
1511
+ setSelectionCorrect(null);
1512
+ setCompletedScore(null);
1513
+ setCompletedMaxScore(null);
1514
+ }, [checkId, props.answer, props.question, choicesKey]);
1515
+ const passed = quizPassed;
1516
+ const resolveScores = () => {
1517
+ const maxScore = completedMaxScore ?? 1;
1518
+ if (quizPassed) {
1519
+ return { score: completedScore ?? maxScore, maxScore };
1520
+ }
1521
+ if (selected !== null && selectionCorrect) {
1522
+ return { score: completedMaxScore ?? maxScore, maxScore };
1523
+ }
1524
+ return { score: 0, maxScore };
1525
+ };
1526
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1527
+ if (!nextPassed || telemetryReplayedRef.current) return;
1528
+ telemetryReplayedRef.current = true;
1529
+ if (nextSelected !== null) {
1530
+ quiz.answer({
1531
+ checkId,
1532
+ question: props.question,
1533
+ choice: nextSelected,
1534
+ correct: nextCorrect ?? false
1535
+ });
1536
+ }
1537
+ quiz.complete({
1538
+ checkId,
1539
+ score: nextScore,
1540
+ maxScore: nextMaxScore,
1541
+ passingScore: props.passingScore ?? nextMaxScore
1542
+ });
1543
+ };
1544
+ const handle = (0, import_react12.useMemo)(
1545
+ () => buildAssessmentHandle({
1546
+ checkId,
1547
+ getScore: () => resolveScores().score,
1548
+ getMaxScore: () => resolveScores().maxScore,
1549
+ getAnswerGiven: () => selected !== null,
1550
+ resetTask: () => {
1551
+ completedRef.current = false;
1552
+ telemetryReplayedRef.current = false;
1553
+ setQuizPassed(false);
1554
+ setSelected(null);
1555
+ setSelectionCorrect(null);
1556
+ setCompletedScore(null);
1557
+ setCompletedMaxScore(null);
1558
+ },
1559
+ showSolutions: () => {
1560
+ },
1561
+ getXAPIData: () => {
1562
+ const { score, maxScore } = resolveScores();
1563
+ return {
1564
+ checkId,
1565
+ interactionType: "mcq",
1566
+ response: selected ?? void 0,
1567
+ correct: selectionCorrect ?? void 0,
1568
+ score,
1569
+ maxScore
1570
+ };
1571
+ },
1572
+ getCurrentState: () => ({
1573
+ selected,
1574
+ selectionCorrect,
1575
+ quizPassed,
1576
+ completedScore,
1577
+ completedMaxScore
1578
+ }),
1579
+ resume: (state) => {
1580
+ const nextSelected = readStringField(state, "selected");
1581
+ if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
1582
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1583
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1584
+ setSelectionCorrect(nextCorrect);
1585
+ }
1586
+ const nextCompletedScore = readNumberField(state, "completedScore");
1587
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1588
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1589
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1590
+ const nextPassed = readBooleanField(state, "quizPassed");
1591
+ if (nextPassed === true || nextPassed === false) {
1592
+ setQuizPassed(nextPassed);
1593
+ completedRef.current = nextPassed;
1594
+ if (nextPassed) {
1595
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1596
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
1597
+ replayTelemetry(
1598
+ nextSelected ?? null,
1599
+ nextCorrect ?? null,
1600
+ nextPassed,
1601
+ score,
1602
+ maxScore
1603
+ );
1604
+ }
1605
+ }
1606
+ }
1607
+ }),
1608
+ [
1609
+ checkId,
1610
+ completedMaxScore,
1611
+ completedScore,
1612
+ props.passingScore,
1613
+ props.question,
1614
+ quiz,
1615
+ quizPassed,
1616
+ selected,
1617
+ selectionCorrect
1618
+ ]
1619
+ );
1620
+ useAssessmentHandleRegistration(checkId, handle, ref);
1621
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1622
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: questionId, children: props.question }),
1623
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1624
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
1625
+ props.choices.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { style: { display: "block" }, children: [
1626
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1627
+ "input",
1628
+ {
1629
+ type: "radio",
1630
+ name: questionId,
1631
+ value: c,
1632
+ checked: selected === c,
1633
+ disabled: passed,
1634
+ "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1635
+ onChange: () => {
1636
+ if (passed) return;
1637
+ setSelected(c);
1638
+ const custom = getPluginScore(c);
1639
+ const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
1275
1640
  setSelectionCorrect(correct);
1276
1641
  quiz.answer({
1277
1642
  checkId,
@@ -1283,9 +1648,12 @@ function QuizInner(props) {
1283
1648
  completedRef.current = true;
1284
1649
  setQuizPassed(true);
1285
1650
  const maxScore = custom?.maxScore ?? 1;
1651
+ const score = custom?.score ?? maxScore;
1652
+ setCompletedScore(score);
1653
+ setCompletedMaxScore(maxScore);
1286
1654
  quiz.complete({
1287
1655
  checkId,
1288
- score: custom?.score ?? maxScore,
1656
+ score,
1289
1657
  maxScore,
1290
1658
  passingScore: props.passingScore ?? maxScore
1291
1659
  });
@@ -1296,7 +1664,115 @@ function QuizInner(props) {
1296
1664
  c
1297
1665
  ] }, `${questionId}-${i}`))
1298
1666
  ] }),
1299
- selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1667
+ selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1668
+ ] });
1669
+ }
1670
+ var QuizInnerForwarded = (0, import_react12.forwardRef)(QuizInner);
1671
+ var Quiz = (0, import_react12.forwardRef)(function Quiz2(props, ref) {
1672
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1673
+ });
1674
+ function KnowledgeCheck(props) {
1675
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1676
+ Quiz,
1677
+ {
1678
+ checkId: props.checkId,
1679
+ question: props.question,
1680
+ choices: props.choices,
1681
+ answer: props.answer,
1682
+ passingScore: props.passingScore
1683
+ }
1684
+ );
1685
+ }
1686
+ function resetQuizWarningsForTests() {
1687
+ resetAssessmentWarningsForTests();
1688
+ }
1689
+
1690
+ // src/components.tsx
1691
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1692
+ function Course(props) {
1693
+ const courseId = (0, import_react13.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1694
+ const providerConfig = (0, import_react13.useMemo)(
1695
+ () => ({ ...props.config, courseId }),
1696
+ [props.config, courseId]
1697
+ );
1698
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": props.title, children: [
1699
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h1", { children: props.title }),
1700
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: props.children })
1701
+ ] }) });
1702
+ }
1703
+ function Lesson(props) {
1704
+ const lessonId = (0, import_react13.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1705
+ const autoComplete = props.autoCompleteOnUnmount !== false;
1706
+ const { setActiveLesson, config } = useLessonkit();
1707
+ const { completeLesson } = useCompletion();
1708
+ const lessonMountGenerationRef = (0, import_react13.useRef)(0);
1709
+ const liveCourseIdRef = (0, import_react13.useRef)(config.courseId);
1710
+ liveCourseIdRef.current = config.courseId;
1711
+ (0, import_react13.useEffect)(() => {
1712
+ const unregister = registerLessonMount(lessonId);
1713
+ const generation = ++lessonMountGenerationRef.current;
1714
+ const mountedCourseId = config.courseId;
1715
+ let effectSurvivedTick = false;
1716
+ queueMicrotask(() => {
1717
+ queueMicrotask(() => {
1718
+ effectSurvivedTick = true;
1719
+ });
1720
+ });
1721
+ setActiveLesson(lessonId);
1722
+ return () => {
1723
+ unregister();
1724
+ if (getLessonMountCount(lessonId) > 0) {
1725
+ return;
1726
+ }
1727
+ if (!autoComplete) return;
1728
+ queueMicrotask(() => {
1729
+ if (!effectSurvivedTick) return;
1730
+ if (lessonMountGenerationRef.current !== generation) return;
1731
+ if (liveCourseIdRef.current !== mountedCourseId) return;
1732
+ completeLesson(lessonId, { courseId: mountedCourseId });
1733
+ });
1734
+ };
1735
+ }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1736
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("article", { "aria-label": props.title, children: [
1737
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h2", { children: props.title }),
1738
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: props.children })
1739
+ ] }) });
1740
+ }
1741
+ function Scenario(props) {
1742
+ const blockId = (0, import_react13.useMemo)(
1743
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1744
+ [props.blockId]
1745
+ );
1746
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1747
+ }
1748
+ function Reflection(props) {
1749
+ const blockId = (0, import_react13.useMemo)(
1750
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1751
+ [props.blockId]
1752
+ );
1753
+ const promptId = (0, import_react13.useId)();
1754
+ const hintId = (0, import_react13.useId)();
1755
+ const [internalValue, setInternalValue] = (0, import_react13.useState)("");
1756
+ const isControlled = props.value !== void 0;
1757
+ const value = isControlled ? props.value : internalValue;
1758
+ const handleChange = (event) => {
1759
+ if (!isControlled) setInternalValue(event.target.value);
1760
+ props.onChange?.(event.target.value);
1761
+ };
1762
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1763
+ props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: promptId, children: props.prompt }) : null,
1764
+ props.hint ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { id: hintId, style: import_accessibility2.visuallyHiddenStyle, children: props.hint }) : null,
1765
+ props.children,
1766
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1767
+ "textarea",
1768
+ {
1769
+ value,
1770
+ onChange: handleChange,
1771
+ "aria-labelledby": props.prompt ? promptId : void 0,
1772
+ "aria-describedby": props.hint ? hintId : void 0,
1773
+ "aria-label": props.prompt ? void 0 : "Reflection response"
1774
+ }
1775
+ )
1300
1776
  ] });
1301
1777
  }
1302
1778
  function ProgressTracker(props) {
@@ -1305,7 +1781,7 @@ function ProgressTracker(props) {
1305
1781
  if (props.totalLessons != null) {
1306
1782
  const total = props.totalLessons;
1307
1783
  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)(
1784
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1309
1785
  "div",
1310
1786
  {
1311
1787
  role: "progressbar",
@@ -1313,7 +1789,7 @@ function ProgressTracker(props) {
1313
1789
  "aria-valuemax": total,
1314
1790
  "aria-valuenow": displayed,
1315
1791
  "aria-label": "Lessons completed",
1316
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
1792
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
1317
1793
  "Lessons completed: ",
1318
1794
  displayed,
1319
1795
  " of ",
@@ -1322,138 +1798,146 @@ function ProgressTracker(props) {
1322
1798
  }
1323
1799
  ) });
1324
1800
  }
1325
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
1801
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
1326
1802
  "Lessons completed: ",
1327
1803
  completed
1328
1804
  ] }) });
1329
1805
  }
1330
1806
 
1331
1807
  // 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");
1808
+ var import_react14 = __toESM(require("react"), 1);
1809
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1399
1810
  var INTERACTION = "trueFalse";
1400
1811
  function TrueFalseInner(props, ref) {
1401
1812
  const { enclosingLessonId } = props;
1402
- const checkId = (0, import_react9.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1813
+ const checkId = (0, import_react14.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1403
1814
  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();
1815
+ const { config } = useLessonkit();
1816
+ const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
1817
+ const [selected, setSelected] = (0, import_react14.useState)(null);
1818
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react14.useState)(null);
1819
+ const [showSolutions, setShowSolutions] = (0, import_react14.useState)(false);
1820
+ const [passed, setPassed] = (0, import_react14.useState)(false);
1821
+ const [completedScore, setCompletedScore] = (0, import_react14.useState)(null);
1822
+ const [completedMaxScore, setCompletedMaxScore] = (0, import_react14.useState)(null);
1823
+ const completedRef = (0, import_react14.useRef)(false);
1824
+ const telemetryReplayedRef = (0, import_react14.useRef)(false);
1825
+ const questionId = import_react14.default.useId();
1411
1826
  const reset = () => {
1412
1827
  completedRef.current = false;
1828
+ telemetryReplayedRef.current = false;
1413
1829
  setPassed(false);
1414
1830
  setSelected(null);
1415
1831
  setSelectionCorrect(null);
1416
1832
  setShowSolutions(false);
1833
+ setCompletedScore(null);
1834
+ setCompletedMaxScore(null);
1417
1835
  };
1418
- (0, import_react9.useEffect)(() => {
1836
+ (0, import_react14.useEffect)(() => {
1419
1837
  reset();
1420
1838
  }, [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,
1839
+ const resolveScores = () => {
1840
+ const maxScore = completedMaxScore ?? 1;
1841
+ if (passed) {
1842
+ return { score: completedScore ?? maxScore, maxScore };
1843
+ }
1844
+ if (selectionCorrect) {
1845
+ return { score: completedMaxScore ?? maxScore, maxScore };
1846
+ }
1847
+ return { score: 0, maxScore };
1848
+ };
1849
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1850
+ if (!nextPassed || telemetryReplayedRef.current) return;
1851
+ telemetryReplayedRef.current = true;
1852
+ if (nextSelected !== null) {
1853
+ assessment.answer({
1854
+ checkId,
1855
+ interactionType: INTERACTION,
1856
+ question: props.question,
1857
+ response: nextSelected,
1858
+ correct: nextCorrect ?? false
1859
+ });
1860
+ }
1861
+ assessment.complete({
1862
+ checkId,
1863
+ interactionType: INTERACTION,
1864
+ score: nextScore,
1865
+ maxScore: nextMaxScore,
1866
+ passingScore: props.passingScore ?? nextMaxScore
1867
+ });
1868
+ };
1869
+ const handle = (0, import_react14.useMemo)(
1870
+ () => buildAssessmentHandle({
1871
+ checkId,
1872
+ getScore: () => resolveScores().score,
1873
+ getMaxScore: () => resolveScores().maxScore,
1427
1874
  getAnswerGiven: () => selected !== null,
1428
1875
  resetTask: reset,
1429
1876
  showSolutions: () => setShowSolutions(true),
1430
- getXAPIData: () => ({
1431
- checkId,
1432
- interactionType: INTERACTION,
1433
- response: selected ?? void 0,
1434
- 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);
1877
+ getXAPIData: () => {
1878
+ const { score, maxScore } = resolveScores();
1879
+ return {
1880
+ checkId,
1881
+ interactionType: INTERACTION,
1882
+ response: selected ?? void 0,
1883
+ correct: selectionCorrect ?? void 0,
1884
+ score,
1885
+ maxScore
1886
+ };
1887
+ },
1888
+ getCurrentState: () => ({
1889
+ selected,
1890
+ selectionCorrect,
1891
+ passed,
1892
+ showSolutions,
1893
+ completedScore,
1894
+ completedMaxScore
1895
+ }),
1896
+ resume: (state) => {
1897
+ const nextSelected = readBooleanField(state, "selected");
1898
+ if (nextSelected === true || nextSelected === false || nextSelected === null) {
1899
+ setSelected(nextSelected);
1900
+ }
1901
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
1902
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1903
+ setSelectionCorrect(nextCorrect);
1904
+ }
1905
+ const nextCompletedScore = readNumberField(state, "completedScore");
1906
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1907
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1908
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1909
+ const nextPassed = readBooleanField(state, "passed");
1910
+ if (nextPassed === true || nextPassed === false) {
1911
+ setPassed(nextPassed);
1912
+ completedRef.current = nextPassed;
1913
+ if (nextPassed) {
1914
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1915
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
1916
+ replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
1917
+ }
1918
+ }
1919
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
1920
+ }
1921
+ }),
1922
+ [
1923
+ assessment,
1924
+ checkId,
1925
+ completedMaxScore,
1926
+ completedScore,
1927
+ passed,
1928
+ props.passingScore,
1929
+ props.question,
1930
+ selected,
1931
+ selectionCorrect,
1932
+ showSolutions
1933
+ ]
1934
+ );
1935
+ useAssessmentHandleRegistration(checkId, handle, ref);
1442
1936
  const submit = (value) => {
1443
1937
  if (passed && !props.enableRetry) return;
1444
1938
  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
1939
  const correct = value === props.answer;
1456
- const scored = scoreFromCustom(custom, correct, 1, props.passingScore);
1940
+ const scored = scoreResponse(value, correct, 1, props.passingScore);
1457
1941
  setSelectionCorrect(scored.passed);
1458
1942
  assessment.answer({
1459
1943
  checkId,
@@ -1465,6 +1949,8 @@ function TrueFalseInner(props, ref) {
1465
1949
  if (scored.passed && !completedRef.current) {
1466
1950
  completedRef.current = true;
1467
1951
  setPassed(true);
1952
+ setCompletedScore(scored.score);
1953
+ setCompletedMaxScore(scored.maxScore);
1468
1954
  assessment.complete({
1469
1955
  checkId,
1470
1956
  interactionType: INTERACTION,
@@ -1475,12 +1961,12 @@ function TrueFalseInner(props, ref) {
1475
1961
  }
1476
1962
  };
1477
1963
  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)(
1964
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1965
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { id: questionId, children: props.question }),
1966
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1967
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
1968
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
1969
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1484
1970
  "input",
1485
1971
  {
1486
1972
  type: "radio",
@@ -1492,8 +1978,8 @@ function TrueFalseInner(props, ref) {
1492
1978
  ),
1493
1979
  "True"
1494
1980
  ] }),
1495
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "block" }, children: [
1496
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1981
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "block" }, children: [
1982
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1497
1983
  "input",
1498
1984
  {
1499
1985
  type: "radio",
@@ -1506,49 +1992,49 @@ function TrueFalseInner(props, ref) {
1506
1992
  "False"
1507
1993
  ] })
1508
1994
  ] }),
1509
- reveal ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
1995
+ reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
1510
1996
  "Correct answer: ",
1511
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: props.answer ? "True" : "False" })
1997
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("strong", { children: props.answer ? "True" : "False" })
1512
1998
  ] }) : 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
1999
+ selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
2000
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2001
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1516
2002
  ] });
1517
2003
  }
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 }) });
2004
+ var TrueFalseInnerForwarded = (0, import_react14.forwardRef)(TrueFalseInner);
2005
+ var TrueFalse = (0, import_react14.forwardRef)(function TrueFalse2(props, ref) {
2006
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1521
2007
  });
1522
2008
 
1523
2009
  // src/blocks/MarkTheWords.tsx
1524
- var import_react10 = __toESM(require("react"), 1);
1525
- var import_jsx_runtime6 = require("react/jsx-runtime");
2010
+ var import_react15 = __toESM(require("react"), 1);
2011
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1526
2012
  var INTERACTION2 = "markTheWords";
1527
2013
  function tokenize(text) {
1528
2014
  return text.split(/(\s+)/).filter((t) => t.length > 0);
1529
2015
  }
1530
2016
  function MarkTheWordsInner(props, ref) {
1531
- const checkId = (0, import_react10.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2017
+ const checkId = (0, import_react15.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1532
2018
  const assessment = useAssessmentState(props.enclosingLessonId);
1533
- const tokens = (0, import_react10.useMemo)(() => tokenize(props.text), [props.text]);
1534
- const correctSet = (0, import_react10.useMemo)(
2019
+ const tokens = (0, import_react15.useMemo)(() => tokenize(props.text), [props.text]);
2020
+ const correctSet = (0, import_react15.useMemo)(
1535
2021
  () => new Set(props.correctWords.map((w) => w.toLowerCase())),
1536
2022
  [props.correctWords]
1537
2023
  );
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);
2024
+ const [marked, setMarked] = (0, import_react15.useState)(() => /* @__PURE__ */ new Set());
2025
+ const [passed, setPassed] = (0, import_react15.useState)(false);
2026
+ const [showSolutions, setShowSolutions] = (0, import_react15.useState)(false);
2027
+ const completedRef = (0, import_react15.useRef)(false);
1542
2028
  const reset = () => {
1543
2029
  completedRef.current = false;
1544
2030
  setPassed(false);
1545
2031
  setMarked(/* @__PURE__ */ new Set());
1546
2032
  setShowSolutions(false);
1547
2033
  };
1548
- (0, import_react10.useEffect)(() => {
2034
+ (0, import_react15.useEffect)(() => {
1549
2035
  reset();
1550
2036
  }, [checkId, props.text, props.correctWords.join("\0")]);
1551
- const selectableIndices = (0, import_react10.useMemo)(() => {
2037
+ const selectableIndices = (0, import_react15.useMemo)(() => {
1552
2038
  const indices = [];
1553
2039
  tokens.forEach((t, i) => {
1554
2040
  if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
@@ -1560,11 +2046,11 @@ function MarkTheWordsInner(props, ref) {
1560
2046
  const maxScore = selectableIndices.length;
1561
2047
  const score = allMarked ? maxScore : marked.size;
1562
2048
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1563
- const handle = (0, import_react10.useMemo)(() => {
1564
- const handleMax = maxScore || 1;
1565
- return {
2049
+ const handle = (0, import_react15.useMemo)(
2050
+ () => buildAssessmentHandle({
2051
+ checkId,
1566
2052
  getScore: () => score,
1567
- getMaxScore: () => handleMax,
2053
+ getMaxScore: () => maxScore || 1,
1568
2054
  getAnswerGiven: () => marked.size > 0,
1569
2055
  resetTask: reset,
1570
2056
  showSolutions: () => setShowSolutions(true),
@@ -1574,12 +2060,22 @@ function MarkTheWordsInner(props, ref) {
1574
2060
  response: [...marked].map((i) => tokens[i]),
1575
2061
  correct: passedThreshold,
1576
2062
  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);
2063
+ maxScore: maxScore || 1
2064
+ }),
2065
+ getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
2066
+ resume: (state) => {
2067
+ const raw = state.marked;
2068
+ if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
2069
+ readBooleanStateField(state, "passed", (value) => {
2070
+ setPassed(value);
2071
+ completedRef.current = value;
2072
+ });
2073
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
2074
+ }
2075
+ }),
2076
+ [checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
2077
+ );
2078
+ useAssessmentHandleRegistration(checkId, handle, ref);
1583
2079
  const toggle = (index) => {
1584
2080
  if (passed && !props.enableRetry) return;
1585
2081
  setMarked((prev) => {
@@ -1589,7 +2085,7 @@ function MarkTheWordsInner(props, ref) {
1589
2085
  return next;
1590
2086
  });
1591
2087
  };
1592
- (0, import_react10.useEffect)(() => {
2088
+ (0, import_react15.useEffect)(() => {
1593
2089
  if (!hasTargets) {
1594
2090
  if (isDevEnvironment4()) {
1595
2091
  console.warn(
@@ -1607,7 +2103,7 @@ function MarkTheWordsInner(props, ref) {
1607
2103
  interactionType: INTERACTION2,
1608
2104
  question: props.text,
1609
2105
  response: [...marked].map((i) => tokens[i]),
1610
- correct: true
2106
+ correct: passedThreshold
1611
2107
  });
1612
2108
  assessment.complete({
1613
2109
  checkId,
@@ -1629,20 +2125,20 @@ function MarkTheWordsInner(props, ref) {
1629
2125
  score,
1630
2126
  tokens
1631
2127
  ]);
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: [
2128
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
2129
+ !hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("p", { role: "alert", children: [
1634
2130
  "No words in this sentence match ",
1635
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { children: "correctWords" }),
2131
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: "correctWords" }),
1636
2132
  ". Check spelling and capitalization in the source text."
1637
2133
  ] }) : 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) => {
2134
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
2135
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1640
2136
  const isWord = !/^\s+$/.test(token);
1641
2137
  const isTarget = isWord && correctSet.has(token.toLowerCase());
1642
- if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react10.default.Fragment, { children: token }, i);
2138
+ if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react15.default.Fragment, { children: token }, i);
1643
2139
  const selected = marked.has(i);
1644
2140
  const solution = showSolutions || passed && props.enableSolutionsButton;
1645
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2141
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1646
2142
  "button",
1647
2143
  {
1648
2144
  type: "button",
@@ -1660,57 +2156,69 @@ function MarkTheWordsInner(props, ref) {
1660
2156
  i
1661
2157
  );
1662
2158
  }) }),
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
2159
+ allMarked ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
2160
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2161
+ props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1666
2162
  ] });
1667
2163
  }
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 }) });
2164
+ var MarkTheWordsInnerForwarded = (0, import_react15.forwardRef)(MarkTheWordsInner);
2165
+ var MarkTheWords = (0, import_react15.forwardRef)(function MarkTheWords2(props, ref) {
2166
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1671
2167
  });
1672
2168
 
1673
2169
  // 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) {
2170
+ var import_react16 = __toESM(require("react"), 1);
2171
+
2172
+ // src/assessment/internal/parseStarDelimitedTemplate.ts
2173
+ function parseStarDelimitedTemplate(template, idPrefix) {
1678
2174
  const parts = [];
1679
- const blanks = [];
2175
+ const values = [];
1680
2176
  const re = /\*([^*]+)\*/g;
1681
2177
  let last = 0;
1682
2178
  let match;
1683
2179
  let n = 0;
1684
2180
  while ((match = re.exec(template)) !== null) {
1685
2181
  parts.push(template.slice(last, match.index));
1686
- const id = `blank-${n++}`;
1687
- blanks.push({ id, answer: match[1].trim() });
1688
- parts.push(id);
2182
+ values.push(match[1].trim());
2183
+ parts.push(`${idPrefix}-${n++}`);
1689
2184
  last = match.index + match[0].length;
1690
2185
  }
1691
2186
  parts.push(template.slice(last));
1692
- return { parts, blanks };
2187
+ return { parts, values };
2188
+ }
2189
+
2190
+ // src/blocks/FillInTheBlanks.tsx
2191
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2192
+ var INTERACTION3 = "fillInBlanks";
2193
+ function parseTemplate(template) {
2194
+ const { parts, values } = parseStarDelimitedTemplate(template, "blank");
2195
+ return {
2196
+ parts,
2197
+ blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
2198
+ };
1693
2199
  }
1694
2200
  function FillInTheBlanksInner(props, ref) {
1695
- const checkId = (0, import_react11.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2201
+ const checkId = (0, import_react16.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1696
2202
  const assessment = useAssessmentState(props.enclosingLessonId);
1697
- const parsed = (0, import_react11.useMemo)(() => parseTemplate(props.template), [props.template]);
2203
+ const parsed = (0, import_react16.useMemo)(() => parseTemplate(props.template), [props.template]);
1698
2204
  const blanks = props.blanks ?? parsed.blanks;
1699
- const [values, setValues] = (0, import_react11.useState)(
2205
+ const [values, setValues] = (0, import_react16.useState)(
1700
2206
  () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
1701
2207
  );
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);
2208
+ const [passed, setPassed] = (0, import_react16.useState)(false);
2209
+ const [showSolutions, setShowSolutions] = (0, import_react16.useState)(false);
2210
+ const [submitted, setSubmitted] = (0, import_react16.useState)(false);
2211
+ const completedRef = (0, import_react16.useRef)(false);
2212
+ const answeredRef = (0, import_react16.useRef)(false);
1706
2213
  const reset = () => {
1707
2214
  completedRef.current = false;
1708
2215
  answeredRef.current = false;
1709
2216
  setPassed(false);
1710
2217
  setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
1711
2218
  setShowSolutions(false);
2219
+ setSubmitted(false);
1712
2220
  };
1713
- (0, import_react11.useEffect)(() => {
2221
+ (0, import_react16.useEffect)(() => {
1714
2222
  reset();
1715
2223
  }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
1716
2224
  const hasBlanks = blanks.length > 0;
@@ -1721,11 +2229,11 @@ function FillInTheBlanksInner(props, ref) {
1721
2229
  });
1722
2230
  const maxScore = blanks.length;
1723
2231
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1724
- const handle = (0, import_react11.useMemo)(() => {
1725
- const handleMax = maxScore || 1;
1726
- return {
2232
+ const handle = (0, import_react16.useMemo)(
2233
+ () => buildAssessmentHandle({
2234
+ checkId,
1727
2235
  getScore: () => score,
1728
- getMaxScore: () => handleMax,
2236
+ getMaxScore: () => maxScore || 1,
1729
2237
  getAnswerGiven: () => allFilled,
1730
2238
  resetTask: reset,
1731
2239
  showSolutions: () => setShowSolutions(true),
@@ -1735,12 +2243,27 @@ function FillInTheBlanksInner(props, ref) {
1735
2243
  response: values,
1736
2244
  correct: passedThreshold,
1737
2245
  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);
2246
+ maxScore: maxScore || 1
2247
+ }),
2248
+ getCurrentState: () => ({ values, passed, showSolutions, submitted }),
2249
+ resume: (state) => {
2250
+ const raw = state.values;
2251
+ if (raw && typeof raw === "object") setValues({ ...raw });
2252
+ readBooleanStateField(state, "passed", (value) => {
2253
+ setPassed(value);
2254
+ completedRef.current = value;
2255
+ answeredRef.current = value;
2256
+ });
2257
+ readBooleanStateField(state, "showSolutions", setShowSolutions);
2258
+ readBooleanStateField(state, "submitted", (value) => {
2259
+ setSubmitted(value);
2260
+ if (value) answeredRef.current = true;
2261
+ });
2262
+ }
2263
+ }),
2264
+ [allFilled, checkId, maxScore, passed, passedThreshold, score, showSolutions, submitted, values]
2265
+ );
2266
+ useAssessmentHandleRegistration(checkId, handle, ref);
1744
2267
  const check = () => {
1745
2268
  if (!hasBlanks) {
1746
2269
  if (isDevEnvironment4()) {
@@ -1749,16 +2272,16 @@ function FillInTheBlanksInner(props, ref) {
1749
2272
  return;
1750
2273
  }
1751
2274
  if (!allFilled) return;
1752
- if (!answeredRef.current) {
1753
- answeredRef.current = true;
1754
- assessment.answer({
1755
- checkId,
1756
- interactionType: INTERACTION3,
1757
- question: props.template,
1758
- response: values,
1759
- correct: passedThreshold
1760
- });
1761
- }
2275
+ if (answeredRef.current || submitted) return;
2276
+ answeredRef.current = true;
2277
+ setSubmitted(true);
2278
+ assessment.answer({
2279
+ checkId,
2280
+ interactionType: INTERACTION3,
2281
+ question: props.template,
2282
+ response: values,
2283
+ correct: passedThreshold
2284
+ });
1762
2285
  if (passedThreshold && !completedRef.current) {
1763
2286
  completedRef.current = true;
1764
2287
  setPassed(true);
@@ -1771,20 +2294,23 @@ function FillInTheBlanksInner(props, ref) {
1771
2294
  });
1772
2295
  }
1773
2296
  };
1774
- (0, import_react11.useEffect)(() => {
1775
- if (!allFilled) answeredRef.current = false;
2297
+ (0, import_react16.useEffect)(() => {
2298
+ if (!allFilled) {
2299
+ answeredRef.current = false;
2300
+ setSubmitted(false);
2301
+ }
1776
2302
  }, [allFilled]);
1777
- (0, import_react11.useEffect)(() => {
2303
+ (0, import_react16.useEffect)(() => {
1778
2304
  if (props.autoCheck && allFilled) check();
1779
2305
  }, [allFilled, props.autoCheck, values, passedThreshold]);
1780
2306
  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) => {
2307
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2308
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: parsed.parts.map((part, i) => {
1783
2309
  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)(
2310
+ if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react16.default.Fragment, { children: part }, i);
2311
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
2312
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
2313
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1788
2314
  "input",
1789
2315
  {
1790
2316
  type: "text",
@@ -1800,61 +2326,51 @@ function FillInTheBlanksInner(props, ref) {
1800
2326
  )
1801
2327
  ] }, blank.id);
1802
2328
  }) }),
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
2329
+ !props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2330
+ !hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2331
+ submitted ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2332
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2333
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1808
2334
  ] });
1809
2335
  }
1810
- var FillInTheBlanksInnerForwarded = (0, import_react11.forwardRef)(FillInTheBlanksInner);
1811
- var FillInTheBlanks = (0, import_react11.forwardRef)(
2336
+ var FillInTheBlanksInnerForwarded = (0, import_react16.forwardRef)(FillInTheBlanksInner);
2337
+ var FillInTheBlanks = (0, import_react16.forwardRef)(
1812
2338
  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 }) });
2339
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1814
2340
  }
1815
2341
  );
1816
2342
 
1817
2343
  // src/blocks/DragTheWords.tsx
1818
- var import_react12 = __toESM(require("react"), 1);
1819
- var import_jsx_runtime8 = require("react/jsx-runtime");
2344
+ var import_react17 = __toESM(require("react"), 1);
2345
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1820
2346
  var INTERACTION4 = "dragTheWords";
1821
2347
  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 };
2348
+ const { parts, values } = parseStarDelimitedTemplate(template, "zone");
2349
+ return { parts, answers: values };
1836
2350
  }
1837
2351
  function DragTheWordsInner(props, ref) {
1838
- const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2352
+ const checkId = (0, import_react17.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1839
2353
  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)(
2354
+ const { parts, answers } = (0, import_react17.useMemo)(() => parseZones(props.template), [props.template]);
2355
+ const [zones, setZones] = (0, import_react17.useState)(
1842
2356
  () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
1843
2357
  );
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);
2358
+ const [pool, setPool] = (0, import_react17.useState)(() => [...props.words]);
2359
+ const [keyboardWord, setKeyboardWord] = (0, import_react17.useState)(null);
2360
+ const [passed, setPassed] = (0, import_react17.useState)(false);
2361
+ const [submitted, setSubmitted] = (0, import_react17.useState)(false);
2362
+ const completedRef = (0, import_react17.useRef)(false);
2363
+ const answeredRef = (0, import_react17.useRef)(false);
1849
2364
  const reset = () => {
1850
2365
  completedRef.current = false;
1851
2366
  answeredRef.current = false;
1852
2367
  setPassed(false);
2368
+ setSubmitted(false);
1853
2369
  setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
1854
2370
  setPool([...props.words]);
1855
2371
  setKeyboardWord(null);
1856
2372
  };
1857
- (0, import_react12.useEffect)(() => {
2373
+ (0, import_react17.useEffect)(() => {
1858
2374
  reset();
1859
2375
  }, [checkId, props.template, props.words.join("\0")]);
1860
2376
  const hasZones = answers.length > 0;
@@ -1865,11 +2381,11 @@ function DragTheWordsInner(props, ref) {
1865
2381
  });
1866
2382
  const maxScore = answers.length;
1867
2383
  const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1868
- const handle = (0, import_react12.useMemo)(() => {
1869
- const handleMax = maxScore || 1;
1870
- return {
2384
+ const handle = (0, import_react17.useMemo)(
2385
+ () => buildAssessmentHandle({
2386
+ checkId,
1871
2387
  getScore: () => score,
1872
- getMaxScore: () => handleMax,
2388
+ getMaxScore: () => maxScore || 1,
1873
2389
  getAnswerGiven: () => allFilled,
1874
2390
  resetTask: reset,
1875
2391
  showSolutions: () => {
@@ -1880,12 +2396,29 @@ function DragTheWordsInner(props, ref) {
1880
2396
  response: zones,
1881
2397
  correct: passedThreshold,
1882
2398
  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);
2399
+ maxScore: maxScore || 1
2400
+ }),
2401
+ getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
2402
+ resume: (state) => {
2403
+ const rawZones = state.zones;
2404
+ if (rawZones && typeof rawZones === "object") setZones({ ...rawZones });
2405
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2406
+ readBooleanStateField(state, "passed", (value) => {
2407
+ setPassed(value);
2408
+ completedRef.current = value;
2409
+ answeredRef.current = value;
2410
+ });
2411
+ readBooleanStateField(state, "submitted", (value) => {
2412
+ setSubmitted(value);
2413
+ if (value) answeredRef.current = true;
2414
+ });
2415
+ const kw = state.keyboardWord;
2416
+ if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2417
+ }
2418
+ }),
2419
+ [allFilled, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, score, submitted, zones]
2420
+ );
2421
+ useAssessmentHandleRegistration(checkId, handle, ref);
1889
2422
  const placeInZone = (zoneId, word) => {
1890
2423
  if (passed && !props.enableRetry) return;
1891
2424
  const prev = zones[zoneId];
@@ -1913,16 +2446,16 @@ function DragTheWordsInner(props, ref) {
1913
2446
  return;
1914
2447
  }
1915
2448
  if (!allFilled) return;
1916
- if (!answeredRef.current) {
1917
- answeredRef.current = true;
1918
- assessment.answer({
1919
- checkId,
1920
- interactionType: INTERACTION4,
1921
- question: props.template,
1922
- response: zones,
1923
- correct: passedThreshold
1924
- });
1925
- }
2449
+ if (answeredRef.current || submitted) return;
2450
+ answeredRef.current = true;
2451
+ setSubmitted(true);
2452
+ assessment.answer({
2453
+ checkId,
2454
+ interactionType: INTERACTION4,
2455
+ question: props.template,
2456
+ response: zones,
2457
+ correct: passedThreshold
2458
+ });
1926
2459
  if (passedThreshold && !completedRef.current) {
1927
2460
  completedRef.current = true;
1928
2461
  setPassed(true);
@@ -1935,15 +2468,18 @@ function DragTheWordsInner(props, ref) {
1935
2468
  });
1936
2469
  }
1937
2470
  };
1938
- (0, import_react12.useEffect)(() => {
1939
- if (!allFilled) answeredRef.current = false;
2471
+ (0, import_react17.useEffect)(() => {
2472
+ if (!allFilled) {
2473
+ answeredRef.current = false;
2474
+ setSubmitted(false);
2475
+ }
1940
2476
  }, [allFilled]);
1941
- (0, import_react12.useEffect)(() => {
2477
+ (0, import_react17.useEffect)(() => {
1942
2478
  if (props.autoCheck && allFilled) check();
1943
2479
  }, [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)(
2480
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2481
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2482
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1947
2483
  "button",
1948
2484
  {
1949
2485
  type: "button",
@@ -1957,9 +2493,9 @@ function DragTheWordsInner(props, ref) {
1957
2493
  },
1958
2494
  word
1959
2495
  )) }),
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)(
2496
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { children: parts.map((part, i) => {
2497
+ if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react17.default.Fragment, { children: part }, i);
2498
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1963
2499
  "span",
1964
2500
  {
1965
2501
  role: "button",
@@ -1972,220 +2508,1598 @@ function DragTheWordsInner(props, ref) {
1972
2508
  if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
1973
2509
  },
1974
2510
  style: {
1975
- display: "inline-block",
1976
- minWidth: "6em",
1977
- border: "1px dashed currentColor",
1978
- padding: "0.2em 0.5em",
1979
- margin: "0 0.2em"
2511
+ display: "inline-block",
2512
+ minWidth: "6em",
2513
+ border: "1px dashed currentColor",
2514
+ padding: "0.2em 0.5em",
2515
+ margin: "0 0.2em"
2516
+ },
2517
+ children: zones[part] || "___"
2518
+ },
2519
+ part
2520
+ );
2521
+ }) }),
2522
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2523
+ !hasZones ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2524
+ submitted ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2525
+ ] });
2526
+ }
2527
+ var DragTheWordsInnerForwarded = (0, import_react17.forwardRef)(DragTheWordsInner);
2528
+ var DragTheWords = (0, import_react17.forwardRef)(function DragTheWords2(props, ref) {
2529
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2530
+ });
2531
+
2532
+ // src/blocks/DragAndDrop.tsx
2533
+ var import_react18 = require("react");
2534
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2535
+ var INTERACTION5 = "dragAndDrop";
2536
+ function DragAndDropInner(props, ref) {
2537
+ const checkId = (0, import_react18.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2538
+ const assessment = useAssessmentState(props.enclosingLessonId);
2539
+ const [assignments, setAssignments] = (0, import_react18.useState)(
2540
+ () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2541
+ );
2542
+ const [pool, setPool] = (0, import_react18.useState)(() => props.items.map((i) => i.id));
2543
+ const [keyboardItem, setKeyboardItem] = (0, import_react18.useState)(null);
2544
+ const [passed, setPassed] = (0, import_react18.useState)(false);
2545
+ const [checked, setChecked] = (0, import_react18.useState)(false);
2546
+ const completedRef = (0, import_react18.useRef)(false);
2547
+ const reset = () => {
2548
+ completedRef.current = false;
2549
+ setPassed(false);
2550
+ setChecked(false);
2551
+ setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2552
+ setPool(props.items.map((i) => i.id));
2553
+ setKeyboardItem(null);
2554
+ };
2555
+ (0, import_react18.useEffect)(() => {
2556
+ reset();
2557
+ }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2558
+ const hasTargets = props.targets.length > 0;
2559
+ const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2560
+ let score = 0;
2561
+ props.targets.forEach((t) => {
2562
+ if (assignments[t.id] === t.accepts) score += 1;
2563
+ });
2564
+ const maxScore = props.targets.length || 1;
2565
+ const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
2566
+ const handle = (0, import_react18.useMemo)(() => {
2567
+ return buildAssessmentHandle({
2568
+ checkId,
2569
+ getScore: () => score,
2570
+ getMaxScore: () => maxScore,
2571
+ getAnswerGiven: () => hasTargets && allFilled,
2572
+ resetTask: reset,
2573
+ showSolutions: () => {
2574
+ },
2575
+ getXAPIData: () => ({
2576
+ checkId,
2577
+ interactionType: INTERACTION5,
2578
+ response: assignments,
2579
+ correct: passedThreshold,
2580
+ score,
2581
+ maxScore
2582
+ }),
2583
+ getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
2584
+ resume: (state) => {
2585
+ const rawAssignments = state.assignments;
2586
+ if (rawAssignments && typeof rawAssignments === "object") {
2587
+ setAssignments({ ...rawAssignments });
2588
+ }
2589
+ if (Array.isArray(state.pool)) setPool([...state.pool]);
2590
+ readBooleanStateField(state, "passed", (value) => {
2591
+ setPassed(value);
2592
+ completedRef.current = value;
2593
+ });
2594
+ readBooleanStateField(state, "checked", setChecked);
2595
+ const item = state.keyboardItem;
2596
+ if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
2597
+ }
2598
+ });
2599
+ }, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
2600
+ useAssessmentHandleRegistration(checkId, handle, ref);
2601
+ const place = (targetId, itemId) => {
2602
+ if (passed && !props.enableRetry) return;
2603
+ setChecked(false);
2604
+ const prev = assignments[targetId];
2605
+ setAssignments((a) => ({ ...a, [targetId]: itemId }));
2606
+ setPool((p) => {
2607
+ const next = p.filter((id) => id !== itemId);
2608
+ if (prev) next.push(prev);
2609
+ return next;
2610
+ });
2611
+ setKeyboardItem(null);
2612
+ };
2613
+ const check = () => {
2614
+ if (!allFilled) return;
2615
+ setChecked(true);
2616
+ assessment.answer({
2617
+ checkId,
2618
+ interactionType: INTERACTION5,
2619
+ response: assignments,
2620
+ correct: passedThreshold
2621
+ });
2622
+ if (passedThreshold && !completedRef.current) {
2623
+ completedRef.current = true;
2624
+ setPassed(true);
2625
+ assessment.complete({
2626
+ checkId,
2627
+ interactionType: INTERACTION5,
2628
+ score,
2629
+ maxScore,
2630
+ passingScore: props.passingScore ?? maxScore
2631
+ });
2632
+ }
2633
+ };
2634
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2635
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2636
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
2637
+ const item = props.items.find((i) => i.id === id);
2638
+ if (!item) return [];
2639
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2640
+ "button",
2641
+ {
2642
+ type: "button",
2643
+ draggable: true,
2644
+ "data-testid": `drag-item-${id}`,
2645
+ "aria-pressed": keyboardItem === id,
2646
+ onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2647
+ onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2648
+ style: { margin: "0.25rem" },
2649
+ children: item.label
2650
+ },
2651
+ id
2652
+ );
2653
+ }) }),
2654
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { children: props.targets.map((target) => {
2655
+ const assigned = assignments[target.id];
2656
+ const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2657
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { children: [
2658
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: target.label }),
2659
+ " ",
2660
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2661
+ "span",
2662
+ {
2663
+ role: "button",
2664
+ tabIndex: 0,
2665
+ "data-testid": `drop-${target.id}`,
2666
+ onDragOver: (e) => e.preventDefault(),
2667
+ onDrop: (e) => {
2668
+ e.preventDefault();
2669
+ const id = e.dataTransfer.getData("text/plain");
2670
+ if (id) place(target.id, id);
2671
+ },
2672
+ onClick: () => keyboardItem && place(target.id, keyboardItem),
2673
+ onKeyDown: (e) => {
2674
+ if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2675
+ },
2676
+ style: {
2677
+ display: "inline-block",
2678
+ minWidth: "8em",
2679
+ border: "1px dashed currentColor",
2680
+ padding: "0.25em"
2681
+ },
2682
+ children: label
2683
+ }
2684
+ )
2685
+ ] }, target.id);
2686
+ }) }),
2687
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
2688
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
2689
+ ] });
2690
+ }
2691
+ var DragAndDropInnerForwarded = (0, import_react18.forwardRef)(DragAndDropInner);
2692
+ var DragAndDrop = (0, import_react18.forwardRef)(function DragAndDrop2(props, ref) {
2693
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2694
+ });
2695
+
2696
+ // src/blocks/AssessmentSequence.tsx
2697
+ var import_react24 = __toESM(require("react"), 1);
2698
+ var import_core17 = require("@lessonkit/core");
2699
+
2700
+ // src/compound/useCompoundShell.ts
2701
+ var import_react22 = require("react");
2702
+ var import_core15 = require("@lessonkit/core");
2703
+
2704
+ // src/compound/useCompoundNavigation.ts
2705
+ var import_react19 = require("react");
2706
+ function useCompoundNavigation(pageCount, index, setIndex) {
2707
+ const goNext = (0, import_react19.useCallback)(() => {
2708
+ if (pageCount < 1) return;
2709
+ setIndex((i) => Math.min(i + 1, pageCount - 1));
2710
+ }, [pageCount, setIndex]);
2711
+ const goPrev = (0, import_react19.useCallback)(() => {
2712
+ setIndex((i) => Math.max(i - 1, 0));
2713
+ }, [setIndex]);
2714
+ const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
2715
+ return {
2716
+ index: clampedIndex,
2717
+ setIndex,
2718
+ goNext,
2719
+ goPrev,
2720
+ progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
2721
+ };
2722
+ }
2723
+
2724
+ // src/compound/useCompoundPersistence.ts
2725
+ var import_react21 = require("react");
2726
+ var import_core14 = require("@lessonkit/core");
2727
+
2728
+ // src/compound/resumeChildHandles.ts
2729
+ function filterRegisteredChildStates(handles, childStates) {
2730
+ const filtered = {};
2731
+ for (const [key, value] of Object.entries(childStates)) {
2732
+ if (handles.has(key)) {
2733
+ filtered[key] = value;
2734
+ }
2735
+ }
2736
+ return filtered;
2737
+ }
2738
+ function resumeChildHandles(handles, childStates, opts) {
2739
+ const pendingKeys = Object.keys(childStates);
2740
+ const alreadyResumed = opts?.alreadyResumed;
2741
+ if (opts?.waitForHandles && pendingKeys.length > 0) {
2742
+ if (handles.size === 0) return false;
2743
+ const registeredPending = pendingKeys.filter((k) => handles.has(k));
2744
+ if (registeredPending.length === 0) {
2745
+ return false;
2746
+ }
2747
+ if (registeredPending.length < pendingKeys.length) {
2748
+ for (const key of registeredPending) {
2749
+ if (alreadyResumed?.has(key)) continue;
2750
+ const handle = handles.get(key);
2751
+ const child = childStates[key];
2752
+ if (handle?.resume && child) {
2753
+ handle.resume(child);
2754
+ alreadyResumed?.add(key);
2755
+ }
2756
+ }
2757
+ return false;
2758
+ }
2759
+ }
2760
+ for (const [checkId, handle] of handles) {
2761
+ if (alreadyResumed?.has(checkId)) continue;
2762
+ const child = childStates[checkId];
2763
+ if (child && handle.resume) {
2764
+ handle.resume(child);
2765
+ alreadyResumed?.add(checkId);
2766
+ }
2767
+ }
2768
+ return true;
2769
+ }
2770
+
2771
+ // src/compound/useCompoundResume.ts
2772
+ var import_react20 = require("react");
2773
+ var import_core12 = require("@lessonkit/core");
2774
+ var import_core13 = require("@lessonkit/core");
2775
+ var warnedCompoundPersistFailure = false;
2776
+ function warnCompoundPersistFailure() {
2777
+ if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
2778
+ warnedCompoundPersistFailure = true;
2779
+ console.warn(
2780
+ "[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
2781
+ );
2782
+ }
2783
+ function useCompoundResume(opts) {
2784
+ const lessonkitCtx = (0, import_react20.useContext)(LessonkitContext);
2785
+ const storageRef = (0, import_react20.useRef)(opts.storage ?? lessonkitCtx?.storage ?? (0, import_core13.createSessionStoragePort)());
2786
+ const resumedRef = (0, import_react20.useRef)(false);
2787
+ const resumeKeyRef = (0, import_react20.useRef)("");
2788
+ const prevEnabledRef = (0, import_react20.useRef)(opts.enabled);
2789
+ (0, import_react20.useEffect)(() => {
2790
+ storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core13.createSessionStoragePort)();
2791
+ }, [opts.storage, lessonkitCtx?.storage]);
2792
+ (0, import_react20.useEffect)(() => {
2793
+ if (!prevEnabledRef.current && opts.enabled) {
2794
+ resumedRef.current = false;
2795
+ }
2796
+ prevEnabledRef.current = opts.enabled;
2797
+ const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
2798
+ if (resumeKeyRef.current !== key) {
2799
+ resumeKeyRef.current = key;
2800
+ resumedRef.current = false;
2801
+ }
2802
+ if (!opts.enabled || !opts.courseId || resumedRef.current) return;
2803
+ const saved = (0, import_core12.loadCompoundState)(storageRef.current, opts.courseId, opts.compoundId);
2804
+ if (saved) {
2805
+ resumedRef.current = true;
2806
+ opts.onResume?.(saved);
2807
+ }
2808
+ }, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
2809
+ return (0, import_react20.useCallback)(
2810
+ (state) => {
2811
+ if (!opts.enabled || !opts.courseId) return;
2812
+ const persisted = (0, import_core12.saveCompoundState)(storageRef.current, opts.courseId, opts.compoundId, state);
2813
+ if (!persisted) warnCompoundPersistFailure();
2814
+ },
2815
+ [opts.enabled, opts.courseId, opts.compoundId]
2816
+ );
2817
+ }
2818
+
2819
+ // src/compound/useCompoundPersistence.ts
2820
+ function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = (0, import_core14.createSessionStoragePort)()) {
2821
+ if (!enabled || !courseId || pageCount < 1) return 0;
2822
+ const saved = (0, import_core14.loadCompoundState)(storage, courseId, compoundId);
2823
+ if (!saved) return 0;
2824
+ return (0, import_core14.clampCompoundPageIndex)(saved.activePageIndex, pageCount);
2825
+ }
2826
+ function stripOrphanChildStates(handles, childStates) {
2827
+ return filterRegisteredChildStates(handles, childStates);
2828
+ }
2829
+ function useCompoundPersistence(opts) {
2830
+ const lessonkitCtx = (0, import_react21.useContext)(LessonkitContext);
2831
+ const storage = opts.storage ?? lessonkitCtx?.storage ?? (0, import_core14.createSessionStoragePort)();
2832
+ const ctx = useCompoundRegistry();
2833
+ const handlesVersion = useCompoundHandlesVersion();
2834
+ const bridgeRef = useCompoundHydrationBridgeRef();
2835
+ const pendingChildResumeRef = (0, import_react21.useRef)(null);
2836
+ const resumedChildKeysRef = (0, import_react21.useRef)(/* @__PURE__ */ new Set());
2837
+ const loadedChildStatesRef = (0, import_react21.useRef)({});
2838
+ const skipSaveUntilHydratedRef = (0, import_react21.useRef)(false);
2839
+ const hydrationKeyRef = (0, import_react21.useRef)("");
2840
+ const hydrationInitRef = (0, import_react21.useRef)(false);
2841
+ const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
2842
+ if (hydrationKeyRef.current !== hydrationKey) {
2843
+ hydrationKeyRef.current = hydrationKey;
2844
+ hydrationInitRef.current = false;
2845
+ loadedChildStatesRef.current = {};
2846
+ skipSaveUntilHydratedRef.current = false;
2847
+ pendingChildResumeRef.current = null;
2848
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2849
+ }
2850
+ if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
2851
+ hydrationInitRef.current = true;
2852
+ const saved = (0, import_core14.loadCompoundState)(storage, opts.courseId, opts.compoundId);
2853
+ if (saved && Object.keys(saved.childStates).length > 0) {
2854
+ loadedChildStatesRef.current = { ...saved.childStates };
2855
+ skipSaveUntilHydratedRef.current = true;
2856
+ pendingChildResumeRef.current = saved;
2857
+ }
2858
+ }
2859
+ const buildState = (0, import_react21.useCallback)(() => {
2860
+ const childStates = {
2861
+ ...loadedChildStatesRef.current
2862
+ };
2863
+ if (ctx) {
2864
+ for (const [checkId, entry] of ctx.getRegisteredHandles()) {
2865
+ const handle = entry.handle;
2866
+ if (handle.getCurrentState) {
2867
+ childStates[checkId] = handle.getCurrentState();
2868
+ delete loadedChildStatesRef.current[checkId];
2869
+ }
2870
+ }
2871
+ }
2872
+ return (0, import_core14.createCompoundResumeState)({
2873
+ activePageIndex: (0, import_core14.clampCompoundPageIndex)(opts.index, opts.pageCount),
2874
+ childStates
2875
+ });
2876
+ }, [ctx, opts.index, opts.pageCount]);
2877
+ const buildStateRef = (0, import_react21.useRef)(buildState);
2878
+ buildStateRef.current = buildState;
2879
+ const finalizeHydration = (0, import_react21.useCallback)(
2880
+ (childStates) => {
2881
+ loadedChildStatesRef.current = {
2882
+ ...loadedChildStatesRef.current,
2883
+ ...childStates
2884
+ };
2885
+ skipSaveUntilHydratedRef.current = false;
2886
+ pendingChildResumeRef.current = null;
2887
+ },
2888
+ []
2889
+ );
2890
+ const applyPendingChildResume = (0, import_react21.useCallback)(() => {
2891
+ const pending = pendingChildResumeRef.current;
2892
+ if (!pending || !ctx) return;
2893
+ const handles = ctx.getHandles();
2894
+ const applied = resumeChildHandles(handles, pending.childStates, {
2895
+ waitForHandles: true,
2896
+ alreadyResumed: resumedChildKeysRef.current
2897
+ });
2898
+ if (!applied) {
2899
+ const handlesAtWait = handles.size;
2900
+ queueMicrotask(() => {
2901
+ if (pendingChildResumeRef.current !== pending) return;
2902
+ const handlesNow = ctx.getHandles();
2903
+ if (handlesNow.size !== handlesAtWait) return;
2904
+ const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
2905
+ resumeChildHandles(handlesNow, registeredOnly2, {
2906
+ alreadyResumed: resumedChildKeysRef.current
2907
+ });
2908
+ finalizeHydration(registeredOnly2);
2909
+ });
2910
+ return;
2911
+ }
2912
+ const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
2913
+ finalizeHydration(registeredOnly);
2914
+ }, [ctx, finalizeHydration]);
2915
+ const saveResume = useCompoundResume({
2916
+ courseId: opts.courseId,
2917
+ compoundId: opts.compoundId,
2918
+ enabled: opts.enabled,
2919
+ storage,
2920
+ onResume: (state) => {
2921
+ const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
2922
+ loadedChildStatesRef.current = { ...state.childStates };
2923
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2924
+ opts.setIndex(clamped);
2925
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2926
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
2927
+ queueMicrotask(() => applyPendingChildResume());
2928
+ }
2929
+ });
2930
+ const persistNow = (0, import_react21.useCallback)(() => {
2931
+ if (!opts.enabled || !opts.courseId) return;
2932
+ saveResume(buildStateRef.current());
2933
+ }, [opts.enabled, opts.courseId, saveResume]);
2934
+ const notifyImperativeResume = (0, import_react21.useCallback)(
2935
+ (state) => {
2936
+ const clamped = (0, import_core14.clampCompoundPageIndex)(state.activePageIndex, opts.pageCount);
2937
+ loadedChildStatesRef.current = { ...state.childStates };
2938
+ skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
2939
+ opts.setIndex(clamped);
2940
+ resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2941
+ pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
2942
+ queueMicrotask(() => applyPendingChildResume());
2943
+ },
2944
+ [opts.pageCount, opts.setIndex, applyPendingChildResume]
2945
+ );
2946
+ (0, import_react21.useEffect)(() => {
2947
+ if (!bridgeRef) return;
2948
+ bridgeRef.current = { notifyImperativeResume };
2949
+ return () => {
2950
+ if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
2951
+ bridgeRef.current = null;
2952
+ }
2953
+ };
2954
+ }, [bridgeRef, notifyImperativeResume]);
2955
+ (0, import_react21.useEffect)(() => {
2956
+ persistNow();
2957
+ }, [persistNow, opts.index, opts.pageCount, handlesVersion]);
2958
+ (0, import_react21.useEffect)(() => {
2959
+ applyPendingChildResume();
2960
+ }, [opts.index, handlesVersion, applyPendingChildResume]);
2961
+ (0, import_react21.useEffect)(() => {
2962
+ if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
2963
+ const flushOnExit = () => {
2964
+ if (document.visibilityState === "hidden") persistNow();
2965
+ };
2966
+ document.addEventListener("visibilitychange", flushOnExit);
2967
+ window.addEventListener("pagehide", flushOnExit);
2968
+ return () => {
2969
+ document.removeEventListener("visibilitychange", flushOnExit);
2970
+ window.removeEventListener("pagehide", flushOnExit);
2971
+ };
2972
+ }, [opts.enabled, opts.courseId, persistNow]);
2973
+ }
2974
+
2975
+ // src/compound/useCompoundShell.ts
2976
+ function useCompoundShell(opts) {
2977
+ const ctx = useCompoundRegistry();
2978
+ useCompoundPersistence({
2979
+ courseId: opts.courseId,
2980
+ compoundId: opts.compoundId,
2981
+ pageCount: opts.pageCount,
2982
+ index: opts.index,
2983
+ setIndex: opts.setIndex,
2984
+ enabled: opts.persistEnabled,
2985
+ storage: opts.storage
2986
+ });
2987
+ const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
2988
+ const visibleIndex = (0, import_core15.clampCompoundPageIndex)(opts.index, opts.pageCount);
2989
+ useCompoundHandleRef(opts.ref, {
2990
+ activePageIndex: visibleIndex,
2991
+ setActivePageIndex: opts.setIndex,
2992
+ getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
2993
+ getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
2994
+ pageCount: opts.pageCount,
2995
+ enableSolutionsButton: opts.enableSolutionsButton
2996
+ });
2997
+ return { visibleIndex, goNext, goPrev, progress, ctx };
2998
+ }
2999
+ function useCompoundInitialIndex(opts) {
3000
+ return (0, import_react22.useMemo)(
3001
+ () => readCompoundInitialIndex(
3002
+ opts.courseId,
3003
+ opts.compoundId,
3004
+ opts.pageCount,
3005
+ opts.persistEnabled,
3006
+ opts.storage
3007
+ ),
3008
+ [opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
3009
+ );
3010
+ }
3011
+
3012
+ // src/compound/validateChildren.ts
3013
+ var import_react23 = __toESM(require("react"), 1);
3014
+ var import_core16 = require("@lessonkit/core");
3015
+
3016
+ // src/compound/blockType.ts
3017
+ var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
3018
+ function setLessonkitBlockType(component, blockType) {
3019
+ component[LESSONKIT_BLOCK_TYPE] = blockType;
3020
+ if (!component.displayName) {
3021
+ component.displayName = blockType;
3022
+ }
3023
+ return component;
3024
+ }
3025
+ function getLessonkitBlockType(component) {
3026
+ if (!component || typeof component !== "object" && typeof component !== "function") {
3027
+ return void 0;
3028
+ }
3029
+ const typed = component;
3030
+ return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
3031
+ }
3032
+
3033
+ // src/compound/validateChildren.ts
3034
+ var warnedPairs = /* @__PURE__ */ new Set();
3035
+ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
3036
+ "Page",
3037
+ "InteractiveBook",
3038
+ "Slide",
3039
+ "SlideDeck",
3040
+ "AssessmentSequence"
3041
+ ]);
3042
+ function warnOrThrow(msg, strict) {
3043
+ if (strict) throw new Error(msg);
3044
+ if (!warnedPairs.has(msg)) {
3045
+ warnedPairs.add(msg);
3046
+ console.warn(msg);
3047
+ }
3048
+ }
3049
+ function validateNode(parent, node, depth, strict) {
3050
+ import_react23.default.Children.forEach(node, (child) => {
3051
+ if (!import_react23.default.isValidElement(child)) return;
3052
+ const blockType = getLessonkitBlockType(child.type);
3053
+ if (!blockType) {
3054
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
3055
+ validateNode(parent, child.props.children, depth, strict);
3056
+ }
3057
+ return;
3058
+ }
3059
+ if (!(0, import_core16.isChildTypeAllowed)(parent, blockType)) {
3060
+ const key = `${parent}:${blockType}`;
3061
+ if (!warnedPairs.has(key)) {
3062
+ warnedPairs.add(key);
3063
+ const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
3064
+ if (strict) throw new Error(msg);
3065
+ console.warn(msg);
3066
+ }
3067
+ }
3068
+ if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
3069
+ const maxDepth = import_core16.COMPOUND_MAX_NESTING_DEPTH[parent];
3070
+ if (depth >= maxDepth) {
3071
+ warnOrThrow(
3072
+ `[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
3073
+ strict
3074
+ );
3075
+ }
3076
+ const nestedParent = blockType;
3077
+ validateNode(nestedParent, child.props.children, depth + 1, strict);
3078
+ } else if (blockType === "Accordion") {
3079
+ const sections = child.props.sections;
3080
+ if (sections) validateAccordionSections(sections, strict);
3081
+ } else if (child.props && typeof child.props === "object" && "children" in child.props) {
3082
+ validateSubtreeForForbidden(
3083
+ child.props.children,
3084
+ import_core16.ACCORDION_FORBIDDEN_CHILD_TYPES,
3085
+ strict
3086
+ );
3087
+ }
3088
+ });
3089
+ }
3090
+ function validateSubtreeForForbidden(node, forbidden, strict) {
3091
+ import_react23.default.Children.forEach(node, (child) => {
3092
+ if (!import_react23.default.isValidElement(child)) return;
3093
+ const blockType = getLessonkitBlockType(child.type);
3094
+ if (blockType && forbidden.includes(blockType)) {
3095
+ warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
3096
+ }
3097
+ if (blockType === "Accordion") {
3098
+ const sections = child.props.sections;
3099
+ if (sections) validateAccordionSections(sections, strict);
3100
+ return;
3101
+ }
3102
+ if (child.props && typeof child.props === "object" && "children" in child.props) {
3103
+ validateSubtreeForForbidden(
3104
+ child.props.children,
3105
+ forbidden,
3106
+ strict
3107
+ );
3108
+ }
3109
+ });
3110
+ }
3111
+ function validateAccordionSections(sections, strict) {
3112
+ if (!isDevEnvironment4() && !strict) return;
3113
+ for (const section of sections) {
3114
+ validateSubtreeForForbidden(section.content, import_core16.ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
3115
+ }
3116
+ }
3117
+ function validateCompoundChildren(parent, children, strict) {
3118
+ if (!isDevEnvironment4() && !strict) return;
3119
+ validateNode(parent, children, 0, strict);
3120
+ }
3121
+
3122
+ // src/compound/warnPersistence.ts
3123
+ var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
3124
+ function warnSharedCompoundStorageKey(opts) {
3125
+ if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
3126
+ console.warn(
3127
+ `[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
3128
+ );
3129
+ }
3130
+
3131
+ // src/blocks/AssessmentSequence.tsx
3132
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3133
+ var AssessmentSequenceInner = (0, import_react24.forwardRef)(
3134
+ function AssessmentSequenceInner2(props, ref) {
3135
+ const { compoundId, childArray, index, setIndex, persistEnabled } = props;
3136
+ const sequential = props.sequential !== false;
3137
+ const { config } = useLessonkit();
3138
+ const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
3139
+ courseId: config.courseId,
3140
+ compoundId,
3141
+ pageCount: childArray.length,
3142
+ index,
3143
+ setIndex,
3144
+ persistEnabled,
3145
+ ref,
3146
+ enableSolutionsButton: props.enableSolutionsButton
3147
+ });
3148
+ validateCompoundChildren("AssessmentSequence", props.children);
3149
+ if (!sequential) {
3150
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
3151
+ }
3152
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
3153
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("p", { children: [
3154
+ "Question ",
3155
+ progress.current,
3156
+ " of ",
3157
+ progress.total
3158
+ ] }),
3159
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
3160
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
3161
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3162
+ "button",
3163
+ {
3164
+ type: "button",
3165
+ "data-testid": "sequence-prev",
3166
+ disabled: visibleIndex === 0 || childArray.length === 0,
3167
+ onClick: goPrev,
3168
+ children: "Previous"
3169
+ }
3170
+ ),
3171
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3172
+ "button",
3173
+ {
3174
+ type: "button",
3175
+ "data-testid": "sequence-next",
3176
+ disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
3177
+ onClick: goNext,
3178
+ children: "Next"
3179
+ }
3180
+ )
3181
+ ] })
3182
+ ] });
3183
+ }
3184
+ );
3185
+ var AssessmentSequence = (0, import_react24.forwardRef)(
3186
+ function AssessmentSequence2(props, ref) {
3187
+ const reactInstanceId = (0, import_react24.useId)();
3188
+ const autoCompoundIdRef = (0, import_react24.useRef)(null);
3189
+ if (!props.blockId && !autoCompoundIdRef.current) {
3190
+ autoCompoundIdRef.current = (0, import_core17.deriveId)(`assessment-sequence-${reactInstanceId}`);
3191
+ }
3192
+ const compoundId = (0, import_react24.useMemo)(
3193
+ () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
3194
+ [props.blockId]
3195
+ );
3196
+ const childArray = import_react24.default.Children.toArray(props.children).filter(
3197
+ import_react24.default.isValidElement
3198
+ );
3199
+ const { config, storage } = useLessonkit();
3200
+ const persistEnabled = config.session?.persistCompoundState !== false;
3201
+ (0, import_react24.useEffect)(() => {
3202
+ warnSharedCompoundStorageKey({
3203
+ persistEnabled,
3204
+ hasExplicitBlockId: Boolean(props.blockId),
3205
+ componentName: "AssessmentSequence"
3206
+ });
3207
+ }, [persistEnabled, props.blockId]);
3208
+ const initialIndex = useCompoundInitialIndex({
3209
+ courseId: config.courseId,
3210
+ compoundId,
3211
+ pageCount: childArray.length,
3212
+ persistEnabled,
3213
+ storage
3214
+ });
3215
+ const [index, setIndex] = (0, import_react24.useState)(initialIndex);
3216
+ const setIndexStable = (0, import_react24.useCallback)((i) => setIndex(i), []);
3217
+ (0, import_react24.useEffect)(() => {
3218
+ setIndex(initialIndex);
3219
+ }, [config.courseId, compoundId, initialIndex]);
3220
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3221
+ AssessmentSequenceInner,
3222
+ {
3223
+ ...props,
3224
+ ref,
3225
+ compoundId,
3226
+ childArray,
3227
+ index,
3228
+ setIndex,
3229
+ persistEnabled
3230
+ }
3231
+ ) });
3232
+ }
3233
+ );
3234
+ setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
3235
+
3236
+ // src/blocks/Text.tsx
3237
+ var import_react25 = require("react");
3238
+ var import_jsx_runtime14 = require("react/jsx-runtime");
3239
+ function Text(props) {
3240
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
3241
+ }
3242
+ setLessonkitBlockType(Text, "Text");
3243
+
3244
+ // src/blocks/Heading.tsx
3245
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3246
+ function Heading(props) {
3247
+ const Tag = `h${props.level}`;
3248
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
3249
+ }
3250
+ setLessonkitBlockType(Heading, "Heading");
3251
+
3252
+ // src/blocks/Image.tsx
3253
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3254
+ function Image(props) {
3255
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3256
+ "img",
3257
+ {
3258
+ src: props.src,
3259
+ alt: props.alt,
3260
+ "data-lk-block-id": props.blockId,
3261
+ "data-testid": props.blockId ? `image-${props.blockId}` : "image",
3262
+ style: { maxWidth: "100%", height: "auto" }
3263
+ }
3264
+ );
3265
+ }
3266
+ setLessonkitBlockType(Image, "Image");
3267
+
3268
+ // src/blocks/Page.tsx
3269
+ var import_react26 = require("react");
3270
+ var import_jsx_runtime17 = require("react/jsx-runtime");
3271
+ function Page(props) {
3272
+ validateCompoundChildren("Page", props.children);
3273
+ const { track } = useLessonkit();
3274
+ const lessonId = useEnclosingLessonId();
3275
+ (0, import_react26.useEffect)(() => {
3276
+ if (props.hidden || !lessonId || props.parentType) return;
3277
+ track(
3278
+ "compound_page_viewed",
3279
+ {
3280
+ blockId: props.blockId,
3281
+ pageIndex: props.pageIndex ?? 0,
3282
+ parentType: props.parentType
3283
+ },
3284
+ { lessonId }
3285
+ );
3286
+ }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
3287
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3288
+ "section",
3289
+ {
3290
+ "aria-label": props.title ?? "Page",
3291
+ "data-lk-block-id": props.blockId,
3292
+ "data-testid": `page-${props.blockId}`,
3293
+ hidden: props.hidden ? true : void 0,
3294
+ children: [
3295
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { children: props.title }) : null,
3296
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: props.children }) })
3297
+ ]
3298
+ }
3299
+ );
3300
+ }
3301
+ setLessonkitBlockType(Page, "Page");
3302
+
3303
+ // src/blocks/InteractiveBook.tsx
3304
+ var import_react27 = __toESM(require("react"), 1);
3305
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3306
+ var InteractiveBookInner = (0, import_react27.forwardRef)(
3307
+ function InteractiveBookInner2(props, ref) {
3308
+ const { blockId, pages, index, setIndex, persistEnabled } = props;
3309
+ validateCompoundChildren("InteractiveBook", pages);
3310
+ const { config, track } = useLessonkit();
3311
+ const lessonId = useEnclosingLessonId();
3312
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3313
+ courseId: config.courseId,
3314
+ compoundId: blockId,
3315
+ pageCount: pages.length,
3316
+ index,
3317
+ setIndex,
3318
+ persistEnabled,
3319
+ ref
3320
+ });
3321
+ const pageTitles = (0, import_react27.useMemo)(
3322
+ () => pages.map((page) => page.props.title),
3323
+ [pages]
3324
+ );
3325
+ (0, import_react27.useEffect)(() => {
3326
+ if (!lessonId || pages.length === 0) return;
3327
+ track(
3328
+ "book_page_viewed",
3329
+ {
3330
+ blockId,
3331
+ pageIndex: visibleIndex,
3332
+ pageTitle: pageTitles[visibleIndex]
3333
+ },
3334
+ { lessonId }
3335
+ );
3336
+ }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
3337
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
3338
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { children: props.title }),
3339
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { children: [
3340
+ "Page ",
3341
+ progress.current,
3342
+ " of ",
3343
+ progress.total
3344
+ ] }),
3345
+ props.showBookScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { "data-testid": "book-score", children: [
3346
+ "Score: ",
3347
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3348
+ " /",
3349
+ " ",
3350
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3351
+ ] }) : null,
3352
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-testid": "interactive-book-page", children: pages.map(
3353
+ (page, i) => import_react27.default.cloneElement(page, {
3354
+ key: page.key ?? page.props.blockId,
3355
+ hidden: i !== visibleIndex,
3356
+ pageIndex: i,
3357
+ parentType: "InteractiveBook"
3358
+ })
3359
+ ) }),
3360
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("nav", { "aria-label": "Book navigation", children: [
3361
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3362
+ "button",
3363
+ {
3364
+ type: "button",
3365
+ "data-testid": "book-prev",
3366
+ disabled: visibleIndex === 0 || pages.length === 0,
3367
+ onClick: goPrev,
3368
+ children: "Previous"
3369
+ }
3370
+ ),
3371
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3372
+ "button",
3373
+ {
3374
+ type: "button",
3375
+ "data-testid": "book-next",
3376
+ disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
3377
+ onClick: goNext,
3378
+ children: "Next"
3379
+ }
3380
+ )
3381
+ ] })
3382
+ ] });
3383
+ }
3384
+ );
3385
+ var InteractiveBook = (0, import_react27.forwardRef)(function InteractiveBook2(props, ref) {
3386
+ const blockId = (0, import_react27.useMemo)(
3387
+ () => normalizeComponentId(props.blockId, "blockId"),
3388
+ [props.blockId]
3389
+ );
3390
+ const pages = import_react27.default.Children.toArray(props.children).filter(
3391
+ import_react27.default.isValidElement
3392
+ );
3393
+ const { config, storage } = useLessonkit();
3394
+ const persistEnabled = config.session?.persistCompoundState !== false;
3395
+ const initialIndex = useCompoundInitialIndex({
3396
+ courseId: config.courseId,
3397
+ compoundId: blockId,
3398
+ pageCount: pages.length,
3399
+ persistEnabled,
3400
+ storage
3401
+ });
3402
+ const [index, setIndex] = (0, import_react27.useState)(initialIndex);
3403
+ const setIndexStable = (0, import_react27.useCallback)((i) => setIndex(i), []);
3404
+ (0, import_react27.useEffect)(() => {
3405
+ setIndex(initialIndex);
3406
+ }, [config.courseId, blockId, initialIndex]);
3407
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3408
+ InteractiveBookInner,
3409
+ {
3410
+ ...props,
3411
+ ref,
3412
+ blockId,
3413
+ pages,
3414
+ index,
3415
+ setIndex,
3416
+ persistEnabled
3417
+ }
3418
+ ) });
3419
+ });
3420
+ setLessonkitBlockType(InteractiveBook, "InteractiveBook");
3421
+
3422
+ // src/blocks/Slide.tsx
3423
+ var import_react28 = require("react");
3424
+ var import_jsx_runtime19 = require("react/jsx-runtime");
3425
+ function Slide(props) {
3426
+ validateCompoundChildren("Slide", props.children);
3427
+ const { track } = useLessonkit();
3428
+ const lessonId = useEnclosingLessonId();
3429
+ (0, import_react28.useEffect)(() => {
3430
+ if (props.hidden || !lessonId || props.parentType) return;
3431
+ track(
3432
+ "compound_page_viewed",
3433
+ {
3434
+ blockId: props.blockId,
3435
+ pageIndex: props.slideIndex ?? 0,
3436
+ parentType: props.parentType
3437
+ },
3438
+ { lessonId }
3439
+ );
3440
+ }, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
3441
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
3442
+ "section",
3443
+ {
3444
+ "aria-label": props.title ?? "Slide",
3445
+ "data-lk-block-id": props.blockId,
3446
+ "data-testid": `slide-${props.blockId}`,
3447
+ hidden: props.hidden ? true : void 0,
3448
+ children: [
3449
+ props.title ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h3", { children: props.title }) : null,
3450
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { children: props.children }) })
3451
+ ]
3452
+ }
3453
+ );
3454
+ }
3455
+ setLessonkitBlockType(Slide, "Slide");
3456
+
3457
+ // src/blocks/SlideDeck.tsx
3458
+ var import_react30 = __toESM(require("react"), 1);
3459
+
3460
+ // src/compound/useCompoundKeyboardNav.ts
3461
+ var import_react29 = require("react");
3462
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
3463
+ function isEditableTarget(target) {
3464
+ if (!(target instanceof HTMLElement)) return false;
3465
+ if (INTERACTIVE_TAGS.has(target.tagName)) return true;
3466
+ if (target.isContentEditable) return true;
3467
+ if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
3468
+ return true;
3469
+ }
3470
+ return false;
3471
+ }
3472
+ function useCompoundKeyboardNav(opts) {
3473
+ const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
3474
+ (0, import_react29.useEffect)(() => {
3475
+ const el = containerRef.current;
3476
+ if (!el || pageCount === 0) return;
3477
+ const onKeyDown = (event) => {
3478
+ if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
3479
+ return;
3480
+ }
3481
+ if (isEditableTarget(event.target)) return;
3482
+ switch (event.key) {
3483
+ case "ArrowRight":
3484
+ case "ArrowDown":
3485
+ if (visibleIndex < pageCount - 1) {
3486
+ event.preventDefault();
3487
+ goNext();
3488
+ }
3489
+ break;
3490
+ case "ArrowLeft":
3491
+ case "ArrowUp":
3492
+ if (visibleIndex > 0) {
3493
+ event.preventDefault();
3494
+ goPrev();
3495
+ }
3496
+ break;
3497
+ case "Home":
3498
+ if (visibleIndex !== 0) {
3499
+ event.preventDefault();
3500
+ setIndex(0);
3501
+ }
3502
+ break;
3503
+ case "End":
3504
+ if (visibleIndex !== pageCount - 1) {
3505
+ event.preventDefault();
3506
+ setIndex(pageCount - 1);
3507
+ }
3508
+ break;
3509
+ default:
3510
+ break;
3511
+ }
3512
+ };
3513
+ el.addEventListener("keydown", onKeyDown);
3514
+ return () => el.removeEventListener("keydown", onKeyDown);
3515
+ }, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
3516
+ }
3517
+
3518
+ // src/blocks/SlideDeck.tsx
3519
+ var import_jsx_runtime20 = require("react/jsx-runtime");
3520
+ var SlideDeckInner = (0, import_react30.forwardRef)(function SlideDeckInner2(props, ref) {
3521
+ const { blockId, slides, index, setIndex, persistEnabled } = props;
3522
+ validateCompoundChildren("SlideDeck", slides);
3523
+ const { config, track } = useLessonkit();
3524
+ const lessonId = useEnclosingLessonId();
3525
+ const containerRef = (0, import_react30.useRef)(null);
3526
+ const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3527
+ courseId: config.courseId,
3528
+ compoundId: blockId,
3529
+ pageCount: slides.length,
3530
+ index,
3531
+ setIndex,
3532
+ persistEnabled,
3533
+ ref
3534
+ });
3535
+ const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), [setIndex]);
3536
+ useCompoundKeyboardNav({
3537
+ containerRef,
3538
+ visibleIndex,
3539
+ pageCount: slides.length,
3540
+ goNext,
3541
+ goPrev,
3542
+ setIndex: setIndexStable
3543
+ });
3544
+ const slideTitles = (0, import_react30.useMemo)(
3545
+ () => slides.map((slide) => slide.props.title),
3546
+ [slides]
3547
+ );
3548
+ (0, import_react30.useEffect)(() => {
3549
+ if (!lessonId || slides.length === 0) return;
3550
+ track(
3551
+ "slide_viewed",
3552
+ {
3553
+ blockId,
3554
+ slideIndex: visibleIndex,
3555
+ slideTitle: slideTitles[visibleIndex]
3556
+ },
3557
+ { lessonId }
3558
+ );
3559
+ }, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
3560
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
3561
+ "section",
3562
+ {
3563
+ ref: containerRef,
3564
+ tabIndex: -1,
3565
+ "aria-label": props.title,
3566
+ "data-testid": "slide-deck",
3567
+ "data-lk-block-id": blockId,
3568
+ children: [
3569
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { children: props.title }),
3570
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { children: [
3571
+ "Slide ",
3572
+ progress.current,
3573
+ " of ",
3574
+ progress.total
3575
+ ] }),
3576
+ props.showDeckScore && ctx ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { "data-testid": "deck-score", children: [
3577
+ "Score: ",
3578
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3579
+ " /",
3580
+ " ",
3581
+ Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3582
+ ] }) : null,
3583
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { "data-testid": "slide-deck-slide", children: slides.map(
3584
+ (slide, i) => import_react30.default.cloneElement(slide, {
3585
+ key: slide.key ?? slide.props.blockId,
3586
+ hidden: i !== visibleIndex,
3587
+ slideIndex: i,
3588
+ parentType: "SlideDeck"
3589
+ })
3590
+ ) }),
3591
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3592
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3593
+ "button",
3594
+ {
3595
+ type: "button",
3596
+ "data-testid": "slide-prev",
3597
+ disabled: visibleIndex === 0 || slides.length === 0,
3598
+ onClick: goPrev,
3599
+ children: "Previous slide"
3600
+ }
3601
+ ),
3602
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3603
+ "button",
3604
+ {
3605
+ type: "button",
3606
+ "data-testid": "slide-next",
3607
+ disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
3608
+ onClick: goNext,
3609
+ children: "Next slide"
3610
+ }
3611
+ )
3612
+ ] })
3613
+ ]
3614
+ }
3615
+ );
3616
+ });
3617
+ var SlideDeck = (0, import_react30.forwardRef)(function SlideDeck2(props, ref) {
3618
+ const blockId = (0, import_react30.useMemo)(
3619
+ () => normalizeComponentId(props.blockId, "blockId"),
3620
+ [props.blockId]
3621
+ );
3622
+ const slides = import_react30.default.Children.toArray(props.children).filter(
3623
+ import_react30.default.isValidElement
3624
+ );
3625
+ const { config, storage } = useLessonkit();
3626
+ const persistEnabled = config.session?.persistCompoundState !== false;
3627
+ const initialIndex = useCompoundInitialIndex({
3628
+ courseId: config.courseId,
3629
+ compoundId: blockId,
3630
+ pageCount: slides.length,
3631
+ persistEnabled,
3632
+ storage
3633
+ });
3634
+ const [index, setIndex] = (0, import_react30.useState)(initialIndex);
3635
+ const setIndexStable = (0, import_react30.useCallback)((i) => setIndex(i), []);
3636
+ (0, import_react30.useEffect)(() => {
3637
+ setIndex(initialIndex);
3638
+ }, [config.courseId, blockId, initialIndex]);
3639
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
3640
+ SlideDeckInner,
3641
+ {
3642
+ ...props,
3643
+ ref,
3644
+ blockId,
3645
+ slides,
3646
+ index,
3647
+ setIndex,
3648
+ persistEnabled
3649
+ }
3650
+ ) });
3651
+ });
3652
+ setLessonkitBlockType(SlideDeck, "SlideDeck");
3653
+
3654
+ // src/blocks/Accordion.tsx
3655
+ var import_react31 = require("react");
3656
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3657
+ function Accordion(props) {
3658
+ if (isDevEnvironment4()) {
3659
+ validateAccordionSections(props.sections);
3660
+ }
3661
+ const [open, setOpen] = (0, import_react31.useState)(/* @__PURE__ */ new Set());
3662
+ const { track } = useLessonkit();
3663
+ const lessonId = useEnclosingLessonId();
3664
+ const baseId = (0, import_react31.useId)();
3665
+ const toggle = (sectionId) => {
3666
+ setOpen((prev) => {
3667
+ const next = new Set(prev);
3668
+ const expanded = !next.has(sectionId);
3669
+ if (expanded) next.add(sectionId);
3670
+ else next.delete(sectionId);
3671
+ track(
3672
+ "accordion_section_toggled",
3673
+ { blockId: props.blockId, sectionId, expanded },
3674
+ lessonId ? { lessonId } : void 0
3675
+ );
3676
+ return next;
3677
+ });
3678
+ };
3679
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3680
+ const expanded = open.has(section.id);
3681
+ const panelId = `${baseId}-${section.id}`;
3682
+ const triggerId = `${baseId}-trigger-${section.id}`;
3683
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { "data-testid": `accordion-section-${section.id}`, children: [
3684
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h4", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3685
+ "button",
3686
+ {
3687
+ id: triggerId,
3688
+ type: "button",
3689
+ "aria-expanded": expanded,
3690
+ "aria-controls": panelId,
3691
+ "data-testid": `accordion-trigger-${section.id}`,
3692
+ onClick: () => toggle(section.id),
3693
+ children: section.title
3694
+ }
3695
+ ) }),
3696
+ expanded ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3697
+ ] }, section.id);
3698
+ }) });
3699
+ }
3700
+ setLessonkitBlockType(Accordion, "Accordion");
3701
+
3702
+ // src/blocks/DialogCards.tsx
3703
+ var import_react32 = require("react");
3704
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3705
+ function DialogCards(props) {
3706
+ const [index, setIndex] = (0, import_react32.useState)(0);
3707
+ const [flipped, setFlipped] = (0, import_react32.useState)(false);
3708
+ const card = props.cards[index];
3709
+ if (!card) return null;
3710
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3711
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { children: [
3712
+ "Card ",
3713
+ index + 1,
3714
+ " of ",
3715
+ props.cards.length
3716
+ ] }),
3717
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3718
+ "button",
3719
+ {
3720
+ type: "button",
3721
+ "data-testid": "dialog-card-flip",
3722
+ "aria-pressed": flipped,
3723
+ onClick: () => setFlipped((f) => !f),
3724
+ style: { minHeight: "6rem", width: "100%" },
3725
+ children: flipped ? card.back : card.front
3726
+ }
3727
+ ),
3728
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("nav", { "aria-label": "Card navigation", children: [
3729
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3730
+ "button",
3731
+ {
3732
+ type: "button",
3733
+ "data-testid": "dialog-prev",
3734
+ disabled: index === 0,
3735
+ onClick: () => {
3736
+ setIndex((i) => i - 1);
3737
+ setFlipped(false);
3738
+ },
3739
+ children: "Previous"
3740
+ }
3741
+ ),
3742
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3743
+ "button",
3744
+ {
3745
+ type: "button",
3746
+ "data-testid": "dialog-next",
3747
+ disabled: index >= props.cards.length - 1,
3748
+ onClick: () => {
3749
+ setIndex((i) => i + 1);
3750
+ setFlipped(false);
3751
+ },
3752
+ children: "Next"
3753
+ }
3754
+ )
3755
+ ] })
3756
+ ] });
3757
+ }
3758
+ setLessonkitBlockType(DialogCards, "DialogCards");
3759
+
3760
+ // src/blocks/Flashcards.tsx
3761
+ var import_react33 = require("react");
3762
+ var import_jsx_runtime23 = require("react/jsx-runtime");
3763
+ function Flashcards(props) {
3764
+ const [index, setIndex] = (0, import_react33.useState)(0);
3765
+ const [face, setFace] = (0, import_react33.useState)("front");
3766
+ const { track } = useLessonkit();
3767
+ const lessonId = useEnclosingLessonId();
3768
+ const card = props.cards[index];
3769
+ if (!card) return null;
3770
+ const flip = () => {
3771
+ const next = face === "front" ? "back" : "front";
3772
+ setFace(next);
3773
+ track(
3774
+ "flashcard_flipped",
3775
+ { blockId: props.blockId, cardIndex: index, face: next },
3776
+ lessonId ? { lessonId } : void 0
3777
+ );
3778
+ };
3779
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3780
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3781
+ props.selfScore ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3782
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3783
+ "button",
3784
+ {
3785
+ type: "button",
3786
+ "data-testid": "flashcard-next",
3787
+ disabled: index >= props.cards.length - 1,
3788
+ onClick: () => {
3789
+ setIndex((i) => i + 1);
3790
+ setFace("front");
3791
+ },
3792
+ children: "Next card"
3793
+ }
3794
+ )
3795
+ ] });
3796
+ }
3797
+ setLessonkitBlockType(Flashcards, "Flashcards");
3798
+
3799
+ // src/blocks/ImageHotspots.tsx
3800
+ var import_react34 = require("react");
3801
+ var import_jsx_runtime24 = require("react/jsx-runtime");
3802
+ function ImageHotspots(props) {
3803
+ const [active, setActive] = (0, import_react34.useState)(null);
3804
+ const { track } = useLessonkit();
3805
+ const lessonId = useEnclosingLessonId();
3806
+ const open = (hotspotId) => {
3807
+ setActive(hotspotId);
3808
+ track(
3809
+ "hotspot_opened",
3810
+ { blockId: props.blockId, hotspotId },
3811
+ lessonId ? { lessonId } : void 0
3812
+ );
3813
+ };
3814
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3815
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3816
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3817
+ props.hotspots.map((h) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3818
+ "button",
3819
+ {
3820
+ type: "button",
3821
+ "aria-expanded": active === h.id,
3822
+ "aria-label": h.label,
3823
+ "data-testid": `hotspot-${h.id}`,
3824
+ style: {
3825
+ position: "absolute",
3826
+ left: `${h.x}%`,
3827
+ top: `${h.y}%`,
3828
+ transform: "translate(-50%, -50%)"
3829
+ },
3830
+ onClick: () => open(h.id),
3831
+ children: "+"
3832
+ },
3833
+ h.id
3834
+ ))
3835
+ ] }),
3836
+ active ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3837
+ props.hotspots.find((h) => h.id === active)?.content,
3838
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3839
+ ] }) : null
3840
+ ] });
3841
+ }
3842
+ setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3843
+
3844
+ // src/blocks/ImageSlider.tsx
3845
+ var import_react35 = require("react");
3846
+ var import_jsx_runtime25 = require("react/jsx-runtime");
3847
+ function ImageSlider(props) {
3848
+ const [index, setIndex] = (0, import_react35.useState)(0);
3849
+ const { track } = useLessonkit();
3850
+ const lessonId = useEnclosingLessonId();
3851
+ const slide = props.slides[index];
3852
+ if (!slide) return null;
3853
+ const goTo = (next) => {
3854
+ setIndex(next);
3855
+ track(
3856
+ "image_slider_changed",
3857
+ { blockId: props.blockId, slideIndex: next },
3858
+ lessonId ? { lessonId } : void 0
3859
+ );
3860
+ };
3861
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
3862
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
3863
+ slide.caption ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { children: slide.caption }) : null,
3864
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("nav", { "aria-label": "Slide navigation", children: [
3865
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3866
+ "button",
3867
+ {
3868
+ type: "button",
3869
+ "data-testid": "slider-prev",
3870
+ disabled: index === 0,
3871
+ onClick: () => goTo(index - 1),
3872
+ children: "Previous"
3873
+ }
3874
+ ),
3875
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { children: [
3876
+ index + 1,
3877
+ " / ",
3878
+ props.slides.length
3879
+ ] }),
3880
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3881
+ "button",
3882
+ {
3883
+ type: "button",
3884
+ "data-testid": "slider-next",
3885
+ disabled: index >= props.slides.length - 1,
3886
+ onClick: () => goTo(index + 1),
3887
+ children: "Next"
3888
+ }
3889
+ )
3890
+ ] })
3891
+ ] });
3892
+ }
3893
+ setLessonkitBlockType(ImageSlider, "ImageSlider");
3894
+
3895
+ // src/blocks/FindHotspot.tsx
3896
+ var import_react36 = require("react");
3897
+ var import_jsx_runtime26 = require("react/jsx-runtime");
3898
+ var INTERACTION6 = "findHotspot";
3899
+ function FindHotspotInner(props, ref) {
3900
+ const checkId = (0, import_react36.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
3901
+ const [selected, setSelected] = (0, import_react36.useState)(null);
3902
+ const [checked, setChecked] = (0, import_react36.useState)(false);
3903
+ const assessment = useAssessmentState(props.enclosingLessonId);
3904
+ const targetIdsKey = props.targets.map((t) => t.id).join("\0");
3905
+ (0, import_react36.useEffect)(() => {
3906
+ setSelected(null);
3907
+ setChecked(false);
3908
+ }, [checkId, props.correctTargetId, targetIdsKey]);
3909
+ const correct = selected === props.correctTargetId;
3910
+ const handle = (0, import_react36.useMemo)(
3911
+ () => buildAssessmentHandle({
3912
+ checkId,
3913
+ getScore: () => checked && correct ? 1 : 0,
3914
+ getMaxScore: () => 1,
3915
+ getAnswerGiven: () => selected !== null,
3916
+ resetTask: () => {
3917
+ setSelected(null);
3918
+ setChecked(false);
3919
+ },
3920
+ showSolutions: () => setSelected(props.correctTargetId),
3921
+ getXAPIData: () => ({
3922
+ checkId,
3923
+ interactionType: INTERACTION6,
3924
+ response: selected ?? void 0,
3925
+ correct: checked ? correct : void 0,
3926
+ score: checked && correct ? 1 : 0,
3927
+ maxScore: 1
3928
+ }),
3929
+ getCurrentState: () => ({ selected, checked }),
3930
+ resume: (state) => {
3931
+ const nextSelected = readStringField(state, "selected");
3932
+ if (typeof nextSelected === "string" || nextSelected === null) {
3933
+ const valid = nextSelected === null || props.targets.some((t) => t.id === nextSelected);
3934
+ setSelected(valid ? nextSelected : null);
3935
+ }
3936
+ readBooleanStateField(state, "checked", setChecked);
3937
+ }
3938
+ }),
3939
+ [checkId, selected, checked, correct, props.correctTargetId, props.targets]
3940
+ );
3941
+ useAssessmentHandleRegistration(checkId, handle, ref);
3942
+ const selectTarget = (id) => {
3943
+ setSelected(id);
3944
+ setChecked(false);
3945
+ };
3946
+ const submit = () => {
3947
+ if (!selected || checked) return;
3948
+ setChecked(true);
3949
+ assessment.answer({
3950
+ checkId,
3951
+ interactionType: INTERACTION6,
3952
+ response: selected,
3953
+ correct
3954
+ });
3955
+ if (correct) {
3956
+ assessment.complete({
3957
+ checkId,
3958
+ interactionType: INTERACTION6,
3959
+ score: 1,
3960
+ maxScore: 1,
3961
+ passingScore: props.passingScore ?? 1
3962
+ });
3963
+ }
3964
+ };
3965
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
3966
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
3967
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3968
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3969
+ "button",
3970
+ {
3971
+ type: "button",
3972
+ "aria-label": t.label,
3973
+ "aria-pressed": selected === t.id,
3974
+ "data-testid": `target-${t.id}`,
3975
+ style: {
3976
+ position: "absolute",
3977
+ left: `${t.x}%`,
3978
+ top: `${t.y}%`,
3979
+ transform: "translate(-50%, -50%)"
1980
3980
  },
1981
- children: zones[part] || "___"
3981
+ onClick: () => selectTarget(t.id),
3982
+ children: t.label
1982
3983
  },
1983
- part
1984
- );
1985
- }) }),
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
3984
+ t.id
3985
+ ))
3986
+ ] }),
3987
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
3988
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
1989
3989
  ] });
1990
3990
  }
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 }) });
3991
+ var FindHotspotInnerForwarded = (0, import_react36.forwardRef)(FindHotspotInner);
3992
+ var FindHotspot = (0, import_react36.forwardRef)(function FindHotspot2(props, ref) {
3993
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
1994
3994
  });
3995
+ setLessonkitBlockType(FindHotspot, "FindHotspot");
1995
3996
 
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]);
3997
+ // src/blocks/FindMultipleHotspots.tsx
3998
+ var import_react37 = require("react");
3999
+ var import_jsx_runtime27 = require("react/jsx-runtime");
4000
+ var INTERACTION7 = "findMultipleHotspots";
4001
+ function FindMultipleHotspotsInner(props, ref) {
4002
+ const checkId = (0, import_react37.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4003
+ const [selected, setSelected] = (0, import_react37.useState)(/* @__PURE__ */ new Set());
4004
+ const [checked, setChecked] = (0, import_react37.useState)(false);
2002
4005
  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;
4006
+ const toggle = (id) => {
4007
+ setSelected((prev) => {
4008
+ const next = new Set(prev);
4009
+ if (next.has(id)) next.delete(id);
4010
+ else next.add(id);
4011
+ return next;
2027
4012
  });
2028
- return {
2029
- getScore: () => score,
2030
- getMaxScore: () => maxScore,
2031
- getAnswerGiven: () => allFilled,
2032
- resetTask: reset,
2033
- showSolutions: () => {
4013
+ setChecked(false);
4014
+ };
4015
+ const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
4016
+ const handle = (0, import_react37.useMemo)(
4017
+ () => buildAssessmentHandle({
4018
+ checkId,
4019
+ getScore: () => checked && correct ? 1 : 0,
4020
+ getMaxScore: () => 1,
4021
+ getAnswerGiven: () => selected.size > 0,
4022
+ resetTask: () => {
4023
+ setSelected(/* @__PURE__ */ new Set());
4024
+ setChecked(false);
2034
4025
  },
4026
+ showSolutions: () => setSelected(new Set(props.correctTargetIds)),
2035
4027
  getXAPIData: () => ({
2036
4028
  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;
4029
+ interactionType: INTERACTION7,
4030
+ response: [...selected],
4031
+ correct: checked ? correct : void 0,
4032
+ score: checked && correct ? 1 : 0,
4033
+ maxScore: 1
4034
+ }),
4035
+ getCurrentState: () => ({ selected: [...selected], checked }),
4036
+ resume: (state) => {
4037
+ const raw = state.selected;
4038
+ if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
4039
+ readBooleanStateField(state, "checked", setChecked);
4040
+ }
4041
+ }),
4042
+ [checkId, selected, checked, correct, props.correctTargetIds]
4043
+ );
4044
+ useAssessmentHandleRegistration(checkId, handle, ref);
4045
+ const submit = () => {
4046
+ if (selected.size === 0 || checked) return;
4047
+ setChecked(true);
2060
4048
  assessment.answer({
2061
4049
  checkId,
2062
- interactionType: INTERACTION5,
2063
- response: assignments,
2064
- correct: allCorrect
4050
+ interactionType: INTERACTION7,
4051
+ response: [...selected],
4052
+ correct
2065
4053
  });
2066
- if (allCorrect && !completedRef.current) {
2067
- completedRef.current = true;
2068
- setPassed(true);
4054
+ if (correct) {
2069
4055
  assessment.complete({
2070
4056
  checkId,
2071
- interactionType: INTERACTION5,
2072
- score: props.targets.length,
2073
- maxScore: props.targets.length,
2074
- passingScore: props.passingScore ?? props.targets.length
4057
+ interactionType: INTERACTION7,
4058
+ score: 1,
4059
+ maxScore: 1,
4060
+ passingScore: props.passingScore ?? 1
2075
4061
  });
2076
4062
  }
2077
4063
  };
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)(
4064
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
4065
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { style: { position: "relative", display: "inline-block" }, children: [
4066
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4067
+ props.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2083
4068
  "button",
2084
4069
  {
2085
4070
  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
4071
+ "aria-label": t.label,
4072
+ "aria-pressed": selected.has(t.id),
4073
+ "data-testid": `target-${t.id}`,
4074
+ style: {
4075
+ position: "absolute",
4076
+ left: `${t.x}%`,
4077
+ top: `${t.y}%`,
4078
+ transform: "translate(-50%, -50%)"
4079
+ },
4080
+ onClick: () => toggle(t.id),
4081
+ children: t.label
2093
4082
  },
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
4083
+ t.id
4084
+ ))
4085
+ ] }),
4086
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
4087
+ checked ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
2132
4088
  ] });
2133
4089
  }
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 }) });
2137
- });
2138
-
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]
2156
- );
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 }) });
4090
+ var FindMultipleHotspotsInnerForwarded = (0, import_react37.forwardRef)(FindMultipleHotspotsInner);
4091
+ var FindMultipleHotspots = (0, import_react37.forwardRef)(
4092
+ function FindMultipleHotspots2(props, ref) {
4093
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
2159
4094
  }
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)(
2171
- "button",
2172
- {
2173
- type: "button",
2174
- "data-testid": "sequence-next",
2175
- disabled: index >= childArray.length - 1,
2176
- onClick: goNext,
2177
- children: "Next"
2178
- }
2179
- )
2180
- ] })
2181
- ] }) });
2182
- }
4095
+ );
4096
+ setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
2183
4097
 
2184
4098
  // src/index.tsx
2185
- var import_core10 = require("@lessonkit/core");
4099
+ var import_core19 = require("@lessonkit/core");
2186
4100
 
2187
4101
  // src/theme/ThemeProvider.tsx
2188
- var import_react15 = __toESM(require("react"), 1);
4102
+ var import_react38 = __toESM(require("react"), 1);
2189
4103
  var import_themes = require("@lessonkit/themes");
2190
4104
 
2191
4105
  // src/theme/applyCssVariables.ts
@@ -2204,11 +4118,11 @@ function applyCssVariables(target, vars, previousKeys) {
2204
4118
  }
2205
4119
 
2206
4120
  // src/theme/ThemeProvider.tsx
2207
- var import_jsx_runtime11 = require("react/jsx-runtime");
2208
- var ThemeContext = (0, import_react15.createContext)(null);
4121
+ var import_jsx_runtime28 = require("react/jsx-runtime");
4122
+ var ThemeContext = (0, import_react38.createContext)(null);
2209
4123
  var useIsoLayoutEffect2 = (
2210
4124
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
2211
- typeof window !== "undefined" ? import_react15.useLayoutEffect : import_react15.default.useEffect
4125
+ typeof window !== "undefined" ? import_react38.useLayoutEffect : import_react38.default.useEffect
2212
4126
  );
2213
4127
  function getSystemMode() {
2214
4128
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -2227,7 +4141,7 @@ function ThemeProvider(props) {
2227
4141
  const preset = props.preset ?? "default";
2228
4142
  const mode = props.mode ?? "light";
2229
4143
  const targetKind = props.target ?? "document";
2230
- const [resolvedMode, setResolvedMode] = (0, import_react15.useState)(
4144
+ const [resolvedMode, setResolvedMode] = (0, import_react38.useState)(
2231
4145
  () => mode === "system" ? getSystemMode() : mode
2232
4146
  );
2233
4147
  useIsoLayoutEffect2(() => {
@@ -2243,20 +4157,20 @@ function ThemeProvider(props) {
2243
4157
  return () => mq.removeEventListener("change", onChange);
2244
4158
  }, [mode]);
2245
4159
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
2246
- const effectiveTheme = (0, import_react15.useMemo)(() => {
4160
+ const effectiveTheme = (0, import_react38.useMemo)(() => {
2247
4161
  const modeBase = resolveModeBase(mode, dataTheme);
2248
4162
  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
4163
  return (0, import_themes.mergeThemes)(base, props.theme ?? {});
2250
4164
  }, [preset, mode, dataTheme, props.theme]);
2251
- const hostRef = (0, import_react15.useRef)(null);
2252
- const appliedKeysRef = (0, import_react15.useRef)(/* @__PURE__ */ new Set());
4165
+ const hostRef = (0, import_react38.useRef)(null);
4166
+ const appliedKeysRef = (0, import_react38.useRef)(/* @__PURE__ */ new Set());
2253
4167
  useIsoLayoutEffect2(() => {
2254
4168
  if (targetKind === "document" && typeof document !== "undefined") {
2255
4169
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
2256
4170
  return () => document.documentElement.removeAttribute("data-lk-theme");
2257
4171
  }
2258
4172
  }, [targetKind, dataTheme]);
2259
- const inject = (0, import_react15.useCallback)(() => {
4173
+ const inject = (0, import_react38.useCallback)(() => {
2260
4174
  const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
2261
4175
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
2262
4176
  if (!el) return;
@@ -2273,7 +4187,7 @@ function ThemeProvider(props) {
2273
4187
  appliedKeysRef.current = /* @__PURE__ */ new Set();
2274
4188
  };
2275
4189
  }, [inject, targetKind]);
2276
- const value = (0, import_react15.useMemo)(
4190
+ const value = (0, import_react38.useMemo)(
2277
4191
  () => ({
2278
4192
  theme: effectiveTheme,
2279
4193
  preset,
@@ -2283,21 +4197,324 @@ function ThemeProvider(props) {
2283
4197
  [effectiveTheme, preset, mode, dataTheme]
2284
4198
  );
2285
4199
  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 }) });
4200
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
2287
4201
  }
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 }) });
4202
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
2289
4203
  }
2290
4204
  function useTheme() {
2291
- const ctx = (0, import_react15.useContext)(ThemeContext);
4205
+ const ctx = (0, import_react38.useContext)(ThemeContext);
2292
4206
  if (!ctx) {
2293
4207
  throw new Error("useTheme must be used within a ThemeProvider");
2294
4208
  }
2295
4209
  return ctx;
2296
4210
  }
2297
4211
 
4212
+ // src/catalogV3Entries.ts
4213
+ var import_core18 = require("@lessonkit/core");
4214
+ var COMPOUND_PARENTS = [
4215
+ "Lesson",
4216
+ "Page",
4217
+ "InteractiveBook",
4218
+ "Slide",
4219
+ "SlideDeck",
4220
+ "AssessmentSequence"
4221
+ ];
4222
+ function extendParents(entry) {
4223
+ if (!entry.parentConstraints?.length) return entry;
4224
+ const merged = /* @__PURE__ */ new Set([...entry.parentConstraints, ...COMPOUND_PARENTS]);
4225
+ return { ...entry, parentConstraints: [...merged] };
4226
+ }
4227
+ var assessmentBehaviourProps = [
4228
+ { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
4229
+ { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
4230
+ { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
4231
+ { name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
4232
+ ];
4233
+ var v3CompoundAndContentEntries = [
4234
+ {
4235
+ type: "Text",
4236
+ category: "content",
4237
+ description: "Paragraph text content.",
4238
+ props: [
4239
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
4240
+ { name: "children", type: "ReactNode", required: true, description: "Text body." }
4241
+ ],
4242
+ requiredIds: [],
4243
+ parentConstraints: [...COMPOUND_PARENTS],
4244
+ a11y: { element: "p", ariaLabel: "Text", keyboard: "N/A", notes: "Semantic paragraph." },
4245
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
4246
+ telemetry: { emits: [] }
4247
+ },
4248
+ {
4249
+ type: "Heading",
4250
+ category: "content",
4251
+ description: "Heading levels 1\u20133.",
4252
+ props: [
4253
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
4254
+ { name: "level", type: "1 | 2 | 3", required: true, description: "Heading level." },
4255
+ { name: "children", type: "ReactNode", required: true, description: "Heading text." }
4256
+ ],
4257
+ requiredIds: [],
4258
+ parentConstraints: [...COMPOUND_PARENTS],
4259
+ a11y: { element: "h1-h3", ariaLabel: "Heading", keyboard: "N/A", notes: "Use one level per outline." },
4260
+ theming: { surface: "global-inherit", stylingNotes: "Inherits theme." },
4261
+ telemetry: { emits: [] }
4262
+ },
4263
+ {
4264
+ type: "Image",
4265
+ category: "content",
4266
+ description: "Image with required alt text.",
4267
+ props: [
4268
+ { name: "blockId", type: "BlockId", required: false, description: "Stable block id." },
4269
+ { name: "src", type: "string", required: true, description: "Image URL." },
4270
+ { name: "alt", type: "string", required: true, description: "Alt text." }
4271
+ ],
4272
+ requiredIds: [],
4273
+ parentConstraints: [...COMPOUND_PARENTS],
4274
+ a11y: { element: "img", ariaLabel: "Image", keyboard: "N/A", notes: "Requires alt." },
4275
+ theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
4276
+ telemetry: { emits: [] }
4277
+ },
4278
+ {
4279
+ type: "Page",
4280
+ category: "container",
4281
+ compoundContract: true,
4282
+ h5pMachineName: "H5P.Column",
4283
+ h5pAlias: "Column",
4284
+ description: "Column layout container (H5P Column / Page).",
4285
+ allowedChildTypes: [...import_core18.PAGE_ALLOWED_CHILD_TYPES],
4286
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Page,
4287
+ props: [
4288
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4289
+ { name: "title", type: "string", required: false, description: "Page title." },
4290
+ { name: "children", type: "ReactNode", required: true, description: "Page content." }
4291
+ ],
4292
+ requiredIds: [],
4293
+ optionalIds: ["blockId"],
4294
+ parentConstraints: ["Lesson", "InteractiveBook"],
4295
+ a11y: { element: "section", ariaLabel: "Page", keyboard: "N/A", notes: "H5P Column equivalent." },
4296
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
4297
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
4298
+ },
4299
+ {
4300
+ type: "InteractiveBook",
4301
+ category: "container",
4302
+ compoundContract: true,
4303
+ h5pMachineName: "H5P.InteractiveBook",
4304
+ h5pAlias: "Interactive Book",
4305
+ description: "Multi-page book with chapter navigation.",
4306
+ allowedChildTypes: [...import_core18.INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
4307
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
4308
+ props: [
4309
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4310
+ { name: "title", type: "string", required: true, description: "Book title." },
4311
+ { name: "showBookScore", type: "boolean", required: false, description: "Show aggregate score." },
4312
+ { name: "children", type: "Page[]", required: true, description: "Page chapters." }
4313
+ ],
4314
+ requiredIds: ["blockId"],
4315
+ parentConstraints: ["Lesson"],
4316
+ a11y: {
4317
+ element: "section",
4318
+ ariaLabel: "Interactive book",
4319
+ keyboard: "Previous/Next chapter navigation.",
4320
+ notes: "H5P Interactive Book equivalent."
4321
+ },
4322
+ theming: { surface: "global-inherit", stylingNotes: "Book chrome." },
4323
+ telemetry: { emits: ["book_page_viewed"], requiresActiveLesson: true }
4324
+ },
4325
+ {
4326
+ type: "Slide",
4327
+ category: "container",
4328
+ compoundContract: true,
4329
+ h5pMachineName: "H5P.CoursePresentation",
4330
+ h5pAlias: "Course Presentation slide",
4331
+ description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
4332
+ allowedChildTypes: [...import_core18.SLIDE_ALLOWED_CHILD_TYPES],
4333
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.Slide,
4334
+ props: [
4335
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4336
+ { name: "title", type: "string", required: false, description: "Slide title." },
4337
+ { name: "children", type: "ReactNode", required: true, description: "Slide content." }
4338
+ ],
4339
+ requiredIds: [],
4340
+ optionalIds: ["blockId"],
4341
+ parentConstraints: ["SlideDeck"],
4342
+ a11y: { element: "section", ariaLabel: "Slide", keyboard: "N/A", notes: "H5P Course Presentation slide row." },
4343
+ theming: { surface: "global-inherit", stylingNotes: "Container." },
4344
+ telemetry: { emits: ["compound_page_viewed"], requiresActiveLesson: true }
4345
+ },
4346
+ {
4347
+ type: "SlideDeck",
4348
+ category: "container",
4349
+ compoundContract: true,
4350
+ h5pMachineName: "H5P.CoursePresentation",
4351
+ h5pAlias: "Course Presentation",
4352
+ description: "Multi-slide presentation with keyboard navigation.",
4353
+ allowedChildTypes: [...import_core18.SLIDE_DECK_ALLOWED_CHILD_TYPES],
4354
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
4355
+ props: [
4356
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4357
+ { name: "title", type: "string", required: true, description: "Deck title." },
4358
+ { name: "showDeckScore", type: "boolean", required: false, description: "Show aggregate score." },
4359
+ { name: "children", type: "Slide[]", required: true, description: "Slides." }
4360
+ ],
4361
+ requiredIds: ["blockId"],
4362
+ parentConstraints: ["Lesson"],
4363
+ a11y: {
4364
+ element: "section",
4365
+ ariaLabel: "Slide deck",
4366
+ keyboard: "Arrow keys, Home, End, Previous/Next slide buttons.",
4367
+ notes: "H5P Course Presentation equivalent."
4368
+ },
4369
+ theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
4370
+ telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
4371
+ },
4372
+ {
4373
+ type: "Accordion",
4374
+ category: "content",
4375
+ h5pMachineName: "H5P.Accordion",
4376
+ h5pAlias: "Accordion",
4377
+ description: "Expandable sections.",
4378
+ props: [
4379
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4380
+ { name: "sections", type: "AccordionSection[]", required: true, description: "Sections." }
4381
+ ],
4382
+ requiredIds: ["blockId"],
4383
+ parentConstraints: [...COMPOUND_PARENTS],
4384
+ a11y: { element: "section", ariaLabel: "Accordion", keyboard: "Button toggles sections.", notes: "No nested accordions." },
4385
+ theming: { surface: "global-inherit", stylingNotes: "Disclosure pattern." },
4386
+ telemetry: { emits: ["accordion_section_toggled"] }
4387
+ },
4388
+ {
4389
+ type: "DialogCards",
4390
+ category: "content",
4391
+ h5pMachineName: "H5P.Dialogcards",
4392
+ h5pAlias: "Dialog Cards",
4393
+ description: "Flip cards with front/back text.",
4394
+ props: [
4395
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4396
+ { name: "cards", type: "DialogCard[]", required: true, description: "Cards." }
4397
+ ],
4398
+ requiredIds: ["blockId"],
4399
+ parentConstraints: [...COMPOUND_PARENTS],
4400
+ a11y: { element: "section", ariaLabel: "Dialog cards", keyboard: "Flip and navigate cards.", notes: "Reduced motion safe." },
4401
+ theming: { surface: "global-inherit", stylingNotes: "Card flip." },
4402
+ telemetry: { emits: [] }
4403
+ },
4404
+ {
4405
+ type: "Flashcards",
4406
+ category: "content",
4407
+ h5pMachineName: "H5P.Flashcards",
4408
+ h5pAlias: "Flashcards",
4409
+ description: "Study flashcards with optional self-score.",
4410
+ props: [
4411
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4412
+ { name: "cards", type: "Flashcard[]", required: true, description: "Cards." },
4413
+ { name: "selfScore", type: "boolean", required: false, description: "Self-score mode." }
4414
+ ],
4415
+ requiredIds: ["blockId"],
4416
+ parentConstraints: [...COMPOUND_PARENTS],
4417
+ a11y: { element: "section", ariaLabel: "Flashcards", keyboard: "Flip and next.", notes: "Not LMS-scored by default." },
4418
+ theming: { surface: "global-inherit", stylingNotes: "Study mode." },
4419
+ telemetry: { emits: ["flashcard_flipped"] }
4420
+ },
4421
+ {
4422
+ type: "ImageHotspots",
4423
+ category: "content",
4424
+ h5pMachineName: "H5P.ImageHotspots",
4425
+ h5pAlias: "Image Hotspots",
4426
+ description: "Image with clickable hotspot popovers.",
4427
+ props: [
4428
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4429
+ { name: "src", type: "string", required: true, description: "Image URL." },
4430
+ { name: "alt", type: "string", required: true, description: "Alt text." },
4431
+ { name: "hotspots", type: "HotspotSpec[]", required: true, description: "Hotspots." }
4432
+ ],
4433
+ requiredIds: ["blockId"],
4434
+ parentConstraints: [...COMPOUND_PARENTS],
4435
+ a11y: { element: "section", ariaLabel: "Image hotspots", keyboard: "Buttons on image.", notes: "Popover dialog." },
4436
+ theming: { surface: "global-inherit", stylingNotes: "Positioned hotspots." },
4437
+ telemetry: { emits: ["hotspot_opened"] }
4438
+ },
4439
+ {
4440
+ type: "ImageSlider",
4441
+ category: "content",
4442
+ h5pMachineName: "H5P.ImageSlider",
4443
+ h5pAlias: "Image Slider",
4444
+ description: "Carousel of images.",
4445
+ props: [
4446
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4447
+ { name: "slides", type: "ImageSlide[]", required: true, description: "Slides." }
4448
+ ],
4449
+ requiredIds: ["blockId"],
4450
+ parentConstraints: [...COMPOUND_PARENTS],
4451
+ a11y: { element: "section", ariaLabel: "Image slider", keyboard: "Previous/next slide.", notes: "Carousel." },
4452
+ theming: { surface: "global-inherit", stylingNotes: "Slider." },
4453
+ telemetry: { emits: ["image_slider_changed"] }
4454
+ },
4455
+ {
4456
+ type: "FindHotspot",
4457
+ category: "assessment",
4458
+ assessmentContract: true,
4459
+ h5pMachineName: "H5P.ImageHotspotQuestion",
4460
+ h5pAlias: "Find the Hotspot",
4461
+ description: "Select the correct region on an image.",
4462
+ props: [
4463
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4464
+ { name: "src", type: "string", required: true, description: "Image URL." },
4465
+ { name: "alt", type: "string", required: true, description: "Alt text." },
4466
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
4467
+ { name: "correctTargetId", type: "string", required: true, description: "Correct target id." },
4468
+ ...assessmentBehaviourProps
4469
+ ],
4470
+ requiredIds: ["checkId"],
4471
+ parentConstraints: [...COMPOUND_PARENTS],
4472
+ a11y: { element: "section", ariaLabel: "Find the hotspot", keyboard: "Select target buttons.", notes: "Scored." },
4473
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4474
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4475
+ },
4476
+ {
4477
+ type: "FindMultipleHotspots",
4478
+ category: "assessment",
4479
+ assessmentContract: true,
4480
+ h5pMachineName: "H5P.ImageMultipleHotspotQuestion",
4481
+ h5pAlias: "Find Multiple Hotspots",
4482
+ description: "Select all correct regions on an image.",
4483
+ props: [
4484
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
4485
+ { name: "src", type: "string", required: true, description: "Image URL." },
4486
+ { name: "alt", type: "string", required: true, description: "Alt text." },
4487
+ { name: "targets", type: "HotspotTarget[]", required: true, description: "Targets." },
4488
+ { name: "correctTargetIds", type: "string[]", required: true, description: "Correct target ids." },
4489
+ ...assessmentBehaviourProps
4490
+ ],
4491
+ requiredIds: ["checkId"],
4492
+ parentConstraints: [...COMPOUND_PARENTS],
4493
+ a11y: { element: "section", ariaLabel: "Find multiple hotspots", keyboard: "Toggle targets.", notes: "Scored." },
4494
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
4495
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
4496
+ }
4497
+ ];
4498
+ function buildV3CatalogFromV2(v2) {
4499
+ const patched = v2.map((entry) => {
4500
+ const base = extendParents(entry);
4501
+ if (entry.type === "AssessmentSequence") {
4502
+ return {
4503
+ ...base,
4504
+ compoundContract: true,
4505
+ allowedChildTypes: [...import_core18.ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
4506
+ maxNestingDepth: import_core18.COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
4507
+ };
4508
+ }
4509
+ return base;
4510
+ });
4511
+ return [...patched, ...v3CompoundAndContentEntries];
4512
+ }
4513
+
2298
4514
  // src/blockCatalog.ts
2299
4515
  var blockCatalogVersion = 1;
2300
4516
  var blockCatalogV2Version = 2;
4517
+ var blockCatalogV3Version = 3;
2301
4518
  var BLOCK_CATALOG = [
2302
4519
  {
2303
4520
  type: "Course",
@@ -2484,7 +4701,7 @@ var BLOCK_CATALOG = [
2484
4701
  }
2485
4702
  }
2486
4703
  ];
2487
- var assessmentBehaviourProps = [
4704
+ var assessmentBehaviourProps2 = [
2488
4705
  { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
2489
4706
  { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
2490
4707
  { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
@@ -2502,7 +4719,7 @@ var v2AssessmentEntries = [
2502
4719
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2503
4720
  { name: "question", type: "string", required: true, description: "Question text." },
2504
4721
  { name: "answer", type: "boolean", required: true, description: "Correct answer." },
2505
- ...assessmentBehaviourProps
4722
+ ...assessmentBehaviourProps2
2506
4723
  ],
2507
4724
  requiredIds: ["checkId"],
2508
4725
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2527,7 +4744,7 @@ var v2AssessmentEntries = [
2527
4744
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2528
4745
  { name: "template", type: "string", required: true, description: "Text with *blank* markers." },
2529
4746
  { name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
2530
- ...assessmentBehaviourProps
4747
+ ...assessmentBehaviourProps2
2531
4748
  ],
2532
4749
  requiredIds: ["checkId"],
2533
4750
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2551,7 +4768,7 @@ var v2AssessmentEntries = [
2551
4768
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2552
4769
  { name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
2553
4770
  { name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
2554
- ...assessmentBehaviourProps
4771
+ ...assessmentBehaviourProps2
2555
4772
  ],
2556
4773
  requiredIds: ["checkId"],
2557
4774
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2575,7 +4792,7 @@ var v2AssessmentEntries = [
2575
4792
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2576
4793
  { name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
2577
4794
  { name: "words", type: "string[]", required: true, description: "Draggable word bank." },
2578
- ...assessmentBehaviourProps
4795
+ ...assessmentBehaviourProps2
2579
4796
  ],
2580
4797
  requiredIds: ["checkId"],
2581
4798
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2599,7 +4816,7 @@ var v2AssessmentEntries = [
2599
4816
  { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2600
4817
  { name: "text", type: "string", required: true, description: "Source text." },
2601
4818
  { name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
2602
- ...assessmentBehaviourProps
4819
+ ...assessmentBehaviourProps2
2603
4820
  ],
2604
4821
  requiredIds: ["checkId"],
2605
4822
  parentConstraints: ["Lesson", "AssessmentSequence"],
@@ -2621,7 +4838,7 @@ var v2AssessmentEntries = [
2621
4838
  props: [
2622
4839
  { name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
2623
4840
  { name: "sequential", type: "boolean", required: false, description: "One question at a time." },
2624
- ...assessmentBehaviourProps.filter((p) => p.name !== "passingScore")
4841
+ ...assessmentBehaviourProps2.filter((p) => p.name !== "passingScore")
2625
4842
  ],
2626
4843
  requiredIds: [],
2627
4844
  parentConstraints: ["Lesson"],
@@ -2639,6 +4856,7 @@ var BLOCK_CATALOG_V2 = [
2639
4856
  ...BLOCK_CATALOG,
2640
4857
  ...v2AssessmentEntries
2641
4858
  ];
4859
+ var BLOCK_CATALOG_V3 = buildV3CatalogFromV2(BLOCK_CATALOG_V2);
2642
4860
  function cloneCatalogEntry(entry) {
2643
4861
  return {
2644
4862
  ...entry,
@@ -2646,6 +4864,7 @@ function cloneCatalogEntry(entry) {
2646
4864
  aliases: entry.aliases ? [...entry.aliases] : void 0,
2647
4865
  optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
2648
4866
  parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
4867
+ allowedChildTypes: entry.allowedChildTypes ? [...entry.allowedChildTypes] : void 0,
2649
4868
  a11y: { ...entry.a11y },
2650
4869
  theming: {
2651
4870
  ...entry.theming,
@@ -2658,35 +4877,51 @@ function cloneCatalogEntry(entry) {
2658
4877
  };
2659
4878
  }
2660
4879
  function buildBlockCatalog(opts) {
2661
- const version = opts?.version ?? 2;
2662
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4880
+ const version = opts?.version ?? 3;
4881
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2663
4882
  return source.map((entry) => cloneCatalogEntry(entry));
2664
4883
  }
2665
4884
  function getBlockCatalogEntry(type, opts) {
2666
- const version = opts?.version ?? 2;
2667
- const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
4885
+ const version = opts?.version ?? 3;
4886
+ const source = version === 3 ? BLOCK_CATALOG_V3 : version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2668
4887
  return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
2669
4888
  }
2670
4889
  // Annotate the CommonJS export names for ESM import in node:
2671
4890
  0 && (module.exports = {
4891
+ Accordion,
2672
4892
  AssessmentSequence,
2673
4893
  BLOCK_CATALOG,
2674
4894
  BLOCK_CATALOG_V2,
4895
+ BLOCK_CATALOG_V3,
2675
4896
  Course,
4897
+ DialogCards,
2676
4898
  DragAndDrop,
2677
4899
  DragTheWords,
2678
4900
  FillInTheBlanks,
4901
+ FindHotspot,
4902
+ FindMultipleHotspots,
4903
+ Flashcards,
4904
+ Heading,
4905
+ Image,
4906
+ ImageHotspots,
4907
+ ImageSlider,
4908
+ InteractiveBook,
2679
4909
  KnowledgeCheck,
2680
4910
  Lesson,
2681
4911
  LessonkitProvider,
2682
4912
  MarkTheWords,
4913
+ Page,
2683
4914
  ProgressTracker,
2684
4915
  Quiz,
2685
4916
  Reflection,
2686
4917
  Scenario,
4918
+ Slide,
4919
+ SlideDeck,
4920
+ Text,
2687
4921
  ThemeProvider,
2688
4922
  TrueFalse,
2689
4923
  blockCatalogV2Version,
4924
+ blockCatalogV3Version,
2690
4925
  blockCatalogVersion,
2691
4926
  buildBlockCatalog,
2692
4927
  buildTelemetryEvent,