@lessonkit/react 1.0.2 → 1.2.0

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