@lessonkit/react 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4304 +1,198 @@
1
- // src/components.tsx
2
- import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
3
- import { visuallyHiddenStyle as visuallyHiddenStyle2 } from "@lessonkit/accessibility";
4
-
5
- // src/context.tsx
6
- import { createContext } from "react";
7
-
8
- // src/provider/useLessonkitProviderRuntime.ts
9
- import {
10
- useCallback,
11
- useEffect,
12
- useLayoutEffect,
13
- useMemo,
14
- useRef,
15
- useState
16
- } from "react";
17
- import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
18
-
19
- // src/runtime/observability.ts
20
- import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
21
- function createXapiQueueFromObservability(observability) {
22
- const opts = {};
23
- if (observability?.onXapiQueueDepth) {
24
- opts.onDepth = observability.onXapiQueueDepth;
25
- }
26
- if (observability?.onXapiQueueCap) {
27
- opts.onCap = observability.onXapiQueueCap;
28
- }
29
- return createInMemoryXAPIQueue(opts);
30
- }
31
- function wrapBatchSink(batchSink, observability) {
32
- if (!batchSink || !observability?.onTelemetrySinkError) return batchSink;
33
- const onError = observability.onTelemetrySinkError;
34
- return async (events) => {
35
- try {
36
- await batchSink(events);
37
- } catch (err) {
38
- onError(err, { sinkId: "tracking-batch" });
39
- throw err;
40
- }
41
- };
42
- }
43
- function warnMissingProductionObservability(observability, opts) {
44
- let isProduction = false;
45
- try {
46
- isProduction = import.meta.env?.PROD === true;
47
- } catch {
48
- }
49
- if (!isProduction) {
50
- const g = globalThis;
51
- isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
52
- }
53
- if (!isProduction) return;
54
- if (!opts.trackingEnabled && !opts.xapiEnabled) return;
55
- const hooks = [
56
- observability?.onTelemetrySinkError,
57
- observability?.onTelemetryBufferDrop,
58
- observability?.onXapiQueueDepth,
59
- observability?.onXapiQueueCap,
60
- observability?.onLxpackBridgeMiss
61
- ];
62
- if (hooks.some(Boolean)) return;
63
- if (typeof console !== "undefined") {
64
- console.warn(
65
- "[lessonkit] Production deployment without observability hooks \u2014 telemetry/xAPI failures and buffer drops will be silent. See https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html"
66
- );
67
- }
68
- }
69
- function wrapTrackingSink(sink, observability) {
70
- if (!sink || !observability?.onTelemetrySinkError) return sink;
71
- const onError = observability.onTelemetrySinkError;
72
- return ((event) => {
73
- try {
74
- const result = sink(event);
75
- if (result != null && typeof result.catch === "function") {
76
- return result.catch((err) => {
77
- onError(err, { sinkId: "tracking" });
78
- });
79
- }
80
- return result;
81
- } catch (err) {
82
- onError(err, { sinkId: "tracking" });
83
- return void 0;
84
- }
85
- });
86
- }
87
-
88
- // src/provider/useLessonkitProviderRuntime.ts
89
- import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
90
-
91
- // src/runtime/emitTelemetry.ts
92
- import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
93
-
94
- // src/runtime/telemetryPipeline.ts
95
- import {
96
- createTelemetryPipeline,
97
- createTrackingPipelineSink
98
- } from "@lessonkit/core";
99
- import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
100
-
101
- // src/runtime/lxpackBridge.ts
102
1
  import {
103
- dispatchBridgeAction,
104
- forwardTelemetryToBridge,
105
- getLxpackBridge,
106
- mapLessonkitTelemetryToBridgeAction,
107
- telemetryEventToLessonkit
108
- } from "@lessonkit/lxpack/bridge";
109
- var BRIDGE_MISS_EVENT_NAMES = /* @__PURE__ */ new Set([
110
- "course_completed",
111
- "lesson_completed",
112
- "assessment_completed",
113
- "quiz_completed"
114
- ]);
115
- function forwardTelemetryToLxpack(event, mode = "auto", opts) {
116
- if (mode === "auto" && opts?.onBridgeMiss && BRIDGE_MISS_EVENT_NAMES.has(event.name) && !getLxpackBridge()) {
117
- opts.onBridgeMiss(event);
118
- }
119
- forwardTelemetryToBridge(event, mode);
120
- }
121
-
122
- // src/runtime/telemetryPipeline.ts
123
- function isDevEnvironment() {
124
- const g = globalThis;
125
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
126
- }
127
- function createLegacyPipeline(opts, extraSinks = []) {
128
- return createTelemetryPipeline([
129
- createTrackingPipelineSink("tracking", (event) => opts.tracking.track(event)),
130
- {
131
- id: "xapi",
132
- emit(event) {
133
- try {
134
- const statement = telemetryEventToXAPIStatement(event);
135
- if (statement) opts.xapi?.send(statement);
136
- } catch (err) {
137
- if (isDevEnvironment()) {
138
- console.warn(
139
- "[lessonkit] xAPI mapping skipped:",
140
- err instanceof Error ? err.message : err
141
- );
142
- }
143
- }
144
- }
145
- },
146
- {
147
- id: "lxpack-bridge",
148
- emit(event) {
149
- forwardTelemetryToLxpack(event, opts.lxpackBridge, {
150
- onBridgeMiss: opts.onLxpackBridgeMiss
151
- });
152
- }
153
- },
154
- ...extraSinks
155
- ]);
156
- }
157
- function emitThroughPipeline(event, opts, extraSinks) {
158
- createLegacyPipeline(opts, extraSinks).emit(event);
159
- }
160
-
161
- // src/runtime/emitTelemetry.ts
162
- var warnedMissingCourseId = false;
163
- function isDevEnvironment2() {
164
- const g = globalThis;
165
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
166
- }
167
- function emitTelemetry(tracking, xapi, event, opts) {
168
- if (!event.courseId) {
169
- if (isDevEnvironment2() && !warnedMissingCourseId) {
170
- warnedMissingCourseId = true;
171
- console.warn("[lessonkit] telemetry event missing courseId");
172
- }
173
- return;
174
- }
175
- const legacy = {
176
- tracking,
177
- xapi,
178
- lxpackBridge: opts?.lxpackBridge ?? "auto",
179
- onLxpackBridgeMiss: opts?.onLxpackBridgeMiss
180
- };
181
- emitThroughPipeline(event, legacy, opts?.extraSinks);
182
- }
183
-
184
- // src/runtime/ports.ts
2
+ Accordion,
3
+ ArithmeticQuiz,
4
+ AssessmentSequence,
5
+ DialogCards,
6
+ DragAndDrop,
7
+ DragTheWords,
8
+ Essay,
9
+ FillInTheBlanks,
10
+ FindHotspot,
11
+ FindMultipleHotspots,
12
+ Flashcards,
13
+ Heading,
14
+ Image,
15
+ ImageHotspots,
16
+ ImagePairing,
17
+ ImageSequencing,
18
+ ImageSlider,
19
+ InformationWall,
20
+ InteractiveBook,
21
+ InteractiveVideo,
22
+ MarkTheWords,
23
+ MemoryGame,
24
+ Page,
25
+ ParallaxSlideshow,
26
+ Questionnaire,
27
+ Slide,
28
+ SlideDeck,
29
+ Summary,
30
+ Text,
31
+ TimedCue,
32
+ TrueFalse,
33
+ Video
34
+ } from "./chunk-4LQ4TTEE.js";
185
35
  import {
186
- createDefaultClock,
187
- createGlobalTimer,
188
- createNoopStorage,
189
- createSessionStoragePort,
190
- resetStoragePortForTests
191
- } from "@lessonkit/core";
192
-
193
- // src/provider/useLessonkitProviderRuntime.ts
194
- import { resetSharedVolatileSessionIdForTests } from "@lessonkit/core";
195
-
196
- // src/runtime/progress.ts
197
- import { createProgressController } from "@lessonkit/core";
198
-
199
- // src/runtime/xapi.ts
200
- import { createXAPIClient } from "@lessonkit/xapi";
201
- function createXapiClientFromConfig(config, queue) {
202
- if (config.xapi?.enabled === false) return null;
203
- if (config.xapi?.client) return config.xapi.client;
204
- if (!config.courseId) return null;
205
- const hasTransport = typeof config.xapi?.transport === "function";
206
- if (!hasTransport && config.xapi?.enabled !== true) return null;
207
- return createXAPIClient({
208
- courseId: config.courseId,
209
- transport: config.xapi?.transport,
210
- exitTransport: config.xapi?.exitTransport,
211
- queue
212
- });
213
- }
214
-
215
- // src/runtime/session.ts
36
+ KnowledgeCheck,
37
+ Quiz,
38
+ getLessonMountCount,
39
+ registerLessonMount,
40
+ resetQuizWarningsForTests
41
+ } from "./chunk-UUTXECVW.js";
216
42
  import {
217
- SESSION_STORAGE_KEY,
218
- getTabSessionId,
219
- resolveSessionId,
220
- hasCourseStarted,
221
- markCourseStarted,
222
- hasCourseStartedEmittedToTracking,
223
- markCourseStartedEmittedToTracking,
224
- hasCourseStartedPipelineDelivered,
225
- markCourseStartedPipelineDelivered,
226
- migrateCourseStartedMark
227
- } from "@lessonkit/core";
228
-
229
- // src/runtime/courseStartedPipeline.ts
230
- import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
231
- function isDevEnvironment3() {
232
- const g = globalThis;
233
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
234
- }
235
- function warnExtraSinkFailure(sinkId, err) {
236
- if (isDevEnvironment3()) {
237
- console.warn(
238
- `[lessonkit] course_started extra sink "${sinkId}" failed:`,
239
- err instanceof Error ? err.message : err
240
- );
241
- }
242
- }
243
- async function emitExtraSinks(sinks, event, emitCtx) {
244
- await Promise.all(
245
- sinks.map(async (sink) => {
246
- let result;
247
- try {
248
- result = sink.emit(event, emitCtx);
249
- } catch (err) {
250
- warnExtraSinkFailure(sink.id, err);
251
- throw err;
252
- }
253
- if (result != null && typeof result.then === "function") {
254
- try {
255
- await result;
256
- } catch (err) {
257
- warnExtraSinkFailure(sink.id, err);
258
- throw err;
259
- }
260
- }
261
- })
262
- );
263
- }
264
- async function emitCourseStartedNonTrackingPipeline(opts) {
265
- let xapiStatementSent = false;
266
- if (!opts.skipXapi && opts.xapi) {
267
- const statement = telemetryEventToXAPIStatement2(opts.event);
268
- if (statement) {
269
- opts.xapi.send(statement);
270
- await opts.xapi.flush();
271
- xapiStatementSent = true;
272
- }
273
- }
274
- forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
275
- onBridgeMiss: opts.onLxpackBridgeMiss
276
- });
277
- const emitCtx = {
278
- courseId: opts.event.courseId,
279
- sessionId: opts.event.sessionId,
280
- attemptId: opts.event.attemptId
281
- };
282
- await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
283
- return { xapiStatementSent };
284
- }
285
-
286
- // src/runtime/plugins.ts
287
- import { buildPluginContext as buildPluginContextFromCore, createPluginRegistry } from "@lessonkit/core";
288
- function createReactPluginHost(plugins) {
289
- if (!plugins?.length) return null;
290
- return createPluginRegistry(plugins);
291
- }
292
- function buildPluginContext(opts) {
293
- return buildPluginContextFromCore(opts);
294
- }
295
- function emitTelemetryWithPlugins(opts) {
296
- const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
297
- if (next === null) return;
298
- emitTelemetry(opts.tracking, opts.xapi, next, {
299
- lxpackBridge: opts.lxpackBridge ?? "auto",
300
- extraSinks: opts.extraSinks,
301
- onLxpackBridgeMiss: opts.onLxpackBridgeMiss
302
- });
303
- }
304
-
305
- // src/provider/courseStarted/emit.ts
306
- var courseStartedTrackingFlights = /* @__PURE__ */ new Map();
307
- function isTrackingActive(tracking) {
308
- return tracking?.enabled !== false;
309
- }
310
- function isCourseStartedSinkSettled(result) {
311
- return result === "emitted";
312
- }
313
- function buildCourseStartedEvent(opts) {
314
- const pluginCtx = buildPluginContext({
315
- courseId: opts.courseId,
316
- sessionId: opts.sessionId,
317
- attemptId: opts.attemptId,
318
- user: opts.user
319
- });
320
- const built = buildTelemetryEvent({
321
- name: "course_started",
322
- courseId: opts.courseId,
323
- sessionId: opts.sessionId,
324
- attemptId: opts.attemptId,
325
- user: opts.user
326
- });
327
- return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
328
- }
329
- async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
330
- const flightKey = `${sessionId}:${courseId}`;
331
- if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
332
- return true;
333
- }
334
- const existing = courseStartedTrackingFlights.get(flightKey);
335
- if (existing) {
336
- const settled = await existing;
337
- if (settled) return true;
338
- }
339
- let resolveFlight;
340
- const flight = new Promise((resolve) => {
341
- resolveFlight = resolve;
342
- });
343
- courseStartedTrackingFlights.set(flightKey, flight);
344
- void (async () => {
345
- try {
346
- if (shouldCommit && !shouldCommit()) {
347
- resolveFlight(false);
348
- return;
349
- }
350
- tracking.track(event);
351
- const delivered = await tracking.flush?.();
352
- if (delivered === false) {
353
- resolveFlight(false);
354
- return;
355
- }
356
- if (markCourseStartedEmittedToTracking(storage, sessionId, courseId) === false) {
357
- resolveFlight(false);
358
- return;
359
- }
360
- resolveFlight(true);
361
- } catch {
362
- resolveFlight(false);
363
- } finally {
364
- courseStartedTrackingFlights.delete(flightKey);
365
- }
366
- })();
367
- return flight;
368
- }
369
- async function emitCourseStartedPipelineOnly(opts) {
370
- try {
371
- if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
372
- const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
373
- event: opts.event,
374
- xapi: opts.xapi,
375
- lxpackBridge: opts.lxpackBridge,
376
- onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
377
- extraSinks: opts.extraSinks,
378
- skipXapi: opts.skipXapi
379
- });
380
- if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
381
- if (markCourseStarted(opts.storage, opts.sessionId, opts.courseId) === false) return "failed";
382
- if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
383
- return "failed";
384
- }
385
- if (xapiStatementSent) {
386
- opts.onXapiStatementSent?.();
387
- }
388
- return "emitted";
389
- } catch {
390
- return "failed";
391
- }
392
- }
393
- async function emitCourseStarted(opts) {
394
- const event = buildCourseStartedEvent(opts);
395
- if (event === null) return "filtered";
396
- const tracked = await emitCourseStartedToTracking(
397
- opts.tracking,
398
- opts.storage,
399
- opts.sessionId,
400
- opts.courseId,
401
- event,
402
- opts.shouldCommit
403
- );
404
- if (!tracked) return "failed";
405
- return emitCourseStartedPipelineOnly({
406
- ...opts,
407
- event,
408
- skipXapi: opts.skipXapi,
409
- onXapiStatementSent: opts.onXapiStatementSent,
410
- shouldCommit: opts.shouldCommit
411
- });
412
- }
413
- async function emitCourseStartedToTrackingOnly(opts) {
414
- const event = buildCourseStartedEvent(opts);
415
- if (event === null) return "filtered";
416
- const tracked = await emitCourseStartedToTracking(
417
- opts.tracking,
418
- opts.storage,
419
- opts.sessionId,
420
- opts.courseId,
421
- event,
422
- opts.shouldCommit
423
- );
424
- if (!tracked) return "failed";
425
- try {
426
- if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
427
- await emitCourseStartedNonTrackingPipeline({
428
- event,
429
- xapi: null,
430
- lxpackBridge: opts.lxpackBridge,
431
- onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
432
- extraSinks: opts.extraSinks,
433
- skipXapi: true
434
- });
435
- if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false) {
436
- return "failed";
437
- }
438
- return "emitted";
439
- } catch {
440
- return "failed";
441
- }
442
- }
443
- async function emitPendingCourseStarted(opts) {
444
- const trackingEmitted = hasCourseStartedEmittedToTracking(
445
- opts.storage,
446
- opts.sessionId,
447
- opts.courseId
448
- );
449
- const sessionStarted = hasCourseStarted(opts.storage, opts.sessionId, opts.courseId);
450
- if (sessionStarted && !trackingEmitted) {
451
- return emitCourseStartedToTrackingOnly(opts);
452
- }
453
- if (trackingEmitted && !sessionStarted) {
454
- const event = buildCourseStartedEvent(opts);
455
- if (event === null) return "filtered";
456
- return emitCourseStartedPipelineOnly({ ...opts, event });
457
- }
458
- if (!trackingEmitted && !sessionStarted) {
459
- return emitCourseStarted(opts);
460
- }
461
- const pipelineDelivered = hasCourseStartedPipelineDelivered(
462
- opts.storage,
463
- opts.sessionId,
464
- opts.courseId
465
- );
466
- if (sessionStarted && trackingEmitted && pipelineDelivered) {
467
- return "emitted";
468
- }
469
- if (sessionStarted && trackingEmitted && !pipelineDelivered) {
470
- const event = buildCourseStartedEvent(opts);
471
- if (event === null) return "filtered";
472
- return emitCourseStartedPipelineOnly({
473
- ...opts,
474
- event,
475
- skipXapi: opts.skipXapi,
476
- onXapiStatementSent: opts.onXapiStatementSent
477
- });
478
- }
479
- return "emitted";
480
- }
481
- function assertTrackingSinkConfig(tracking) {
482
- if (!tracking?.sink || !tracking?.batchSink) return;
483
- throw new Error(
484
- "[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
485
- );
486
- }
487
-
488
- // src/runtime/telemetry.ts
489
- import { createTrackingClient } from "@lessonkit/core";
490
- function createTrackingClientFromConfig(config, observability) {
491
- if (config.tracking?.enabled === false) return createTrackingClient();
492
- if (config.tracking?.createClient) return config.tracking.createClient();
493
- return createTrackingClient({
494
- sink: config.tracking?.sink,
495
- batchSink: config.tracking?.batchSink,
496
- batch: config.tracking?.batch,
497
- exitBatchSink: config.tracking?.exitBatchSink,
498
- onBufferDrop: observability?.onTelemetryBufferDrop
499
- });
500
- }
501
- async function disposeTrackingClient(client) {
502
- try {
503
- await client?.flush?.();
504
- } catch {
505
- }
506
- try {
507
- await client?.dispose?.();
508
- } catch {
509
- }
510
- }
511
-
512
- // src/provider/useLessonkitProviderRuntime.ts
513
- var useIsoLayoutEffect = (
514
- /* v8 ignore next -- SSR uses useEffect when window is unavailable */
515
- typeof window !== "undefined" ? useLayoutEffect : useEffect
516
- );
517
- var defaultStorage = createSessionStoragePort();
518
- function useLessonkitProviderRuntime(config) {
519
- const normalizedCourseId = useMemo(
520
- () => assertValidId(config.courseId, "courseId"),
521
- [config.courseId]
522
- );
523
- const normalizedConfig = useMemo(
524
- () => ({ ...config, courseId: normalizedCourseId }),
525
- [config, normalizedCourseId]
526
- );
527
- const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
528
- useEffect(() => {
529
- if (useV2Runtime) return;
530
- const g = globalThis;
531
- if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
532
- console.warn(
533
- '[lessonkit] LessonkitProvider runtimeVersion "v1" is deprecated; omit or use "v2" (default). v1 will be removed in LessonKit 2.0.'
534
- );
535
- }, [useV2Runtime]);
536
- const extraSinksRef = useRef(normalizedConfig.sinks);
537
- extraSinksRef.current = normalizedConfig.sinks;
538
- const headlessRef = useRef(null);
539
- const sessionIdRef = useRef(resolveSessionId(defaultStorage, normalizedConfig.session?.sessionId));
540
- const prevConfiguredSessionIdRef = useRef(normalizedConfig.session?.sessionId);
541
- if (normalizedConfig.session?.sessionId) {
542
- sessionIdRef.current = normalizedConfig.session.sessionId;
543
- } else if (prevConfiguredSessionIdRef.current) {
544
- sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
545
- }
546
- const attemptIdRef = useRef(normalizedConfig.session?.attemptId);
547
- const userRef = useRef(normalizedConfig.session?.user);
548
- attemptIdRef.current = normalizedConfig.session?.attemptId;
549
- userRef.current = normalizedConfig.session?.user;
550
- const courseIdRef = useRef(normalizedCourseId);
551
- courseIdRef.current = normalizedCourseId;
552
- const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
553
- lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
554
- const observabilityRef = useRef(normalizedConfig.observability);
555
- observabilityRef.current = normalizedConfig.observability;
556
- const onLxpackBridgeMiss = useCallback((event) => {
557
- observabilityRef.current?.onLxpackBridgeMiss?.(event);
558
- }, []);
559
- const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
560
- const pluginHost = useMemo(
561
- () => createReactPluginHost(normalizedConfig.plugins),
562
- [pluginsFingerprint]
563
- );
564
- const pluginHostRef = useRef(pluginHost);
565
- pluginHostRef.current = pluginHost;
566
- const progressRef = useRef(createProgressController());
567
- const courseStartedEmittedToSinkRef = useRef(false);
568
- const courseStartedEmitGenerationRef = useRef(0);
569
- const prevCourseIdForProgressRef = useRef(normalizedCourseId);
570
- const pendingCourseIdResetRef = useRef(false);
571
- const prevUseV2RuntimeRef = useRef(useV2Runtime);
572
- const xapiCourseStartedSentOnClientRef = useRef(false);
573
- const xapiBootstrapSendRef = useRef(false);
574
- if (prevUseV2RuntimeRef.current !== useV2Runtime) {
575
- prevUseV2RuntimeRef.current = useV2Runtime;
576
- if (useV2Runtime) {
577
- headlessRef.current = createLessonkitRuntime({
578
- courseId: normalizedCourseId,
579
- runtimeVersion: "v2",
580
- session: normalizedConfig.session,
581
- plugins: pluginHostRef.current ?? normalizedConfig.plugins,
582
- deferPluginSetup: true
583
- });
584
- progressRef.current = headlessRef.current.progress;
585
- } else {
586
- headlessRef.current = null;
587
- progressRef.current = createProgressController();
588
- }
589
- pendingCourseIdResetRef.current = true;
590
- courseStartedEmittedToSinkRef.current = false;
591
- courseStartedEmitGenerationRef.current += 1;
592
- } else if (useV2Runtime && !headlessRef.current) {
593
- headlessRef.current = createLessonkitRuntime({
594
- courseId: normalizedCourseId,
595
- runtimeVersion: "v2",
596
- session: normalizedConfig.session,
597
- plugins: pluginHostRef.current ?? normalizedConfig.plugins,
598
- deferPluginSetup: true
599
- });
600
- }
601
- if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
602
- prevCourseIdForProgressRef.current = normalizedCourseId;
603
- if (useV2Runtime && headlessRef.current) {
604
- headlessRef.current.resetForCourseChange(normalizedCourseId);
605
- progressRef.current = headlessRef.current.progress;
606
- } else {
607
- progressRef.current = createProgressController();
608
- }
609
- pendingCourseIdResetRef.current = true;
610
- courseStartedEmittedToSinkRef.current = false;
611
- courseStartedEmitGenerationRef.current += 1;
612
- }
613
- if (useV2Runtime && headlessRef.current) {
614
- progressRef.current = headlessRef.current.progress;
615
- }
616
- const [progress, setProgress] = useState(() => progressRef.current.getState());
617
- const syncProgress = useCallback(() => {
618
- setProgress(progressRef.current.getState());
619
- }, []);
620
- const activeLessonIdRef = useRef(progress.activeLessonId);
621
- activeLessonIdRef.current = progress.activeLessonId;
622
- const xapiQueueRef = useRef(createXapiQueueFromObservability(normalizedConfig.observability));
623
- const xapiRef = useRef(null);
624
- const [xapi, setXapi] = useState(null);
625
- const prevXapiCourseIdRef = useRef(normalizedCourseId);
626
- const xapiEnabled = normalizedConfig.xapi?.enabled;
627
- const xapiClient = normalizedConfig.xapi?.client;
628
- const xapiTransport = normalizedConfig.xapi?.transport;
629
- const courseId = normalizedCourseId;
630
- const trackingEnabled = normalizedConfig.tracking?.enabled;
631
- useIsoLayoutEffect(() => {
632
- const courseChanged = prevXapiCourseIdRef.current !== courseId;
633
- if (courseChanged) {
634
- if (normalizedConfig.xapi?.client) {
635
- const g = globalThis;
636
- if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
637
- console.warn(
638
- "[lessonkit] courseId changed while using config.xapi.client; flush the client between courses or use config.xapi.transport so the provider can manage the queue."
639
- );
640
- }
641
- void xapiRef.current?.flush();
642
- }
643
- xapiQueueRef.current = createXapiQueueFromObservability(observabilityRef.current);
644
- prevXapiCourseIdRef.current = courseId;
645
- xapiCourseStartedSentOnClientRef.current = false;
646
- xapiBootstrapSendRef.current = false;
647
- }
648
- const prev = xapiRef.current;
649
- const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
650
- xapiRef.current = next;
651
- setXapi(next);
652
- let bootstrapSent = false;
653
- let bootstrapAlreadyStarted = false;
654
- if (next) {
655
- const sessionId = sessionIdRef.current;
656
- const cid = courseIdRef.current;
657
- const trackingActive = isTrackingActive(normalizedConfig.tracking);
658
- bootstrapAlreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
659
- const clientChanged = !prev || prev !== next;
660
- const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
661
- const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapSendRef.current && (!bootstrapAlreadyStarted || clientChanged);
662
- if (needsBootstrap) {
663
- try {
664
- const event = buildCourseStartedEvent({
665
- pluginHost: pluginHostRef.current,
666
- courseId: cid,
667
- sessionId,
668
- attemptId: attemptIdRef.current,
669
- user: userRef.current,
670
- lxpackBridge: lxpackBridgeModeRef.current
671
- });
672
- if (event !== null) {
673
- const statement = telemetryEventToXAPIStatement3(event);
674
- if (statement) {
675
- next.send(statement);
676
- xapiBootstrapSendRef.current = true;
677
- bootstrapSent = true;
678
- }
679
- }
680
- } catch {
681
- }
682
- }
683
- }
684
- let cancelled = false;
685
- void (async () => {
686
- if (prev) {
687
- try {
688
- await prev.flush();
689
- } catch {
690
- }
691
- }
692
- if (cancelled) return;
693
- try {
694
- await next?.flush();
695
- if (bootstrapSent && !cancelled) {
696
- if (!bootstrapAlreadyStarted) {
697
- markCourseStarted(defaultStorage, sessionIdRef.current, courseIdRef.current);
698
- }
699
- xapiCourseStartedSentOnClientRef.current = true;
700
- }
701
- } catch {
702
- }
703
- })();
704
- return () => {
705
- cancelled = true;
706
- void prev?.flush();
707
- };
708
- }, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
709
- const trackingRef = useRef(createTrackingClient2());
710
- const trackingClientForUnmountRef = useRef(trackingRef.current);
711
- const [tracking, setTracking] = useState(() => trackingRef.current);
712
- const trackingSink = normalizedConfig.tracking?.sink;
713
- const trackingBatchSink = normalizedConfig.tracking?.batchSink;
714
- const batchEnabled = normalizedConfig.tracking?.batch?.enabled;
715
- const batchFlushIntervalMs = normalizedConfig.tracking?.batch?.flushIntervalMs;
716
- const batchMaxBatchSize = normalizedConfig.tracking?.batch?.maxBatchSize;
717
- const buildCurrentPluginCtx = useCallback(
718
- () => buildPluginContext({
719
- courseId: courseIdRef.current,
720
- sessionId: sessionIdRef.current,
721
- attemptId: attemptIdRef.current,
722
- user: userRef.current
723
- }),
724
- []
725
- );
726
- useIsoLayoutEffect(() => {
727
- const prev = trackingRef.current;
728
- const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
729
- const userBatchSink = wrapBatchSink(
730
- normalizedConfig.tracking?.batchSink,
731
- observabilityRef.current
732
- );
733
- assertTrackingSinkConfig(normalizedConfig.tracking);
734
- const sink = pluginHostRef.current && baseSink ? (
735
- /* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
736
- pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
737
- ) : baseSink;
738
- const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
739
- const host = pluginHostRef.current;
740
- const ctx = buildCurrentPluginCtx();
741
- const delivered = host.deliverTelemetryBatch(events, ctx);
742
- const perEventForBatch = [];
743
- const collector = (event) => {
744
- perEventForBatch.push(event);
745
- };
746
- const composedPerEvent = host.composeTrackingSink(collector, buildCurrentPluginCtx) ?? collector;
747
- for (const event of delivered) {
748
- await Promise.resolve(composedPerEvent(event));
749
- }
750
- return userBatchSink(perEventForBatch);
751
- } : userBatchSink;
752
- const next = createTrackingClientFromConfig(
753
- {
754
- tracking: { ...normalizedConfig.tracking, sink, batchSink }
755
- },
756
- observabilityRef.current
757
- );
758
- trackingRef.current = next;
759
- trackingClientForUnmountRef.current = next;
760
- setTracking(next);
761
- const sessionId = sessionIdRef.current;
762
- const cid = courseIdRef.current;
763
- const trackingActive = isTrackingActive(normalizedConfig.tracking);
764
- const courseStartedFullySettled = hasCourseStartedEmittedToTracking(defaultStorage, sessionId, cid) && hasCourseStarted(defaultStorage, sessionId, cid) && hasCourseStartedPipelineDelivered(defaultStorage, sessionId, cid);
765
- if (!trackingActive) {
766
- courseStartedEmittedToSinkRef.current = false;
767
- } else if (courseStartedFullySettled) {
768
- courseStartedEmittedToSinkRef.current = true;
769
- } else if (!courseStartedEmittedToSinkRef.current) {
770
- const generation = ++courseStartedEmitGenerationRef.current;
771
- const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
772
- void (async () => {
773
- if (generation !== courseStartedEmitGenerationRef.current) return;
774
- const result = await emitPendingCourseStarted({
775
- pluginHost: pluginHostRef.current,
776
- tracking: next,
777
- xapi: xapiRef.current,
778
- storage: defaultStorage,
779
- sessionId,
780
- courseId: cid,
781
- attemptId: attemptIdRef.current,
782
- user: userRef.current,
783
- lxpackBridge: lxpackBridgeModeRef.current,
784
- onLxpackBridgeMiss,
785
- extraSinks: extraSinksRef.current,
786
- skipXapi: xapiCourseStartedSentOnClientRef.current,
787
- onXapiStatementSent: () => {
788
- xapiCourseStartedSentOnClientRef.current = true;
789
- },
790
- shouldCommit
791
- });
792
- if (generation !== courseStartedEmitGenerationRef.current) return;
793
- courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
794
- })();
795
- }
796
- return () => {
797
- courseStartedEmitGenerationRef.current += 1;
798
- if (prev !== trackingRef.current) {
799
- void disposeTrackingClient(prev);
800
- }
801
- };
802
- }, [
803
- trackingEnabled,
804
- trackingSink,
805
- trackingBatchSink,
806
- batchEnabled,
807
- batchFlushIntervalMs,
808
- batchMaxBatchSize,
809
- normalizedConfig.plugins,
810
- normalizedCourseId,
811
- buildCurrentPluginCtx
812
- ]);
813
- const emitWithBridge = useCallback((trackingClient, event) => {
814
- emitTelemetryWithPlugins({
815
- pluginHost: pluginHostRef.current,
816
- tracking: trackingClient,
817
- xapi: xapiRef.current,
818
- event,
819
- pluginCtx: buildPluginContext({
820
- courseId: courseIdRef.current,
821
- sessionId: sessionIdRef.current,
822
- attemptId: attemptIdRef.current,
823
- user: userRef.current
824
- }),
825
- lxpackBridge: lxpackBridgeModeRef.current,
826
- onLxpackBridgeMiss,
827
- extraSinks: extraSinksRef.current
828
- });
829
- }, [onLxpackBridgeMiss]);
830
- const emitLifecycleEvent = useCallback(
831
- (name, data, lessonId) => {
832
- const event = tryBuildTelemetryEvent({
833
- name,
834
- courseId: courseIdRef.current,
835
- lessonId: lessonId ?? activeLessonIdRef.current,
836
- sessionId: sessionIdRef.current,
837
- attemptId: attemptIdRef.current,
838
- user: userRef.current,
839
- data
840
- });
841
- if (!event) return;
842
- emitWithBridge(trackingRef.current, event);
843
- },
844
- [emitWithBridge]
845
- );
846
- const track = useCallback(
847
- (name, data, opts) => {
848
- const event = tryBuildTelemetryEvent({
849
- name,
850
- courseId: courseIdRef.current,
851
- lessonId: opts?.lessonId ?? activeLessonIdRef.current,
852
- sessionId: sessionIdRef.current,
853
- attemptId: attemptIdRef.current,
854
- user: userRef.current,
855
- data
856
- });
857
- if (!event) return;
858
- emitWithBridge(trackingRef.current, event);
859
- },
860
- [emitWithBridge]
861
- );
862
- useLayoutEffect(() => {
863
- if (!pendingCourseIdResetRef.current) return;
864
- pendingCourseIdResetRef.current = false;
865
- syncProgress();
866
- if (!isTrackingActive(normalizedConfig.tracking)) return;
867
- const sessionId = sessionIdRef.current;
868
- const cid = courseIdRef.current;
869
- void (async () => {
870
- try {
871
- await trackingRef.current?.flush?.();
872
- } catch {
873
- }
874
- if (!courseStartedEmittedToSinkRef.current) {
875
- const result = await emitPendingCourseStarted({
876
- pluginHost: pluginHostRef.current,
877
- tracking: trackingRef.current,
878
- xapi: xapiRef.current,
879
- storage: defaultStorage,
880
- sessionId,
881
- courseId: cid,
882
- attemptId: attemptIdRef.current,
883
- user: userRef.current,
884
- lxpackBridge: lxpackBridgeModeRef.current,
885
- onLxpackBridgeMiss,
886
- extraSinks: extraSinksRef.current,
887
- skipXapi: xapiCourseStartedSentOnClientRef.current,
888
- onXapiStatementSent: () => {
889
- xapiCourseStartedSentOnClientRef.current = true;
890
- }
891
- });
892
- courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
893
- }
894
- })();
895
- }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, onLxpackBridgeMiss]);
896
- const emitLessonCompleted = useCallback(
897
- (lessonId, durationMs) => {
898
- track("lesson_completed", { lessonId, durationMs }, { lessonId });
899
- if (durationMs !== void 0) {
900
- track("lesson_time_on_task", { lessonId, durationMs }, { lessonId });
901
- }
902
- },
903
- [track]
904
- );
905
- const completeLesson = useCallback(
906
- (lessonId, opts) => {
907
- if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
908
- return;
909
- }
910
- if (useV2Runtime && headlessRef.current) {
911
- headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
912
- syncProgress();
913
- void Promise.resolve(trackingRef.current?.flush?.());
914
- return;
915
- }
916
- const result = progressRef.current.completeLesson(lessonId, Date.now());
917
- if (!result.didComplete) return;
918
- syncProgress();
919
- emitLessonCompleted(lessonId, result.durationMs);
920
- void Promise.resolve(trackingRef.current?.flush?.());
921
- },
922
- [syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
923
- );
924
- useEffect(() => {
925
- return () => {
926
- const client = trackingClientForUnmountRef.current;
927
- const xapi2 = xapiRef.current;
928
- void (async () => {
929
- try {
930
- await xapi2?.flush();
931
- } catch {
932
- }
933
- try {
934
- await client?.flush?.();
935
- } catch {
936
- }
937
- try {
938
- await client?.dispose?.();
939
- } catch {
940
- }
941
- })();
942
- };
943
- }, []);
944
- useEffect(() => {
945
- if (typeof document === "undefined") return;
946
- const flushOnPageExit = () => {
947
- try {
948
- xapiRef.current?.flushOnExit?.();
949
- trackingRef.current?.flushOnExit?.();
950
- } finally {
951
- void xapiRef.current?.flush();
952
- void trackingRef.current?.flush?.();
953
- }
954
- };
955
- const onVisibilityChange = () => {
956
- if (document.visibilityState === "hidden") flushOnPageExit();
957
- };
958
- document.addEventListener("visibilitychange", onVisibilityChange);
959
- window.addEventListener("pagehide", flushOnPageExit);
960
- return () => {
961
- document.removeEventListener("visibilitychange", onVisibilityChange);
962
- window.removeEventListener("pagehide", flushOnPageExit);
963
- };
964
- }, []);
965
- useEffect(() => {
966
- warnMissingProductionObservability(observabilityRef.current, {
967
- trackingEnabled: isTrackingActive(normalizedConfig.tracking),
968
- xapiEnabled: normalizedConfig.xapi?.enabled !== false && Boolean(
969
- normalizedConfig.xapi?.client || normalizedConfig.xapi?.transport || normalizedConfig.xapi?.enabled === true
970
- )
971
- });
972
- }, [
973
- normalizedConfig.tracking,
974
- normalizedConfig.xapi?.enabled,
975
- normalizedConfig.xapi?.client,
976
- normalizedConfig.xapi?.transport,
977
- normalizedConfig.observability
978
- ]);
979
- const setActiveLesson = useCallback(
980
- (lessonId) => {
981
- if (useV2Runtime && headlessRef.current) {
982
- headlessRef.current.setActiveLesson(lessonId, emitLifecycleEvent);
983
- syncProgress();
984
- void Promise.resolve(trackingRef.current?.flush?.());
985
- return;
986
- }
987
- const current = progressRef.current.getState();
988
- if (current.activeLessonId === lessonId) return;
989
- if (current.completedLessonIds.has(lessonId)) {
990
- progressRef.current.setActiveLesson(lessonId, Date.now());
991
- syncProgress();
992
- return;
993
- }
994
- const previous = current.activeLessonId;
995
- if (previous && previous !== lessonId) {
996
- const completed = progressRef.current.completeLesson(previous, Date.now());
997
- if (completed.didComplete) {
998
- emitLessonCompleted(previous, completed.durationMs);
999
- void Promise.resolve(trackingRef.current?.flush?.());
1000
- }
1001
- }
1002
- progressRef.current.setActiveLesson(lessonId, Date.now());
1003
- syncProgress();
1004
- track("lesson_started", { lessonId }, { lessonId });
1005
- },
1006
- [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
1007
- );
1008
- const completeCourse = useCallback(() => {
1009
- if (useV2Runtime && headlessRef.current) {
1010
- headlessRef.current.completeCourse(emitLifecycleEvent);
1011
- syncProgress();
1012
- void trackingRef.current?.flush?.();
1013
- return;
1014
- }
1015
- const current = progressRef.current.getState();
1016
- if (current.activeLessonId) {
1017
- const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
1018
- if (lessonResult.didComplete) {
1019
- emitLessonCompleted(current.activeLessonId, lessonResult.durationMs);
1020
- }
1021
- }
1022
- const result = progressRef.current.completeCourse();
1023
- if (!result.didComplete) return;
1024
- syncProgress();
1025
- track("course_completed");
1026
- void trackingRef.current?.flush?.();
1027
- }, [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]);
1028
- const sessionUser = normalizedConfig.session?.user;
1029
- const sessionUserKey = useMemo(
1030
- () => sessionUser ? JSON.stringify(sessionUser) : "",
1031
- [sessionUser]
1032
- );
1033
- const sessionAttemptId = normalizedConfig.session?.attemptId;
1034
- const sessionConfiguredId = normalizedConfig.session?.sessionId;
1035
- useEffect(() => {
1036
- if (useV2Runtime && headlessRef.current) {
1037
- headlessRef.current.updateConfig({
1038
- courseId: normalizedCourseId,
1039
- session: normalizedConfig.session
1040
- });
1041
- }
1042
- }, [
1043
- useV2Runtime,
1044
- normalizedCourseId,
1045
- sessionAttemptId,
1046
- sessionConfiguredId,
1047
- sessionUserKey,
1048
- normalizedConfig.session
1049
- ]);
1050
- useEffect(() => {
1051
- if (!useV2Runtime || !headlessRef.current) return;
1052
- headlessRef.current.updateConfig({
1053
- plugins: pluginHostRef.current ?? normalizedConfig.plugins
1054
- });
1055
- }, [useV2Runtime, pluginHost]);
1056
- useEffect(() => {
1057
- const host = useV2Runtime ? headlessRef.current?.pluginHost ?? null : pluginHost;
1058
- if (!host) return;
1059
- const ctx = buildPluginContext({
1060
- courseId: courseIdRef.current,
1061
- sessionId: sessionIdRef.current,
1062
- attemptId: attemptIdRef.current,
1063
- user: userRef.current
1064
- });
1065
- host.setupAll(ctx);
1066
- return () => {
1067
- host.disposeAll();
1068
- };
1069
- }, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
1070
- useEffect(() => {
1071
- const nextConfigured = normalizedConfig.session?.sessionId;
1072
- const prevConfigured = prevConfiguredSessionIdRef.current;
1073
- if (nextConfigured === prevConfigured) return;
1074
- prevConfiguredSessionIdRef.current = nextConfigured;
1075
- const cid = courseIdRef.current;
1076
- if (nextConfigured) {
1077
- const fromIds = /* @__PURE__ */ new Set();
1078
- if (prevConfigured) fromIds.add(prevConfigured);
1079
- const tabId = getTabSessionId(defaultStorage);
1080
- if (tabId) fromIds.add(tabId);
1081
- for (const fromId of fromIds) {
1082
- if (fromId !== nextConfigured) {
1083
- migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
1084
- }
1085
- }
1086
- sessionIdRef.current = nextConfigured;
1087
- } else if (prevConfigured) {
1088
- const nextAuto = resolveSessionId(defaultStorage, void 0);
1089
- migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
1090
- sessionIdRef.current = nextAuto;
1091
- }
1092
- }, [sessionConfiguredId, normalizedCourseId]);
1093
- const runtime = useMemo(
1094
- () => ({
1095
- config: normalizedConfig,
1096
- tracking,
1097
- xapi,
1098
- storage: defaultStorage,
1099
- session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
1100
- progress,
1101
- setActiveLesson,
1102
- completeLesson,
1103
- completeCourse,
1104
- track,
1105
- plugins: pluginHost
1106
- }),
1107
- [
1108
- normalizedConfig,
1109
- tracking,
1110
- xapi,
1111
- progress,
1112
- setActiveLesson,
1113
- completeLesson,
1114
- completeCourse,
1115
- track,
1116
- pluginHost,
1117
- sessionUser,
1118
- sessionAttemptId,
1119
- sessionConfiguredId
1120
- ]
1121
- );
1122
- return runtime;
1123
- }
1124
-
1125
- // src/context.tsx
1126
- import { jsx } from "react/jsx-runtime";
1127
- var LessonkitContext = createContext(null);
1128
- function LessonkitProvider(props) {
1129
- const runtime = useLessonkitProviderRuntime(props.config);
1130
- return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
1131
- }
1132
-
1133
- // src/hooks.ts
1134
- import { useContext, useMemo as useMemo3 } from "react";
1135
-
1136
- // src/assessment/useAssessmentState.ts
1137
- import { useMemo as useMemo2 } from "react";
1138
- function useAssessmentState(enclosingLessonId) {
1139
- const { track } = useLessonkit();
1140
- const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
1141
- return useMemo2(
1142
- () => ({
1143
- answer: (data) => {
1144
- track("assessment_answered", data, trackOpts);
1145
- },
1146
- complete: (data) => {
1147
- track("assessment_completed", data, trackOpts);
1148
- }
1149
- }),
1150
- [track, enclosingLessonId]
1151
- );
1152
- }
1153
-
1154
- // src/hooks.ts
1155
- function useLessonkit() {
1156
- const ctx = useContext(LessonkitContext);
1157
- if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
1158
- return ctx;
1159
- }
1160
- function useProgress() {
1161
- const { progress } = useLessonkit();
1162
- return progress;
1163
- }
1164
- function useTracking() {
1165
- const { track } = useLessonkit();
1166
- return useMemo3(() => ({ track }), [track]);
1167
- }
1168
- function useCompletion() {
1169
- const { completeLesson, completeCourse } = useLessonkit();
1170
- return useMemo3(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
1171
- }
1172
- function useQuizState(enclosingLessonId) {
1173
- const { track } = useLessonkit();
1174
- const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
1175
- return useMemo3(
1176
- () => ({
1177
- answer: (opts) => {
1178
- track("quiz_answered", opts, trackOpts);
1179
- },
1180
- complete: (opts) => {
1181
- track("quiz_completed", opts, trackOpts);
1182
- }
1183
- }),
1184
- [track, enclosingLessonId]
1185
- );
1186
- }
1187
-
1188
- // src/lessonContext.tsx
1189
- import { createContext as createContext2, useContext as useContext2 } from "react";
1190
- var LessonContext = createContext2(void 0);
1191
- function useEnclosingLessonId() {
1192
- return useContext2(LessonContext);
1193
- }
1194
-
1195
- // src/runtime/validateComponentId.ts
1196
- import { assertValidId as assertValidId2 } from "@lessonkit/core";
1197
- function isDevEnvironment4() {
1198
- const g = globalThis;
1199
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
1200
- }
1201
- function normalizeComponentId(id, path) {
1202
- if (path === "courseId") return assertValidId2(id, "courseId");
1203
- if (path === "lessonId") return assertValidId2(id, "lessonId");
1204
- if (path === "checkId") return assertValidId2(id, "checkId");
1205
- if (path === "blockId") return assertValidId2(id, "blockId");
1206
- return assertValidId2(id, path);
1207
- }
1208
-
1209
- // src/runtime/lessonMountRegistry.ts
1210
- var mountCounts = /* @__PURE__ */ new Map();
1211
- var warnedConcurrentLessons = false;
1212
- function registerLessonMount(lessonId) {
1213
- if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
1214
- warnedConcurrentLessons = true;
1215
- console.warn(
1216
- "[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
1217
- );
1218
- }
1219
- mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
1220
- return () => {
1221
- const next = (mountCounts.get(lessonId) ?? 1) - 1;
1222
- if (next <= 0) {
1223
- mountCounts.delete(lessonId);
1224
- } else {
1225
- mountCounts.set(lessonId, next);
1226
- }
1227
- };
1228
- }
1229
- function getLessonMountCount(lessonId) {
1230
- return mountCounts.get(lessonId) ?? 0;
1231
- }
1232
-
1233
- // src/components/Quiz.tsx
1234
- import { forwardRef, useEffect as useEffect3, useId, useMemo as useMemo5, useRef as useRef4, useState as useState3 } from "react";
1235
- import { visuallyHiddenStyle } from "@lessonkit/accessibility";
1236
-
1237
- // src/assessment/AssessmentLessonGuard.tsx
1238
- import { useEffect as useEffect2 } from "react";
1239
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1240
- var warnedAssessmentOutsideLesson = false;
1241
- function resetAssessmentWarningsForTests() {
1242
- warnedAssessmentOutsideLesson = false;
1243
- }
1244
- function AssessmentLessonGuard(props) {
1245
- const enclosingLessonId = useEnclosingLessonId();
1246
- const missingLesson = enclosingLessonId === void 0;
1247
- useEffect2(() => {
1248
- if (!missingLesson || isDevEnvironment4()) return;
1249
- if (!warnedAssessmentOutsideLesson) {
1250
- warnedAssessmentOutsideLesson = true;
1251
- console.error(
1252
- `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1253
- );
1254
- }
1255
- }, [missingLesson, props.blockLabel]);
1256
- if (missingLesson && isDevEnvironment4()) {
1257
- throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1258
- }
1259
- if (missingLesson) {
1260
- return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsxs("p", { children: [
1261
- props.blockLabel,
1262
- " must be placed inside a Lesson."
1263
- ] }) });
1264
- }
1265
- return /* @__PURE__ */ jsx2(Fragment, { children: props.children(enclosingLessonId) });
1266
- }
1267
-
1268
- // src/assessment/internal/buildAssessmentHandle.ts
1269
- function buildAssessmentHandle(opts) {
1270
- return {
1271
- getScore: opts.getScore,
1272
- getMaxScore: opts.getMaxScore,
1273
- getAnswerGiven: opts.getAnswerGiven,
1274
- resetTask: opts.resetTask,
1275
- showSolutions: opts.showSolutions,
1276
- getXAPIData: opts.getXAPIData,
1277
- ...opts.getCurrentState ? { getCurrentState: opts.getCurrentState } : {},
1278
- ...opts.resume ? { resume: opts.resume } : {}
1279
- };
1280
- }
1281
-
1282
- // src/assessment/internal/resumeState.ts
1283
- function readBooleanField(state, key) {
1284
- const value = state[key];
1285
- if (value === true || value === false || value === null) return value;
1286
- return void 0;
1287
- }
1288
- function readStringField(state, key) {
1289
- const value = state[key];
1290
- if (typeof value === "string" || value === null) return value;
1291
- return void 0;
1292
- }
1293
- function readNumberField(state, key) {
1294
- const value = state[key];
1295
- if (typeof value === "number" && Number.isFinite(value)) return value;
1296
- if (value === null) return null;
1297
- return void 0;
1298
- }
1299
- function readBooleanStateField(state, key, apply) {
1300
- const value = state[key];
1301
- if (typeof value === "boolean") apply(value);
1302
- }
1303
-
1304
- // src/assessment/internal/useAssessmentHandleRegistration.ts
1305
- import { useImperativeHandle as useImperativeHandle2 } from "react";
1306
-
1307
- // src/compound/CompoundProvider.tsx
1308
- import React5, { createContext as createContext5, useCallback as useCallback2, useContext as useContext5, useImperativeHandle, useMemo as useMemo4, useRef as useRef3, useState as useState2 } from "react";
1309
- import { clampCompoundPageIndex, createCompoundResumeState } from "@lessonkit/core";
1310
-
1311
- // src/compound/aggregateScores.ts
1312
- function aggregateAssessmentScores(handles, opts) {
1313
- let score = 0;
1314
- let maxScore = 0;
1315
- let allAnswered = true;
1316
- for (const entry of handles) {
1317
- const handle = "handle" in entry ? entry.handle : entry;
1318
- const pageIndex = "handle" in entry ? entry.pageIndex : void 0;
1319
- score += handle.getScore();
1320
- maxScore += handle.getMaxScore();
1321
- const countsForAnswerGiven = opts?.answerPageIndex === void 0 || pageIndex === void 0 || pageIndex === opts.answerPageIndex;
1322
- if (countsForAnswerGiven && !handle.getAnswerGiven()) allAnswered = false;
1323
- }
1324
- return { score, maxScore, allAnswered };
1325
- }
1326
-
1327
- // src/compound/CompoundHydrationBridge.tsx
1328
- import { createContext as createContext3, useContext as useContext3, useRef as useRef2 } from "react";
1329
- import { jsx as jsx3 } from "react/jsx-runtime";
1330
- var CompoundHydrationBridgeContext = createContext3(
1331
- null
1332
- );
1333
- function CompoundHydrationBridgeProvider({ children }) {
1334
- const bridgeRef = useRef2(null);
1335
- return /* @__PURE__ */ jsx3(CompoundHydrationBridgeContext.Provider, { value: bridgeRef, children });
1336
- }
1337
- function useCompoundHydrationBridgeRef() {
1338
- return useContext3(CompoundHydrationBridgeContext);
1339
- }
1340
-
1341
- // src/compound/CompoundPageIndexContext.tsx
1342
- import { createContext as createContext4, useContext as useContext4 } from "react";
1343
- import { jsx as jsx4 } from "react/jsx-runtime";
1344
- var CompoundPageIndexContext = createContext4(void 0);
1345
- function CompoundPageIndexProvider({
1346
- pageIndex,
1347
- children
1348
- }) {
1349
- return /* @__PURE__ */ jsx4(CompoundPageIndexContext.Provider, { value: pageIndex, children });
1350
- }
1351
- function useCompoundPageIndex() {
1352
- return useContext4(CompoundPageIndexContext);
1353
- }
1354
-
1355
- // src/compound/CompoundProvider.tsx
1356
- import { jsx as jsx5 } from "react/jsx-runtime";
1357
- var CompoundRegistryContext = createContext5(null);
1358
- var CompoundHandlesVersionContext = createContext5(0);
1359
- function CompoundProvider({
1360
- children,
1361
- activePageIndex: _activePageIndex,
1362
- onActivePageIndexChange: _onActivePageIndexChange
1363
- }) {
1364
- const registryRef = useRef3(/* @__PURE__ */ new Map());
1365
- const [handlesVersion, setHandlesVersion] = useState2(0);
1366
- const register = useCallback2((checkId, handle, pageIndex) => {
1367
- const prev = registryRef.current.get(checkId);
1368
- if (prev && prev.handle !== handle) {
1369
- const message = `[lessonkit] duplicate checkId "${checkId}" registered in the same compound container; the previous handle was replaced.`;
1370
- if (isDevEnvironment4()) {
1371
- console.error(message);
1372
- } else {
1373
- console.warn(message);
1374
- }
1375
- }
1376
- registryRef.current.set(checkId, { handle, pageIndex });
1377
- if (prev?.handle !== handle || prev?.pageIndex !== pageIndex) {
1378
- setHandlesVersion((v) => v + 1);
1379
- }
1380
- return () => {
1381
- const current = registryRef.current.get(checkId);
1382
- if (current?.handle === handle) {
1383
- registryRef.current.delete(checkId);
1384
- setHandlesVersion((v) => v + 1);
1385
- }
1386
- };
1387
- }, []);
1388
- const registryValue = useMemo4(
1389
- () => ({
1390
- register,
1391
- getHandles: () => {
1392
- const handles = /* @__PURE__ */ new Map();
1393
- for (const [checkId, entry] of registryRef.current) {
1394
- handles.set(checkId, entry.handle);
1395
- }
1396
- return handles;
1397
- },
1398
- getRegisteredHandles: () => registryRef.current
1399
- }),
1400
- [register]
1401
- );
1402
- return /* @__PURE__ */ jsx5(CompoundHydrationBridgeProvider, { children: /* @__PURE__ */ jsx5(CompoundRegistryContext.Provider, { value: registryValue, children: /* @__PURE__ */ jsx5(CompoundHandlesVersionContext.Provider, { value: handlesVersion, children }) }) });
1403
- }
1404
- function useCompoundRegistry() {
1405
- const registry = useContext5(CompoundRegistryContext);
1406
- const handlesVersion = useContext5(CompoundHandlesVersionContext);
1407
- if (!registry) return null;
1408
- return { ...registry, handlesVersion };
1409
- }
1410
- function useCompoundHandlesVersion() {
1411
- return useContext5(CompoundHandlesVersionContext);
1412
- }
1413
- function useRegisterAssessmentHandle(checkId, handle) {
1414
- const registry = useContext5(CompoundRegistryContext);
1415
- const pageIndex = useCompoundPageIndex();
1416
- React5.useLayoutEffect(() => {
1417
- if (!registry || !handle) return;
1418
- return registry.register(checkId, handle, pageIndex);
1419
- }, [registry, checkId, handle, pageIndex]);
1420
- }
1421
- function useCompoundHandleRef(ref, opts) {
1422
- const { activePageIndex, setActivePageIndex, getHandles, getRegisteredHandles, pageCount } = opts;
1423
- const bridgeRef = useCompoundHydrationBridgeRef();
1424
- const setIndexClamped = useCallback2(
1425
- (index) => {
1426
- const next = pageCount !== void 0 ? clampCompoundPageIndex(index, pageCount) : Math.max(0, Math.floor(index));
1427
- setActivePageIndex(next);
1428
- },
1429
- [pageCount, setActivePageIndex]
1430
- );
1431
- useImperativeHandle(
1432
- ref,
1433
- () => ({
1434
- getScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).score,
1435
- getMaxScore: () => aggregateAssessmentScores(getRegisteredHandles().values()).maxScore,
1436
- getAnswerGiven: () => aggregateAssessmentScores(getRegisteredHandles().values(), {
1437
- answerPageIndex: activePageIndex
1438
- }).allAnswered,
1439
- resetTask: () => {
1440
- for (const entry of getRegisteredHandles().values()) entry.handle.resetTask();
1441
- },
1442
- showSolutions: () => {
1443
- if (!opts.enableSolutionsButton) return;
1444
- for (const entry of getRegisteredHandles().values()) entry.handle.showSolutions();
1445
- },
1446
- getCurrentState: () => {
1447
- const childStates = {};
1448
- for (const [checkId, entry] of getRegisteredHandles()) {
1449
- if (entry.handle.getCurrentState) {
1450
- childStates[checkId] = entry.handle.getCurrentState();
1451
- }
1452
- }
1453
- return createCompoundResumeState({ activePageIndex, childStates });
1454
- },
1455
- resume: (state) => {
1456
- bridgeRef?.current?.notifyImperativeResume(state);
1457
- }
1458
- }),
1459
- [activePageIndex, setIndexClamped, getHandles, getRegisteredHandles, opts.enableSolutionsButton, bridgeRef]
1460
- );
1461
- }
1462
-
1463
- // src/assessment/internal/useAssessmentHandleRegistration.ts
1464
- function useAssessmentHandleRegistration(checkId, handle, ref) {
1465
- useImperativeHandle2(ref, () => handle, [handle]);
1466
- useRegisterAssessmentHandle(checkId, handle);
1467
- }
1468
-
1469
- // src/assessment/internal/usePluginScoring.ts
1470
- import { useCallback as useCallback3 } from "react";
1471
-
1472
- // src/assessment/scoring.ts
1473
- function resolvePassingThreshold(passingScore, maxScore) {
1474
- return passingScore ?? maxScore;
1475
- }
1476
- function meetsPassingThreshold(score, maxScore, passingScore) {
1477
- const threshold = resolvePassingThreshold(passingScore, maxScore);
1478
- return score >= threshold;
1479
- }
1480
- function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
1481
- const maxScore = custom?.maxScore ?? fallbackMax;
1482
- if (custom?.passed !== void 0) {
1483
- const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
1484
- return { score: score2, maxScore, passed: custom.passed };
1485
- }
1486
- if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1487
- const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1488
- return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
1489
- }
1490
- const score = fallbackCorrect ? maxScore : 0;
1491
- const passed = meetsPassingThreshold(score, maxScore, passingScore);
1492
- return { score, maxScore, passed };
1493
- }
1494
-
1495
- // src/assessment/internal/usePluginScoring.ts
1496
- function usePluginScoring(checkId, lessonId) {
1497
- const { plugins, config, session } = useLessonkit();
1498
- const getPluginScore = useCallback3(
1499
- (response) => {
1500
- const pluginCtx = buildPluginContext({
1501
- courseId: config.courseId,
1502
- sessionId: session.sessionId,
1503
- attemptId: session.attemptId,
1504
- user: session.user
1505
- });
1506
- return plugins?.scoreAssessment({ checkId, lessonId, response }, pluginCtx) ?? null;
1507
- },
1508
- [checkId, config.courseId, lessonId, plugins, session.attemptId, session.sessionId, session.user]
1509
- );
1510
- const scoreResponse = useCallback3(
1511
- (response, defaultCorrect, maxScore = 1, passingScore) => scoreFromCustom(getPluginScore(response), defaultCorrect, maxScore, passingScore),
1512
- [getPluginScore]
1513
- );
1514
- const isChoiceCorrect = useCallback3(
1515
- (choice, answer, custom, passingScore) => {
1516
- if (!custom) return choice === answer;
1517
- if (custom.passed !== void 0) return custom.passed;
1518
- if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1519
- return meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
1520
- }
1521
- return choice === answer;
1522
- },
1523
- []
1524
- );
1525
- return { getPluginScore, scoreResponse, isChoiceCorrect };
1526
- }
1527
-
1528
- // src/components/Quiz.tsx
1529
- import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
1530
- function QuizInner(props, ref) {
1531
- const { enclosingLessonId } = props;
1532
- const checkId = useMemo5(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1533
- const quiz = useQuizState(enclosingLessonId);
1534
- const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
1535
- const [selected, setSelected] = useState3(null);
1536
- const [selectionCorrect, setSelectionCorrect] = useState3(null);
1537
- const [quizPassed, setQuizPassed] = useState3(false);
1538
- const [completedScore, setCompletedScore] = useState3(null);
1539
- const [completedMaxScore, setCompletedMaxScore] = useState3(null);
1540
- const completedRef = useRef4(false);
1541
- const telemetryReplayedRef = useRef4(false);
1542
- const questionId = useId();
1543
- const choicesKey = props.choices.join("\0");
1544
- useEffect3(() => {
1545
- completedRef.current = false;
1546
- telemetryReplayedRef.current = false;
1547
- setQuizPassed(false);
1548
- setSelected(null);
1549
- setSelectionCorrect(null);
1550
- setCompletedScore(null);
1551
- setCompletedMaxScore(null);
1552
- }, [checkId, props.answer, props.question, choicesKey]);
1553
- const passed = quizPassed;
1554
- const resolveScores = () => {
1555
- const maxScore = completedMaxScore ?? 1;
1556
- if (quizPassed) {
1557
- return { score: completedScore ?? maxScore, maxScore };
1558
- }
1559
- if (selected !== null && selectionCorrect) {
1560
- return { score: completedMaxScore ?? maxScore, maxScore };
1561
- }
1562
- return { score: 0, maxScore };
1563
- };
1564
- const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1565
- if (!nextPassed || telemetryReplayedRef.current) return;
1566
- telemetryReplayedRef.current = true;
1567
- if (nextSelected !== null) {
1568
- quiz.answer({
1569
- checkId,
1570
- question: props.question,
1571
- choice: nextSelected,
1572
- correct: nextCorrect ?? false
1573
- });
1574
- }
1575
- quiz.complete({
1576
- checkId,
1577
- score: nextScore,
1578
- maxScore: nextMaxScore,
1579
- passingScore: props.passingScore ?? nextMaxScore
1580
- });
1581
- };
1582
- const handle = useMemo5(
1583
- () => buildAssessmentHandle({
1584
- checkId,
1585
- getScore: () => resolveScores().score,
1586
- getMaxScore: () => resolveScores().maxScore,
1587
- getAnswerGiven: () => selected !== null,
1588
- resetTask: () => {
1589
- completedRef.current = false;
1590
- telemetryReplayedRef.current = false;
1591
- setQuizPassed(false);
1592
- setSelected(null);
1593
- setSelectionCorrect(null);
1594
- setCompletedScore(null);
1595
- setCompletedMaxScore(null);
1596
- },
1597
- showSolutions: () => {
1598
- },
1599
- getXAPIData: () => {
1600
- const { score, maxScore } = resolveScores();
1601
- return {
1602
- checkId,
1603
- interactionType: "mcq",
1604
- response: selected ?? void 0,
1605
- correct: selectionCorrect ?? void 0,
1606
- score,
1607
- maxScore
1608
- };
1609
- },
1610
- getCurrentState: () => ({
1611
- selected,
1612
- selectionCorrect,
1613
- quizPassed,
1614
- completedScore,
1615
- completedMaxScore
1616
- }),
1617
- resume: (state) => {
1618
- const nextSelected = readStringField(state, "selected");
1619
- if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
1620
- const nextCorrect = readBooleanField(state, "selectionCorrect");
1621
- if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1622
- setSelectionCorrect(nextCorrect);
1623
- }
1624
- const nextCompletedScore = readNumberField(state, "completedScore");
1625
- if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1626
- const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1627
- if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1628
- const nextPassed = readBooleanField(state, "quizPassed");
1629
- if (nextPassed === true || nextPassed === false) {
1630
- setQuizPassed(nextPassed);
1631
- completedRef.current = nextPassed;
1632
- if (nextPassed) {
1633
- const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1634
- const score = nextCompletedScore ?? completedScore ?? maxScore;
1635
- replayTelemetry(
1636
- nextSelected ?? null,
1637
- nextCorrect ?? null,
1638
- nextPassed,
1639
- score,
1640
- maxScore
1641
- );
1642
- }
1643
- }
1644
- }
1645
- }),
1646
- [
1647
- checkId,
1648
- completedMaxScore,
1649
- completedScore,
1650
- props.passingScore,
1651
- props.question,
1652
- quiz,
1653
- quizPassed,
1654
- selected,
1655
- selectionCorrect
1656
- ]
1657
- );
1658
- useAssessmentHandleRegistration(checkId, handle, ref);
1659
- return /* @__PURE__ */ jsxs2("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
1660
- /* @__PURE__ */ jsx6("p", { id: questionId, children: props.question }),
1661
- /* @__PURE__ */ jsxs2("fieldset", { "aria-labelledby": questionId, children: [
1662
- /* @__PURE__ */ jsx6("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
1663
- props.choices.map((c, i) => /* @__PURE__ */ jsxs2("label", { style: { display: "block" }, children: [
1664
- /* @__PURE__ */ jsx6(
1665
- "input",
1666
- {
1667
- type: "radio",
1668
- name: questionId,
1669
- value: c,
1670
- checked: selected === c,
1671
- disabled: passed,
1672
- "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
1673
- onChange: () => {
1674
- if (passed) return;
1675
- setSelected(c);
1676
- const custom = getPluginScore(c);
1677
- const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
1678
- setSelectionCorrect(correct);
1679
- quiz.answer({
1680
- checkId,
1681
- question: props.question,
1682
- choice: c,
1683
- correct
1684
- });
1685
- if (correct && !completedRef.current) {
1686
- completedRef.current = true;
1687
- setQuizPassed(true);
1688
- const maxScore = custom?.maxScore ?? 1;
1689
- const score = custom?.score ?? maxScore;
1690
- setCompletedScore(score);
1691
- setCompletedMaxScore(maxScore);
1692
- quiz.complete({
1693
- checkId,
1694
- score,
1695
- maxScore,
1696
- passingScore: props.passingScore ?? maxScore
1697
- });
1698
- }
1699
- }
1700
- }
1701
- ),
1702
- c
1703
- ] }, `${questionId}-${i}`))
1704
- ] }),
1705
- selected && selectionCorrect !== null ? /* @__PURE__ */ jsx6("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
1706
- ] });
1707
- }
1708
- var QuizInnerForwarded = forwardRef(QuizInner);
1709
- var Quiz = forwardRef(function Quiz2(props, ref) {
1710
- return /* @__PURE__ */ jsx6(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx6(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1711
- });
1712
- function KnowledgeCheck(props) {
1713
- return /* @__PURE__ */ jsx6(
1714
- Quiz,
1715
- {
1716
- checkId: props.checkId,
1717
- question: props.question,
1718
- choices: props.choices,
1719
- answer: props.answer,
1720
- passingScore: props.passingScore
1721
- }
1722
- );
1723
- }
1724
- function resetQuizWarningsForTests() {
1725
- resetAssessmentWarningsForTests();
1726
- }
43
+ LessonContext,
44
+ LessonkitProvider,
45
+ assertProductionCourseConfig,
46
+ normalizeComponentId,
47
+ resetAssessmentWarningsForTests,
48
+ shouldEnforceProductionGuard,
49
+ useAssessmentState,
50
+ useCompletion,
51
+ useLessonkit,
52
+ useProgress,
53
+ useQuizState,
54
+ useTracking
55
+ } from "./chunk-TDM3ARE7.js";
1727
56
 
1728
57
  // src/components.tsx
1729
- import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
58
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
59
+ import { visuallyHiddenStyle } from "@lessonkit/accessibility";
60
+ import { jsx, jsxs } from "react/jsx-runtime";
1730
61
  function Course(props) {
1731
- const courseId = useMemo6(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1732
- const providerConfig = useMemo6(
62
+ const courseId = useMemo(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
63
+ const providerConfig = useMemo(
1733
64
  () => ({ ...props.config, courseId }),
1734
65
  [props.config, courseId]
1735
- );
1736
- return /* @__PURE__ */ jsx7(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs3("section", { "aria-label": props.title, children: [
1737
- /* @__PURE__ */ jsx7("h1", { children: props.title }),
1738
- /* @__PURE__ */ jsx7("div", { children: props.children })
1739
- ] }) });
1740
- }
1741
- function Lesson(props) {
1742
- const lessonId = useMemo6(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1743
- const autoComplete = props.autoCompleteOnUnmount !== false;
1744
- const { setActiveLesson, config } = useLessonkit();
1745
- const { completeLesson } = useCompletion();
1746
- const lessonMountGenerationRef = useRef5(0);
1747
- const liveCourseIdRef = useRef5(config.courseId);
1748
- liveCourseIdRef.current = config.courseId;
1749
- useEffect4(() => {
1750
- const unregister = registerLessonMount(lessonId);
1751
- const generation = ++lessonMountGenerationRef.current;
1752
- const mountedCourseId = config.courseId;
1753
- let effectSurvivedTick = false;
1754
- queueMicrotask(() => {
1755
- queueMicrotask(() => {
1756
- effectSurvivedTick = true;
1757
- });
1758
- });
1759
- setActiveLesson(lessonId);
1760
- return () => {
1761
- unregister();
1762
- if (getLessonMountCount(lessonId) > 0) {
1763
- return;
1764
- }
1765
- if (!autoComplete) return;
1766
- queueMicrotask(() => {
1767
- if (!effectSurvivedTick) return;
1768
- if (lessonMountGenerationRef.current !== generation) return;
1769
- if (liveCourseIdRef.current !== mountedCourseId) return;
1770
- completeLesson(lessonId, { courseId: mountedCourseId });
1771
- });
1772
- };
1773
- }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
1774
- return /* @__PURE__ */ jsx7(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs3("article", { "aria-label": props.title, children: [
1775
- /* @__PURE__ */ jsx7("h2", { children: props.title }),
1776
- /* @__PURE__ */ jsx7("div", { children: props.children })
1777
- ] }) });
1778
- }
1779
- function Scenario(props) {
1780
- const blockId = useMemo6(
1781
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1782
- [props.blockId]
1783
- );
1784
- return /* @__PURE__ */ jsx7("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1785
- }
1786
- function Reflection(props) {
1787
- const blockId = useMemo6(
1788
- () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1789
- [props.blockId]
1790
- );
1791
- const promptId = useId2();
1792
- const hintId = useId2();
1793
- const [internalValue, setInternalValue] = useState4("");
1794
- const isControlled = props.value !== void 0;
1795
- const value = isControlled ? props.value : internalValue;
1796
- const handleChange = (event) => {
1797
- if (!isControlled) setInternalValue(event.target.value);
1798
- props.onChange?.(event.target.value);
1799
- };
1800
- return /* @__PURE__ */ jsxs3("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
1801
- props.prompt ? /* @__PURE__ */ jsx7("p", { id: promptId, children: props.prompt }) : null,
1802
- props.hint ? /* @__PURE__ */ jsx7("p", { id: hintId, style: visuallyHiddenStyle2, children: props.hint }) : null,
1803
- props.children,
1804
- /* @__PURE__ */ jsx7(
1805
- "textarea",
1806
- {
1807
- value,
1808
- onChange: handleChange,
1809
- "aria-labelledby": props.prompt ? promptId : void 0,
1810
- "aria-describedby": props.hint ? hintId : void 0,
1811
- "aria-label": props.prompt ? void 0 : "Reflection response"
1812
- }
1813
- )
1814
- ] });
1815
- }
1816
- function ProgressTracker(props) {
1817
- const { progress } = useLessonkit();
1818
- const completed = progress.completedLessonIds.size;
1819
- if (props.totalLessons != null) {
1820
- const total = props.totalLessons;
1821
- const displayed = Math.min(completed, total);
1822
- return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx7(
1823
- "div",
1824
- {
1825
- role: "progressbar",
1826
- "aria-valuemin": 0,
1827
- "aria-valuemax": total,
1828
- "aria-valuenow": displayed,
1829
- "aria-label": "Lessons completed",
1830
- children: /* @__PURE__ */ jsxs3("p", { children: [
1831
- "Lessons completed: ",
1832
- displayed,
1833
- " of ",
1834
- total
1835
- ] })
1836
- }
1837
- ) });
1838
- }
1839
- return /* @__PURE__ */ jsx7("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs3("p", { children: [
1840
- "Lessons completed: ",
1841
- completed
1842
- ] }) });
1843
- }
1844
-
1845
- // src/blocks/TrueFalse.tsx
1846
- import React9, { forwardRef as forwardRef2, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
1847
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1848
- var INTERACTION = "trueFalse";
1849
- function TrueFalseInner(props, ref) {
1850
- const { enclosingLessonId } = props;
1851
- const checkId = useMemo7(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1852
- const assessment = useAssessmentState(enclosingLessonId);
1853
- const { config } = useLessonkit();
1854
- const { scoreResponse } = usePluginScoring(checkId, enclosingLessonId);
1855
- const [selected, setSelected] = useState5(null);
1856
- const [selectionCorrect, setSelectionCorrect] = useState5(null);
1857
- const [showSolutions, setShowSolutions] = useState5(false);
1858
- const [passed, setPassed] = useState5(false);
1859
- const [completedScore, setCompletedScore] = useState5(null);
1860
- const [completedMaxScore, setCompletedMaxScore] = useState5(null);
1861
- const completedRef = useRef6(false);
1862
- const telemetryReplayedRef = useRef6(false);
1863
- const questionId = React9.useId();
1864
- const reset = () => {
1865
- completedRef.current = false;
1866
- telemetryReplayedRef.current = false;
1867
- setPassed(false);
1868
- setSelected(null);
1869
- setSelectionCorrect(null);
1870
- setShowSolutions(false);
1871
- setCompletedScore(null);
1872
- setCompletedMaxScore(null);
1873
- };
1874
- useEffect5(() => {
1875
- reset();
1876
- }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1877
- const resolveScores = () => {
1878
- const maxScore = completedMaxScore ?? 1;
1879
- if (passed) {
1880
- return { score: completedScore ?? maxScore, maxScore };
1881
- }
1882
- if (selectionCorrect) {
1883
- return { score: completedMaxScore ?? maxScore, maxScore };
1884
- }
1885
- return { score: 0, maxScore };
1886
- };
1887
- const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
1888
- if (!nextPassed || telemetryReplayedRef.current) return;
1889
- telemetryReplayedRef.current = true;
1890
- if (nextSelected !== null) {
1891
- assessment.answer({
1892
- checkId,
1893
- interactionType: INTERACTION,
1894
- question: props.question,
1895
- response: nextSelected,
1896
- correct: nextCorrect ?? false
1897
- });
1898
- }
1899
- assessment.complete({
1900
- checkId,
1901
- interactionType: INTERACTION,
1902
- score: nextScore,
1903
- maxScore: nextMaxScore,
1904
- passingScore: props.passingScore ?? nextMaxScore
1905
- });
1906
- };
1907
- const handle = useMemo7(
1908
- () => buildAssessmentHandle({
1909
- checkId,
1910
- getScore: () => resolveScores().score,
1911
- getMaxScore: () => resolveScores().maxScore,
1912
- getAnswerGiven: () => selected !== null,
1913
- resetTask: reset,
1914
- showSolutions: () => setShowSolutions(true),
1915
- getXAPIData: () => {
1916
- const { score, maxScore } = resolveScores();
1917
- return {
1918
- checkId,
1919
- interactionType: INTERACTION,
1920
- response: selected ?? void 0,
1921
- correct: selectionCorrect ?? void 0,
1922
- score,
1923
- maxScore
1924
- };
1925
- },
1926
- getCurrentState: () => ({
1927
- selected,
1928
- selectionCorrect,
1929
- passed,
1930
- showSolutions,
1931
- completedScore,
1932
- completedMaxScore
1933
- }),
1934
- resume: (state) => {
1935
- const nextSelected = readBooleanField(state, "selected");
1936
- if (nextSelected === true || nextSelected === false || nextSelected === null) {
1937
- setSelected(nextSelected);
1938
- }
1939
- const nextCorrect = readBooleanField(state, "selectionCorrect");
1940
- if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
1941
- setSelectionCorrect(nextCorrect);
1942
- }
1943
- const nextCompletedScore = readNumberField(state, "completedScore");
1944
- if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
1945
- const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
1946
- if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
1947
- const nextPassed = readBooleanField(state, "passed");
1948
- if (nextPassed === true || nextPassed === false) {
1949
- setPassed(nextPassed);
1950
- completedRef.current = nextPassed;
1951
- if (nextPassed) {
1952
- const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
1953
- const score = nextCompletedScore ?? completedScore ?? maxScore;
1954
- replayTelemetry(nextSelected ?? null, nextCorrect ?? null, nextPassed, score, maxScore);
1955
- }
1956
- }
1957
- readBooleanStateField(state, "showSolutions", setShowSolutions);
1958
- }
1959
- }),
1960
- [
1961
- assessment,
1962
- checkId,
1963
- completedMaxScore,
1964
- completedScore,
1965
- passed,
1966
- props.passingScore,
1967
- props.question,
1968
- selected,
1969
- selectionCorrect,
1970
- showSolutions
1971
- ]
1972
- );
1973
- useAssessmentHandleRegistration(checkId, handle, ref);
1974
- const submit = (value) => {
1975
- if (passed && !props.enableRetry) return;
1976
- setSelected(value);
1977
- const correct = value === props.answer;
1978
- const scored = scoreResponse(value, correct, 1, props.passingScore);
1979
- setSelectionCorrect(scored.passed);
1980
- assessment.answer({
1981
- checkId,
1982
- interactionType: INTERACTION,
1983
- question: props.question,
1984
- response: value,
1985
- correct: scored.passed
1986
- });
1987
- if (scored.passed && !completedRef.current) {
1988
- completedRef.current = true;
1989
- setPassed(true);
1990
- setCompletedScore(scored.score);
1991
- setCompletedMaxScore(scored.maxScore);
1992
- assessment.complete({
1993
- checkId,
1994
- interactionType: INTERACTION,
1995
- score: scored.score,
1996
- maxScore: scored.maxScore,
1997
- passingScore: props.passingScore ?? scored.maxScore
1998
- });
1999
- }
2000
- };
2001
- const reveal = showSolutions || passed && props.enableSolutionsButton;
2002
- return /* @__PURE__ */ jsxs4("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
2003
- /* @__PURE__ */ jsx8("p", { id: questionId, children: props.question }),
2004
- /* @__PURE__ */ jsxs4("fieldset", { "aria-labelledby": questionId, children: [
2005
- /* @__PURE__ */ jsx8("legend", { className: "lk-visually-hidden", children: "True or False" }),
2006
- /* @__PURE__ */ jsxs4("label", { style: { display: "block", marginRight: "1rem" }, children: [
2007
- /* @__PURE__ */ jsx8(
2008
- "input",
2009
- {
2010
- type: "radio",
2011
- name: `${questionId}-tf`,
2012
- checked: selected === true,
2013
- disabled: passed && !props.enableRetry,
2014
- onChange: () => submit(true)
2015
- }
2016
- ),
2017
- "True"
2018
- ] }),
2019
- /* @__PURE__ */ jsxs4("label", { style: { display: "block" }, children: [
2020
- /* @__PURE__ */ jsx8(
2021
- "input",
2022
- {
2023
- type: "radio",
2024
- name: `${questionId}-tf`,
2025
- checked: selected === false,
2026
- disabled: passed && !props.enableRetry,
2027
- onChange: () => submit(false)
2028
- }
2029
- ),
2030
- "False"
2031
- ] })
2032
- ] }),
2033
- reveal ? /* @__PURE__ */ jsxs4("p", { children: [
2034
- "Correct answer: ",
2035
- /* @__PURE__ */ jsx8("strong", { children: props.answer ? "True" : "False" })
2036
- ] }) : null,
2037
- selected !== null && selectionCorrect !== null ? /* @__PURE__ */ jsx8("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
2038
- props.enableRetry && passed ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2039
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2040
- ] });
2041
- }
2042
- var TrueFalseInnerForwarded = forwardRef2(TrueFalseInner);
2043
- var TrueFalse = forwardRef2(function TrueFalse2(props, ref) {
2044
- return /* @__PURE__ */ jsx8(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx8(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2045
- });
2046
-
2047
- // src/blocks/MarkTheWords.tsx
2048
- import React10, { forwardRef as forwardRef3, useEffect as useEffect6, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
2049
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
2050
- var INTERACTION2 = "markTheWords";
2051
- function tokenize(text) {
2052
- return text.split(/(\s+)/).filter((t) => t.length > 0);
2053
- }
2054
- function MarkTheWordsInner(props, ref) {
2055
- const checkId = useMemo8(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2056
- const assessment = useAssessmentState(props.enclosingLessonId);
2057
- const tokens = useMemo8(() => tokenize(props.text), [props.text]);
2058
- const correctSet = useMemo8(
2059
- () => new Set(props.correctWords.map((w) => w.toLowerCase())),
2060
- [props.correctWords]
2061
- );
2062
- const [marked, setMarked] = useState6(() => /* @__PURE__ */ new Set());
2063
- const [passed, setPassed] = useState6(false);
2064
- const [showSolutions, setShowSolutions] = useState6(false);
2065
- const completedRef = useRef7(false);
2066
- const reset = () => {
2067
- completedRef.current = false;
2068
- setPassed(false);
2069
- setMarked(/* @__PURE__ */ new Set());
2070
- setShowSolutions(false);
2071
- };
2072
- useEffect6(() => {
2073
- reset();
2074
- }, [checkId, props.text, props.correctWords.join("\0")]);
2075
- const selectableIndices = useMemo8(() => {
2076
- const indices = [];
2077
- tokens.forEach((t, i) => {
2078
- if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
2079
- });
2080
- return indices;
2081
- }, [tokens, correctSet]);
2082
- const hasTargets = selectableIndices.length > 0;
2083
- const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
2084
- const maxScore = selectableIndices.length;
2085
- const score = allMarked ? maxScore : marked.size;
2086
- const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2087
- const handle = useMemo8(
2088
- () => buildAssessmentHandle({
2089
- checkId,
2090
- getScore: () => score,
2091
- getMaxScore: () => maxScore || 1,
2092
- getAnswerGiven: () => marked.size > 0,
2093
- resetTask: reset,
2094
- showSolutions: () => setShowSolutions(true),
2095
- getXAPIData: () => ({
2096
- checkId,
2097
- interactionType: INTERACTION2,
2098
- response: [...marked].map((i) => tokens[i]),
2099
- correct: passedThreshold,
2100
- score,
2101
- maxScore: maxScore || 1
2102
- }),
2103
- getCurrentState: () => ({ marked: [...marked], passed, showSolutions }),
2104
- resume: (state) => {
2105
- const raw = state.marked;
2106
- if (Array.isArray(raw)) setMarked(new Set(raw.filter((i) => typeof i === "number")));
2107
- readBooleanStateField(state, "passed", (value) => {
2108
- setPassed(value);
2109
- completedRef.current = value;
2110
- });
2111
- readBooleanStateField(state, "showSolutions", setShowSolutions);
2112
- }
2113
- }),
2114
- [checkId, marked, maxScore, passed, passedThreshold, score, showSolutions, tokens]
2115
- );
2116
- useAssessmentHandleRegistration(checkId, handle, ref);
2117
- const toggle = (index) => {
2118
- if (passed && !props.enableRetry) return;
2119
- setMarked((prev) => {
2120
- const next = new Set(prev);
2121
- if (next.has(index)) next.delete(index);
2122
- else next.add(index);
2123
- return next;
2124
- });
2125
- };
2126
- useEffect6(() => {
2127
- if (!hasTargets) {
2128
- if (isDevEnvironment4()) {
2129
- console.warn(
2130
- "[lessonkit] MarkTheWords: no tokens match correctWords",
2131
- props.correctWords
2132
- );
2133
- }
2134
- return;
2135
- }
2136
- if (!passedThreshold || completedRef.current) return;
2137
- completedRef.current = true;
2138
- setPassed(true);
2139
- assessment.answer({
2140
- checkId,
2141
- interactionType: INTERACTION2,
2142
- question: props.text,
2143
- response: [...marked].map((i) => tokens[i]),
2144
- correct: passedThreshold
2145
- });
2146
- assessment.complete({
2147
- checkId,
2148
- interactionType: INTERACTION2,
2149
- score,
2150
- maxScore,
2151
- passingScore: props.passingScore ?? maxScore
2152
- });
2153
- }, [
2154
- assessment,
2155
- checkId,
2156
- hasTargets,
2157
- marked,
2158
- maxScore,
2159
- passedThreshold,
2160
- props.passingScore,
2161
- props.correctWords,
2162
- props.text,
2163
- score,
2164
- tokens
2165
- ]);
2166
- return /* @__PURE__ */ jsxs5("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
2167
- !hasTargets ? /* @__PURE__ */ jsxs5("p", { role: "alert", children: [
2168
- "No words in this sentence match ",
2169
- /* @__PURE__ */ jsx9("code", { children: "correctWords" }),
2170
- ". Check spelling and capitalization in the source text."
2171
- ] }) : null,
2172
- /* @__PURE__ */ jsx9("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
2173
- /* @__PURE__ */ jsx9("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
2174
- const isWord = !/^\s+$/.test(token);
2175
- const isTarget = isWord && correctSet.has(token.toLowerCase());
2176
- if (!isTarget) return /* @__PURE__ */ jsx9(React10.Fragment, { children: token }, i);
2177
- const selected = marked.has(i);
2178
- const solution = showSolutions || passed && props.enableSolutionsButton;
2179
- return /* @__PURE__ */ jsx9(
2180
- "button",
2181
- {
2182
- type: "button",
2183
- "data-testid": `mark-word-${i}`,
2184
- "aria-pressed": selected,
2185
- disabled: passed && !props.enableRetry,
2186
- onClick: () => toggle(i),
2187
- style: {
2188
- margin: "0 0.1em",
2189
- textDecoration: solution ? "underline" : void 0,
2190
- fontWeight: selected || solution ? "bold" : void 0
2191
- },
2192
- children: token
2193
- },
2194
- i
2195
- );
2196
- }) }),
2197
- allMarked ? /* @__PURE__ */ jsx9("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
2198
- props.enableRetry && passed ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2199
- props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2200
- ] });
2201
- }
2202
- var MarkTheWordsInnerForwarded = forwardRef3(MarkTheWordsInner);
2203
- var MarkTheWords = forwardRef3(function MarkTheWords2(props, ref) {
2204
- return /* @__PURE__ */ jsx9(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx9(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2205
- });
2206
-
2207
- // src/blocks/FillInTheBlanks.tsx
2208
- import React11, { forwardRef as forwardRef4, useEffect as useEffect7, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
2209
-
2210
- // src/assessment/internal/parseStarDelimitedTemplate.ts
2211
- function parseStarDelimitedTemplate(template, idPrefix) {
2212
- const parts = [];
2213
- const values = [];
2214
- const re = /\*([^*]+)\*/g;
2215
- let last = 0;
2216
- let match;
2217
- let n = 0;
2218
- while ((match = re.exec(template)) !== null) {
2219
- parts.push(template.slice(last, match.index));
2220
- values.push(match[1].trim());
2221
- parts.push(`${idPrefix}-${n++}`);
2222
- last = match.index + match[0].length;
2223
- }
2224
- parts.push(template.slice(last));
2225
- return { parts, values };
2226
- }
2227
-
2228
- // src/blocks/FillInTheBlanks.tsx
2229
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2230
- var INTERACTION3 = "fillInBlanks";
2231
- function parseTemplate(template) {
2232
- const { parts, values } = parseStarDelimitedTemplate(template, "blank");
2233
- return {
2234
- parts,
2235
- blanks: values.map((answer, i) => ({ id: `blank-${i}`, answer }))
2236
- };
2237
- }
2238
- function FillInTheBlanksInner(props, ref) {
2239
- const checkId = useMemo9(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2240
- const assessment = useAssessmentState(props.enclosingLessonId);
2241
- const parsed = useMemo9(() => parseTemplate(props.template), [props.template]);
2242
- const blanks = props.blanks ?? parsed.blanks;
2243
- const [values, setValues] = useState7(
2244
- () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
2245
- );
2246
- const [passed, setPassed] = useState7(false);
2247
- const [showSolutions, setShowSolutions] = useState7(false);
2248
- const [submitted, setSubmitted] = useState7(false);
2249
- const completedRef = useRef8(false);
2250
- const answeredRef = useRef8(false);
2251
- const checkSnapshotRef = useRef8(null);
2252
- const telemetryReplayedRef = useRef8(false);
2253
- const reset = () => {
2254
- completedRef.current = false;
2255
- answeredRef.current = false;
2256
- checkSnapshotRef.current = null;
2257
- telemetryReplayedRef.current = false;
2258
- setPassed(false);
2259
- setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
2260
- setShowSolutions(false);
2261
- setSubmitted(false);
2262
- };
2263
- useEffect7(() => {
2264
- reset();
2265
- }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
2266
- const hasBlanks = blanks.length > 0;
2267
- const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
2268
- let score = 0;
2269
- blanks.forEach((b) => {
2270
- if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
2271
- });
2272
- const maxScore = blanks.length;
2273
- const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2274
- const replayTelemetry = (nextValues, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2275
- if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2276
- telemetryReplayedRef.current = true;
2277
- const nextPassedThreshold = meetsPassingThreshold(
2278
- nextScore,
2279
- nextMaxScore || 1,
2280
- props.passingScore
2281
- );
2282
- assessment.answer({
2283
- checkId,
2284
- interactionType: INTERACTION3,
2285
- question: props.template,
2286
- response: nextValues,
2287
- correct: nextPassedThreshold
2288
- });
2289
- if (nextPassed || nextPassedThreshold) {
2290
- assessment.complete({
2291
- checkId,
2292
- interactionType: INTERACTION3,
2293
- score: nextScore,
2294
- maxScore: nextMaxScore,
2295
- passingScore: props.passingScore ?? nextMaxScore
2296
- });
2297
- }
2298
- };
2299
- const handle = useMemo9(
2300
- () => buildAssessmentHandle({
2301
- checkId,
2302
- getScore: () => score,
2303
- getMaxScore: () => maxScore || 1,
2304
- getAnswerGiven: () => allFilled,
2305
- resetTask: reset,
2306
- showSolutions: () => setShowSolutions(true),
2307
- getXAPIData: () => ({
2308
- checkId,
2309
- interactionType: INTERACTION3,
2310
- response: values,
2311
- correct: passedThreshold,
2312
- score,
2313
- maxScore: maxScore || 1
2314
- }),
2315
- getCurrentState: () => ({ values, passed, showSolutions, submitted }),
2316
- resume: (state) => {
2317
- const raw = state.values;
2318
- let nextValues = values;
2319
- if (raw && typeof raw === "object") {
2320
- nextValues = { ...raw };
2321
- setValues(nextValues);
2322
- }
2323
- let nextPassed = passed;
2324
- let nextSubmitted = submitted;
2325
- readBooleanStateField(state, "passed", (value) => {
2326
- nextPassed = value;
2327
- setPassed(value);
2328
- completedRef.current = value;
2329
- answeredRef.current = value;
2330
- });
2331
- readBooleanStateField(state, "showSolutions", setShowSolutions);
2332
- readBooleanStateField(state, "submitted", (value) => {
2333
- nextSubmitted = value;
2334
- setSubmitted(value);
2335
- if (value) answeredRef.current = true;
2336
- });
2337
- let nextScore = 0;
2338
- blanks.forEach((b) => {
2339
- if ((nextValues[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) nextScore += 1;
2340
- });
2341
- replayTelemetry(nextValues, nextPassed, nextSubmitted, nextScore, blanks.length);
2342
- }
2343
- }),
2344
- [allFilled, assessment, blanks, checkId, maxScore, passed, passedThreshold, props.passingScore, props.template, score, showSolutions, submitted, values]
2345
- );
2346
- useAssessmentHandleRegistration(checkId, handle, ref);
2347
- const check = () => {
2348
- if (!hasBlanks) {
2349
- if (isDevEnvironment4()) {
2350
- console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
2351
- }
2352
- return;
2353
- }
2354
- if (!allFilled) return;
2355
- if (passed) return;
2356
- const snapshot = JSON.stringify(values);
2357
- if (checkSnapshotRef.current === snapshot) return;
2358
- checkSnapshotRef.current = snapshot;
2359
- answeredRef.current = true;
2360
- setSubmitted(true);
2361
- assessment.answer({
2362
- checkId,
2363
- interactionType: INTERACTION3,
2364
- question: props.template,
2365
- response: values,
2366
- correct: passedThreshold
2367
- });
2368
- if (passedThreshold && !completedRef.current) {
2369
- completedRef.current = true;
2370
- setPassed(true);
2371
- assessment.complete({
2372
- checkId,
2373
- interactionType: INTERACTION3,
2374
- score,
2375
- maxScore,
2376
- passingScore: props.passingScore ?? maxScore
2377
- });
2378
- }
2379
- };
2380
- useEffect7(() => {
2381
- if (!allFilled) {
2382
- answeredRef.current = false;
2383
- checkSnapshotRef.current = null;
2384
- setSubmitted(false);
2385
- }
2386
- }, [allFilled]);
2387
- useEffect7(() => {
2388
- if (props.autoCheck && allFilled && !passed) check();
2389
- }, [allFilled, props.autoCheck, values, passedThreshold, passed]);
2390
- const reveal = showSolutions || passed && props.enableSolutionsButton;
2391
- return /* @__PURE__ */ jsxs6("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
2392
- /* @__PURE__ */ jsx10("p", { children: parsed.parts.map((part, i) => {
2393
- const blank = blanks.find((b) => b.id === part);
2394
- if (!blank) return /* @__PURE__ */ jsx10(React11.Fragment, { children: part }, i);
2395
- return /* @__PURE__ */ jsxs6("label", { style: { margin: "0 0.25em" }, children: [
2396
- /* @__PURE__ */ jsx10("span", { className: "lk-visually-hidden", children: blank.answer }),
2397
- /* @__PURE__ */ jsx10(
2398
- "input",
2399
- {
2400
- type: "text",
2401
- "data-testid": `blank-${blank.id}`,
2402
- "aria-label": `Blank ${blank.id}`,
2403
- value: reveal ? blank.answer : values[blank.id] ?? "",
2404
- readOnly: reveal,
2405
- disabled: passed && !props.enableRetry,
2406
- onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
2407
- onBlur: () => props.autoCheck && check(),
2408
- size: Math.max(8, blank.answer.length + 2)
2409
- }
2410
- )
2411
- ] }, blank.id);
2412
- }) }),
2413
- !props.autoCheck ? /* @__PURE__ */ jsx10("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
2414
- !hasBlanks ? /* @__PURE__ */ jsx10("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
2415
- submitted ? /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
2416
- props.enableRetry && passed ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: reset, children: "Try again" }) : null,
2417
- props.enableSolutionsButton && !reveal ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
2418
- ] });
2419
- }
2420
- var FillInTheBlanksInnerForwarded = forwardRef4(FillInTheBlanksInner);
2421
- var FillInTheBlanks = forwardRef4(
2422
- function FillInTheBlanks2(props, ref) {
2423
- return /* @__PURE__ */ jsx10(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx10(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2424
- }
2425
- );
2426
-
2427
- // src/blocks/DragTheWords.tsx
2428
- import React12, { forwardRef as forwardRef5, useEffect as useEffect8, useMemo as useMemo10, useRef as useRef9, useState as useState8 } from "react";
2429
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2430
- var INTERACTION4 = "dragTheWords";
2431
- function parseZones(template) {
2432
- const { parts, values } = parseStarDelimitedTemplate(template, "zone");
2433
- return { parts, answers: values };
2434
- }
2435
- function DragTheWordsInner(props, ref) {
2436
- const checkId = useMemo10(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2437
- const assessment = useAssessmentState(props.enclosingLessonId);
2438
- const { parts, answers } = useMemo10(() => parseZones(props.template), [props.template]);
2439
- const [zones, setZones] = useState8(
2440
- () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
2441
- );
2442
- const [pool, setPool] = useState8(() => [...props.words]);
2443
- const [keyboardWord, setKeyboardWord] = useState8(null);
2444
- const [passed, setPassed] = useState8(false);
2445
- const [submitted, setSubmitted] = useState8(false);
2446
- const completedRef = useRef9(false);
2447
- const answeredRef = useRef9(false);
2448
- const checkSnapshotRef = useRef9(null);
2449
- const telemetryReplayedRef = useRef9(false);
2450
- const reset = () => {
2451
- completedRef.current = false;
2452
- answeredRef.current = false;
2453
- checkSnapshotRef.current = null;
2454
- telemetryReplayedRef.current = false;
2455
- setPassed(false);
2456
- setSubmitted(false);
2457
- setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
2458
- setPool([...props.words]);
2459
- setKeyboardWord(null);
2460
- };
2461
- useEffect8(() => {
2462
- reset();
2463
- }, [checkId, props.template, props.words.join("\0")]);
2464
- const hasZones = answers.length > 0;
2465
- const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
2466
- let score = 0;
2467
- answers.forEach((ans, i) => {
2468
- if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
2469
- });
2470
- const maxScore = answers.length;
2471
- const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
2472
- const replayTelemetry = (nextZones, nextPassed, nextSubmitted, nextScore, nextMaxScore) => {
2473
- if (telemetryReplayedRef.current || !nextSubmitted && !nextPassed) return;
2474
- telemetryReplayedRef.current = true;
2475
- const nextPassedThreshold = meetsPassingThreshold(
2476
- nextScore,
2477
- nextMaxScore || 1,
2478
- props.passingScore
2479
- );
2480
- assessment.answer({
2481
- checkId,
2482
- interactionType: INTERACTION4,
2483
- question: props.template,
2484
- response: nextZones,
2485
- correct: nextPassedThreshold
2486
- });
2487
- if (nextPassed || nextPassedThreshold) {
2488
- assessment.complete({
2489
- checkId,
2490
- interactionType: INTERACTION4,
2491
- score: nextScore,
2492
- maxScore: nextMaxScore,
2493
- passingScore: props.passingScore ?? nextMaxScore
2494
- });
2495
- }
2496
- };
2497
- const handle = useMemo10(
2498
- () => buildAssessmentHandle({
2499
- checkId,
2500
- getScore: () => score,
2501
- getMaxScore: () => maxScore || 1,
2502
- getAnswerGiven: () => allFilled,
2503
- resetTask: reset,
2504
- showSolutions: () => {
2505
- },
2506
- getXAPIData: () => ({
2507
- checkId,
2508
- interactionType: INTERACTION4,
2509
- response: zones,
2510
- correct: passedThreshold,
2511
- score,
2512
- maxScore: maxScore || 1
2513
- }),
2514
- getCurrentState: () => ({ zones, pool, passed, keyboardWord, submitted }),
2515
- resume: (state) => {
2516
- const rawZones = state.zones;
2517
- let nextZones = zones;
2518
- if (rawZones && typeof rawZones === "object") {
2519
- nextZones = { ...rawZones };
2520
- setZones(nextZones);
2521
- }
2522
- if (Array.isArray(state.pool)) setPool([...state.pool]);
2523
- let nextPassed = passed;
2524
- let nextSubmitted = submitted;
2525
- readBooleanStateField(state, "passed", (value) => {
2526
- nextPassed = value;
2527
- setPassed(value);
2528
- completedRef.current = value;
2529
- answeredRef.current = value;
2530
- });
2531
- readBooleanStateField(state, "submitted", (value) => {
2532
- nextSubmitted = value;
2533
- setSubmitted(value);
2534
- if (value) answeredRef.current = true;
2535
- });
2536
- const kw = state.keyboardWord;
2537
- if (kw === null || typeof kw === "string") setKeyboardWord(kw ?? null);
2538
- let nextScore = 0;
2539
- answers.forEach((ans, i) => {
2540
- if ((nextZones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) nextScore += 1;
2541
- });
2542
- replayTelemetry(nextZones, nextPassed, nextSubmitted, nextScore, answers.length);
2543
- }
2544
- }),
2545
- [allFilled, answers, assessment, checkId, keyboardWord, maxScore, passed, passedThreshold, pool, props.passingScore, props.template, score, submitted, zones]
2546
- );
2547
- useAssessmentHandleRegistration(checkId, handle, ref);
2548
- const placeInZone = (zoneId, word) => {
2549
- if (passed && !props.enableRetry) return;
2550
- const prev = zones[zoneId];
2551
- setZones((z) => ({ ...z, [zoneId]: word }));
2552
- setPool((p) => {
2553
- const next = p.filter((w) => w !== word);
2554
- if (prev) next.push(prev);
2555
- return next;
2556
- });
2557
- setKeyboardWord(null);
2558
- };
2559
- const onDragStart = (word) => (e) => {
2560
- e.dataTransfer.setData("text/plain", word);
2561
- };
2562
- const onDrop = (zoneId) => (e) => {
2563
- e.preventDefault();
2564
- const word = e.dataTransfer.getData("text/plain");
2565
- if (word) placeInZone(zoneId, word);
2566
- };
2567
- const check = () => {
2568
- if (!hasZones) {
2569
- if (isDevEnvironment4()) {
2570
- console.warn("[lessonkit] DragTheWords has no drop zones in template");
2571
- }
2572
- return;
2573
- }
2574
- if (!allFilled) return;
2575
- if (passed) return;
2576
- const snapshot = JSON.stringify(zones);
2577
- if (checkSnapshotRef.current === snapshot) return;
2578
- checkSnapshotRef.current = snapshot;
2579
- answeredRef.current = true;
2580
- setSubmitted(true);
2581
- assessment.answer({
2582
- checkId,
2583
- interactionType: INTERACTION4,
2584
- question: props.template,
2585
- response: zones,
2586
- correct: passedThreshold
2587
- });
2588
- if (passedThreshold && !completedRef.current) {
2589
- completedRef.current = true;
2590
- setPassed(true);
2591
- assessment.complete({
2592
- checkId,
2593
- interactionType: INTERACTION4,
2594
- score,
2595
- maxScore,
2596
- passingScore: props.passingScore ?? maxScore
2597
- });
2598
- }
2599
- };
2600
- useEffect8(() => {
2601
- if (!allFilled) {
2602
- answeredRef.current = false;
2603
- checkSnapshotRef.current = null;
2604
- setSubmitted(false);
2605
- }
2606
- }, [allFilled]);
2607
- useEffect8(() => {
2608
- if (props.autoCheck && allFilled && !passed) check();
2609
- }, [allFilled, props.autoCheck, zones, passedThreshold, passed]);
2610
- return /* @__PURE__ */ jsxs7("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
2611
- /* @__PURE__ */ jsx11("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
2612
- /* @__PURE__ */ jsx11("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ jsx11(
2613
- "button",
2614
- {
2615
- type: "button",
2616
- draggable: true,
2617
- "data-testid": `word-${word}`,
2618
- "aria-pressed": keyboardWord === word,
2619
- onDragStart: onDragStart(word),
2620
- onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
2621
- style: { margin: "0.25rem" },
2622
- children: word
2623
- },
2624
- word
2625
- )) }),
2626
- /* @__PURE__ */ jsx11("p", { children: parts.map((part, i) => {
2627
- if (!part.startsWith("zone-")) return /* @__PURE__ */ jsx11(React12.Fragment, { children: part }, i);
2628
- return /* @__PURE__ */ jsx11(
2629
- "span",
2630
- {
2631
- role: "button",
2632
- tabIndex: 0,
2633
- "data-testid": part,
2634
- onDragOver: (e) => e.preventDefault(),
2635
- onDrop: onDrop(part),
2636
- onClick: () => keyboardWord && placeInZone(part, keyboardWord),
2637
- onKeyDown: (e) => {
2638
- if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
2639
- },
2640
- style: {
2641
- display: "inline-block",
2642
- minWidth: "6em",
2643
- border: "1px dashed currentColor",
2644
- padding: "0.2em 0.5em",
2645
- margin: "0 0.2em"
2646
- },
2647
- children: zones[part] || "___"
2648
- },
2649
- part
2650
- );
2651
- }) }),
2652
- /* @__PURE__ */ jsx11("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2653
- !hasZones ? /* @__PURE__ */ jsx11("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
2654
- submitted ? /* @__PURE__ */ jsx11("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
2655
- ] });
2656
- }
2657
- var DragTheWordsInnerForwarded = forwardRef5(DragTheWordsInner);
2658
- var DragTheWords = forwardRef5(function DragTheWords2(props, ref) {
2659
- return /* @__PURE__ */ jsx11(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx11(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2660
- });
2661
-
2662
- // src/blocks/DragAndDrop.tsx
2663
- import { forwardRef as forwardRef6, useEffect as useEffect9, useMemo as useMemo11, useRef as useRef10, useState as useState9 } from "react";
2664
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2665
- var INTERACTION5 = "dragAndDrop";
2666
- function DragAndDropInner(props, ref) {
2667
- const checkId = useMemo11(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2668
- const assessment = useAssessmentState(props.enclosingLessonId);
2669
- const [assignments, setAssignments] = useState9(
2670
- () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2671
- );
2672
- const [pool, setPool] = useState9(() => props.items.map((i) => i.id));
2673
- const [keyboardItem, setKeyboardItem] = useState9(null);
2674
- const [passed, setPassed] = useState9(false);
2675
- const [checked, setChecked] = useState9(false);
2676
- const completedRef = useRef10(false);
2677
- const reset = () => {
2678
- completedRef.current = false;
2679
- setPassed(false);
2680
- setChecked(false);
2681
- setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2682
- setPool(props.items.map((i) => i.id));
2683
- setKeyboardItem(null);
2684
- };
2685
- useEffect9(() => {
2686
- reset();
2687
- }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2688
- const hasTargets = props.targets.length > 0;
2689
- const allFilled = hasTargets && props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2690
- let score = 0;
2691
- props.targets.forEach((t) => {
2692
- if (assignments[t.id] === t.accepts) score += 1;
2693
- });
2694
- const maxScore = props.targets.length || 1;
2695
- const passedThreshold = meetsPassingThreshold(score, maxScore, props.passingScore);
2696
- const handle = useMemo11(() => {
2697
- return buildAssessmentHandle({
2698
- checkId,
2699
- getScore: () => score,
2700
- getMaxScore: () => maxScore,
2701
- getAnswerGiven: () => hasTargets && allFilled,
2702
- resetTask: reset,
2703
- showSolutions: () => {
2704
- },
2705
- getXAPIData: () => ({
2706
- checkId,
2707
- interactionType: INTERACTION5,
2708
- response: assignments,
2709
- correct: passedThreshold,
2710
- score,
2711
- maxScore
2712
- }),
2713
- getCurrentState: () => ({ assignments, pool, passed, checked, keyboardItem }),
2714
- resume: (state) => {
2715
- const rawAssignments = state.assignments;
2716
- if (rawAssignments && typeof rawAssignments === "object") {
2717
- setAssignments({ ...rawAssignments });
2718
- }
2719
- if (Array.isArray(state.pool)) setPool([...state.pool]);
2720
- readBooleanStateField(state, "passed", (value) => {
2721
- setPassed(value);
2722
- completedRef.current = value;
2723
- });
2724
- readBooleanStateField(state, "checked", setChecked);
2725
- const item = state.keyboardItem;
2726
- if (item === null || typeof item === "string") setKeyboardItem(item ?? null);
2727
- }
2728
- });
2729
- }, [allFilled, assignments, checkId, checked, hasTargets, keyboardItem, maxScore, passed, passedThreshold, pool, props.targets, score]);
2730
- useAssessmentHandleRegistration(checkId, handle, ref);
2731
- const place = (targetId, itemId) => {
2732
- if (passed && !props.enableRetry) return;
2733
- setChecked(false);
2734
- const prev = assignments[targetId];
2735
- setAssignments((a) => ({ ...a, [targetId]: itemId }));
2736
- setPool((p) => {
2737
- const next = p.filter((id) => id !== itemId);
2738
- if (prev) next.push(prev);
2739
- return next;
2740
- });
2741
- setKeyboardItem(null);
2742
- };
2743
- const check = () => {
2744
- if (!allFilled) return;
2745
- setChecked(true);
2746
- assessment.answer({
2747
- checkId,
2748
- interactionType: INTERACTION5,
2749
- response: assignments,
2750
- correct: passedThreshold
2751
- });
2752
- if (passedThreshold && !completedRef.current) {
2753
- completedRef.current = true;
2754
- setPassed(true);
2755
- assessment.complete({
2756
- checkId,
2757
- interactionType: INTERACTION5,
2758
- score,
2759
- maxScore,
2760
- passingScore: props.passingScore ?? maxScore
2761
- });
2762
- }
2763
- };
2764
- return /* @__PURE__ */ jsxs8("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2765
- /* @__PURE__ */ jsx12("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2766
- /* @__PURE__ */ jsx12("div", { role: "list", "aria-label": "Draggable items", children: pool.flatMap((id) => {
2767
- const item = props.items.find((i) => i.id === id);
2768
- if (!item) return [];
2769
- return /* @__PURE__ */ jsx12(
2770
- "button",
2771
- {
2772
- type: "button",
2773
- draggable: true,
2774
- "data-testid": `drag-item-${id}`,
2775
- "aria-pressed": keyboardItem === id,
2776
- onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2777
- onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2778
- style: { margin: "0.25rem" },
2779
- children: item.label
2780
- },
2781
- id
2782
- );
2783
- }) }),
2784
- /* @__PURE__ */ jsx12("ul", { children: props.targets.map((target) => {
2785
- const assigned = assignments[target.id];
2786
- const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2787
- return /* @__PURE__ */ jsxs8("li", { children: [
2788
- /* @__PURE__ */ jsx12("strong", { children: target.label }),
2789
- " ",
2790
- /* @__PURE__ */ jsx12(
2791
- "span",
2792
- {
2793
- role: "button",
2794
- tabIndex: 0,
2795
- "data-testid": `drop-${target.id}`,
2796
- onDragOver: (e) => e.preventDefault(),
2797
- onDrop: (e) => {
2798
- e.preventDefault();
2799
- const id = e.dataTransfer.getData("text/plain");
2800
- if (id) place(target.id, id);
2801
- },
2802
- onClick: () => keyboardItem && place(target.id, keyboardItem),
2803
- onKeyDown: (e) => {
2804
- if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2805
- },
2806
- style: {
2807
- display: "inline-block",
2808
- minWidth: "8em",
2809
- border: "1px dashed currentColor",
2810
- padding: "0.25em"
2811
- },
2812
- children: label
2813
- }
2814
- )
2815
- ] }, target.id);
2816
- }) }),
2817
- /* @__PURE__ */ jsx12("button", { type: "button", "data-testid": "check-drag-drop", disabled: !hasTargets || !allFilled || passed, onClick: check, children: "Check" }),
2818
- checked ? /* @__PURE__ */ jsx12("p", { role: "status", "aria-live": "polite", children: passedThreshold ? "Correct" : "Try again" }) : null
2819
- ] });
2820
- }
2821
- var DragAndDropInnerForwarded = forwardRef6(DragAndDropInner);
2822
- var DragAndDrop = forwardRef6(function DragAndDrop2(props, ref) {
2823
- return /* @__PURE__ */ jsx12(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx12(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2824
- });
2825
-
2826
- // src/blocks/AssessmentSequence.tsx
2827
- import React16, { forwardRef as forwardRef7, useCallback as useCallback7, useEffect as useEffect12, useId as useId3, useMemo as useMemo13, useRef as useRef13, useState as useState10 } from "react";
2828
- import { deriveId } from "@lessonkit/core";
2829
-
2830
- // src/compound/useCompoundShell.ts
2831
- import { useMemo as useMemo12 } from "react";
2832
- import { clampCompoundPageIndex as clampCompoundPageIndex3 } from "@lessonkit/core";
2833
-
2834
- // src/compound/useCompoundNavigation.ts
2835
- import { useCallback as useCallback4 } from "react";
2836
- function useCompoundNavigation(pageCount, index, setIndex) {
2837
- const goNext = useCallback4(() => {
2838
- if (pageCount < 1) return;
2839
- setIndex((i) => Math.min(i + 1, pageCount - 1));
2840
- }, [pageCount, setIndex]);
2841
- const goPrev = useCallback4(() => {
2842
- setIndex((i) => Math.max(i - 1, 0));
2843
- }, [setIndex]);
2844
- const clampedIndex = pageCount < 1 ? 0 : Math.min(index, pageCount - 1);
2845
- return {
2846
- index: clampedIndex,
2847
- setIndex,
2848
- goNext,
2849
- goPrev,
2850
- progress: { current: pageCount < 1 ? 0 : clampedIndex + 1, total: pageCount }
2851
- };
2852
- }
2853
-
2854
- // src/compound/useCompoundPersistence.ts
2855
- import { useCallback as useCallback6, useContext as useContext7, useEffect as useEffect11, useRef as useRef12 } from "react";
2856
- import {
2857
- clampCompoundPageIndex as clampCompoundPageIndex2,
2858
- createCompoundResumeState as createCompoundResumeState2,
2859
- createSessionStoragePort as createSessionStoragePort3,
2860
- loadCompoundState as loadCompoundState2
2861
- } from "@lessonkit/core";
2862
-
2863
- // src/compound/resumeChildHandles.ts
2864
- function filterRegisteredChildStates(handles, childStates) {
2865
- const filtered = {};
2866
- for (const [key, value] of Object.entries(childStates)) {
2867
- if (handles.has(key)) {
2868
- filtered[key] = value;
2869
- }
2870
- }
2871
- return filtered;
2872
- }
2873
- function resumeChildHandles(handles, childStates, opts) {
2874
- const pendingKeys = Object.keys(childStates);
2875
- const alreadyResumed = opts?.alreadyResumed;
2876
- if (opts?.waitForHandles && pendingKeys.length > 0) {
2877
- if (handles.size === 0) return false;
2878
- const registeredPending = pendingKeys.filter((k) => handles.has(k));
2879
- if (registeredPending.length === 0) {
2880
- return false;
2881
- }
2882
- if (registeredPending.length < pendingKeys.length) {
2883
- for (const key of registeredPending) {
2884
- if (alreadyResumed?.has(key)) continue;
2885
- const handle = handles.get(key);
2886
- const child = childStates[key];
2887
- if (handle?.resume && child) {
2888
- handle.resume(child);
2889
- alreadyResumed?.add(key);
2890
- }
2891
- }
2892
- return false;
2893
- }
2894
- }
2895
- for (const [checkId, handle] of handles) {
2896
- if (alreadyResumed?.has(checkId)) continue;
2897
- const child = childStates[checkId];
2898
- if (child && handle.resume) {
2899
- handle.resume(child);
2900
- alreadyResumed?.add(checkId);
2901
- }
2902
- }
2903
- return true;
2904
- }
2905
-
2906
- // src/compound/useCompoundResume.ts
2907
- import { useCallback as useCallback5, useContext as useContext6, useEffect as useEffect10, useRef as useRef11 } from "react";
2908
- import { loadCompoundState, saveCompoundState } from "@lessonkit/core";
2909
- import { createSessionStoragePort as createSessionStoragePort2 } from "@lessonkit/core";
2910
- var warnedCompoundPersistFailure = false;
2911
- function warnCompoundPersistFailure() {
2912
- if (warnedCompoundPersistFailure || !isDevEnvironment4()) return;
2913
- warnedCompoundPersistFailure = true;
2914
- console.warn(
2915
- "[lessonkit] compound resume state could not be saved to sessionStorage (quota or privacy mode); progress may be lost on reload."
2916
- );
2917
- }
2918
- function useCompoundResume(opts) {
2919
- const lessonkitCtx = useContext6(LessonkitContext);
2920
- const storageRef = useRef11(opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2());
2921
- const resumedRef = useRef11(false);
2922
- const resumeKeyRef = useRef11("");
2923
- const prevEnabledRef = useRef11(opts.enabled);
2924
- useEffect10(() => {
2925
- storageRef.current = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort2();
2926
- }, [opts.storage, lessonkitCtx?.storage]);
2927
- useEffect10(() => {
2928
- if (!prevEnabledRef.current && opts.enabled) {
2929
- resumedRef.current = false;
2930
- }
2931
- prevEnabledRef.current = opts.enabled;
2932
- const key = `${opts.courseId ?? ""}:${opts.compoundId}`;
2933
- if (resumeKeyRef.current !== key) {
2934
- resumeKeyRef.current = key;
2935
- resumedRef.current = false;
2936
- }
2937
- if (!opts.enabled || !opts.courseId || resumedRef.current) return;
2938
- const saved = loadCompoundState(storageRef.current, opts.courseId, opts.compoundId);
2939
- if (saved) {
2940
- resumedRef.current = true;
2941
- opts.onResume?.(saved);
2942
- }
2943
- }, [opts.enabled, opts.courseId, opts.compoundId, opts.onResume]);
2944
- return useCallback5(
2945
- (state) => {
2946
- if (!opts.enabled || !opts.courseId) return;
2947
- const persisted = saveCompoundState(storageRef.current, opts.courseId, opts.compoundId, state);
2948
- if (!persisted) warnCompoundPersistFailure();
2949
- },
2950
- [opts.enabled, opts.courseId, opts.compoundId]
2951
- );
2952
- }
2953
-
2954
- // src/compound/useCompoundPersistence.ts
2955
- function readCompoundInitialIndex(courseId, compoundId, pageCount, enabled, storage = createSessionStoragePort3()) {
2956
- if (!enabled || !courseId || pageCount < 1) return 0;
2957
- const saved = loadCompoundState2(storage, courseId, compoundId);
2958
- if (!saved) return 0;
2959
- return clampCompoundPageIndex2(saved.activePageIndex, pageCount);
2960
- }
2961
- function stripOrphanChildStates(handles, childStates) {
2962
- return filterRegisteredChildStates(handles, childStates);
2963
- }
2964
- function useCompoundPersistence(opts) {
2965
- const lessonkitCtx = useContext7(LessonkitContext);
2966
- const storage = opts.storage ?? lessonkitCtx?.storage ?? createSessionStoragePort3();
2967
- const ctx = useCompoundRegistry();
2968
- const handlesVersion = useCompoundHandlesVersion();
2969
- const bridgeRef = useCompoundHydrationBridgeRef();
2970
- const pendingChildResumeRef = useRef12(null);
2971
- const resumedChildKeysRef = useRef12(/* @__PURE__ */ new Set());
2972
- const loadedChildStatesRef = useRef12({});
2973
- const skipSaveUntilHydratedRef = useRef12(false);
2974
- const hydrationKeyRef = useRef12("");
2975
- const hydrationInitRef = useRef12(false);
2976
- const hydrationKey = `${opts.courseId ?? ""}:${opts.compoundId}`;
2977
- if (hydrationKeyRef.current !== hydrationKey) {
2978
- hydrationKeyRef.current = hydrationKey;
2979
- hydrationInitRef.current = false;
2980
- loadedChildStatesRef.current = {};
2981
- skipSaveUntilHydratedRef.current = false;
2982
- pendingChildResumeRef.current = null;
2983
- resumedChildKeysRef.current = /* @__PURE__ */ new Set();
2984
- }
2985
- if (!hydrationInitRef.current && opts.enabled && opts.courseId) {
2986
- hydrationInitRef.current = true;
2987
- const saved = loadCompoundState2(storage, opts.courseId, opts.compoundId);
2988
- if (saved && Object.keys(saved.childStates).length > 0) {
2989
- loadedChildStatesRef.current = { ...saved.childStates };
2990
- skipSaveUntilHydratedRef.current = true;
2991
- pendingChildResumeRef.current = saved;
2992
- }
2993
- }
2994
- const buildState = useCallback6(() => {
2995
- const childStates = {
2996
- ...loadedChildStatesRef.current
2997
- };
2998
- if (ctx) {
2999
- for (const [checkId, entry] of ctx.getRegisteredHandles()) {
3000
- const handle = entry.handle;
3001
- if (handle.getCurrentState) {
3002
- childStates[checkId] = handle.getCurrentState();
3003
- delete loadedChildStatesRef.current[checkId];
3004
- }
3005
- }
3006
- }
3007
- return createCompoundResumeState2({
3008
- activePageIndex: clampCompoundPageIndex2(opts.index, opts.pageCount),
3009
- childStates
3010
- });
3011
- }, [ctx, opts.index, opts.pageCount]);
3012
- const buildStateRef = useRef12(buildState);
3013
- buildStateRef.current = buildState;
3014
- const persistNowRef = useRef12(() => {
3015
- });
3016
- const finalizeHydration = useCallback6(
3017
- (childStates) => {
3018
- loadedChildStatesRef.current = {
3019
- ...loadedChildStatesRef.current,
3020
- ...childStates
3021
- };
3022
- skipSaveUntilHydratedRef.current = false;
3023
- pendingChildResumeRef.current = null;
3024
- queueMicrotask(() => persistNowRef.current());
3025
- },
3026
- []
3027
- );
3028
- const applyPendingChildResume = useCallback6(() => {
3029
- const pending = pendingChildResumeRef.current;
3030
- if (!pending || !ctx) return;
3031
- const handles = ctx.getHandles();
3032
- const applied = resumeChildHandles(handles, pending.childStates, {
3033
- waitForHandles: true,
3034
- alreadyResumed: resumedChildKeysRef.current
3035
- });
3036
- if (!applied) {
3037
- if (handles.size === 0) {
3038
- const registeredOnly2 = stripOrphanChildStates(handles, pending.childStates);
3039
- resumeChildHandles(handles, registeredOnly2, {
3040
- alreadyResumed: resumedChildKeysRef.current
3041
- });
3042
- finalizeHydration(registeredOnly2);
3043
- return;
3044
- }
3045
- const handlesAtWait = handles.size;
3046
- queueMicrotask(() => {
3047
- if (pendingChildResumeRef.current !== pending) return;
3048
- const handlesNow = ctx.getHandles();
3049
- if (handlesNow.size !== handlesAtWait) return;
3050
- const registeredOnly2 = stripOrphanChildStates(handlesNow, pending.childStates);
3051
- resumeChildHandles(handlesNow, registeredOnly2, {
3052
- alreadyResumed: resumedChildKeysRef.current
3053
- });
3054
- finalizeHydration(registeredOnly2);
3055
- });
3056
- return;
3057
- }
3058
- const registeredOnly = stripOrphanChildStates(handles, pending.childStates);
3059
- finalizeHydration(registeredOnly);
3060
- }, [ctx, finalizeHydration]);
3061
- const saveResume = useCompoundResume({
3062
- courseId: opts.courseId,
3063
- compoundId: opts.compoundId,
3064
- enabled: opts.enabled,
3065
- storage,
3066
- onResume: (state) => {
3067
- const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
3068
- loadedChildStatesRef.current = { ...state.childStates };
3069
- skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
3070
- opts.setIndex(clamped);
3071
- resumedChildKeysRef.current = /* @__PURE__ */ new Set();
3072
- pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
3073
- queueMicrotask(() => applyPendingChildResume());
3074
- }
3075
- });
3076
- const persistNow = useCallback6(() => {
3077
- if (!opts.enabled || !opts.courseId) return;
3078
- if (skipSaveUntilHydratedRef.current) return;
3079
- saveResume(buildStateRef.current());
3080
- }, [opts.enabled, opts.courseId, saveResume]);
3081
- useEffect11(() => {
3082
- persistNowRef.current = persistNow;
3083
- }, [persistNow]);
3084
- const notifyImperativeResume = useCallback6(
3085
- (state) => {
3086
- const clamped = clampCompoundPageIndex2(state.activePageIndex, opts.pageCount);
3087
- loadedChildStatesRef.current = { ...state.childStates };
3088
- skipSaveUntilHydratedRef.current = Object.keys(state.childStates).length > 0;
3089
- opts.setIndex(clamped);
3090
- resumedChildKeysRef.current = /* @__PURE__ */ new Set();
3091
- pendingChildResumeRef.current = { ...state, activePageIndex: clamped, childStates: state.childStates };
3092
- queueMicrotask(() => applyPendingChildResume());
3093
- },
3094
- [opts.pageCount, opts.setIndex, applyPendingChildResume]
3095
- );
3096
- useEffect11(() => {
3097
- if (!bridgeRef) return;
3098
- bridgeRef.current = { notifyImperativeResume };
3099
- return () => {
3100
- if (bridgeRef.current?.notifyImperativeResume === notifyImperativeResume) {
3101
- bridgeRef.current = null;
3102
- }
3103
- };
3104
- }, [bridgeRef, notifyImperativeResume]);
3105
- useEffect11(() => {
3106
- applyPendingChildResume();
3107
- }, [opts.index, handlesVersion, applyPendingChildResume]);
3108
- useEffect11(() => {
3109
- persistNow();
3110
- }, [persistNow, opts.index, opts.pageCount, handlesVersion]);
3111
- useEffect11(() => {
3112
- if (!opts.enabled || !opts.courseId || typeof document === "undefined") return;
3113
- const flushOnExit = () => {
3114
- if (document.visibilityState === "hidden") persistNow();
3115
- };
3116
- document.addEventListener("visibilitychange", flushOnExit);
3117
- window.addEventListener("pagehide", flushOnExit);
3118
- return () => {
3119
- document.removeEventListener("visibilitychange", flushOnExit);
3120
- window.removeEventListener("pagehide", flushOnExit);
3121
- };
3122
- }, [opts.enabled, opts.courseId, persistNow]);
3123
- }
3124
-
3125
- // src/compound/useCompoundShell.ts
3126
- function useCompoundShell(opts) {
3127
- const ctx = useCompoundRegistry();
3128
- useCompoundPersistence({
3129
- courseId: opts.courseId,
3130
- compoundId: opts.compoundId,
3131
- pageCount: opts.pageCount,
3132
- index: opts.index,
3133
- setIndex: opts.setIndex,
3134
- enabled: opts.persistEnabled,
3135
- storage: opts.storage
3136
- });
3137
- const { goNext, goPrev, progress } = useCompoundNavigation(opts.pageCount, opts.index, opts.setIndex);
3138
- const visibleIndex = clampCompoundPageIndex3(opts.index, opts.pageCount);
3139
- useCompoundHandleRef(opts.ref, {
3140
- activePageIndex: visibleIndex,
3141
- setActivePageIndex: opts.setIndex,
3142
- getHandles: () => ctx?.getHandles() ?? /* @__PURE__ */ new Map(),
3143
- getRegisteredHandles: () => ctx?.getRegisteredHandles() ?? /* @__PURE__ */ new Map(),
3144
- pageCount: opts.pageCount,
3145
- enableSolutionsButton: opts.enableSolutionsButton
3146
- });
3147
- return { visibleIndex, goNext, goPrev, progress, ctx };
3148
- }
3149
- function useCompoundInitialIndex(opts) {
3150
- return useMemo12(
3151
- () => readCompoundInitialIndex(
3152
- opts.courseId,
3153
- opts.compoundId,
3154
- opts.pageCount,
3155
- opts.persistEnabled,
3156
- opts.storage
3157
- ),
3158
- [opts.courseId, opts.compoundId, opts.pageCount, opts.persistEnabled, opts.storage]
3159
- );
3160
- }
3161
-
3162
- // src/compound/validateChildren.ts
3163
- import React15 from "react";
3164
- import {
3165
- ACCORDION_FORBIDDEN_CHILD_TYPES,
3166
- COMPOUND_MAX_NESTING_DEPTH,
3167
- isChildTypeAllowed
3168
- } from "@lessonkit/core";
3169
-
3170
- // src/compound/blockType.ts
3171
- var LESSONKIT_BLOCK_TYPE = /* @__PURE__ */ Symbol.for("lessonkit.blockType");
3172
- function setLessonkitBlockType(component, blockType) {
3173
- component[LESSONKIT_BLOCK_TYPE] = blockType;
3174
- if (!component.displayName) {
3175
- component.displayName = blockType;
3176
- }
3177
- return component;
3178
- }
3179
- function getLessonkitBlockType(component) {
3180
- if (!component || typeof component !== "object" && typeof component !== "function") {
3181
- return void 0;
3182
- }
3183
- const typed = component;
3184
- return typed[LESSONKIT_BLOCK_TYPE] ?? typed.displayName;
3185
- }
3186
-
3187
- // src/compound/validateChildren.ts
3188
- var warnedPairs = /* @__PURE__ */ new Set();
3189
- var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
3190
- "Page",
3191
- "InteractiveBook",
3192
- "Slide",
3193
- "SlideDeck",
3194
- "AssessmentSequence"
3195
- ]);
3196
- function warnOrThrow(msg, strict) {
3197
- if (strict) throw new Error(msg);
3198
- if (!warnedPairs.has(msg)) {
3199
- warnedPairs.add(msg);
3200
- console.warn(msg);
3201
- }
3202
- }
3203
- function validateNode(parent, node, depth, strict) {
3204
- React15.Children.forEach(node, (child) => {
3205
- if (!React15.isValidElement(child)) return;
3206
- const blockType = getLessonkitBlockType(child.type);
3207
- if (!blockType) {
3208
- if (child.props && typeof child.props === "object" && "children" in child.props) {
3209
- validateNode(parent, child.props.children, depth, strict);
3210
- }
3211
- return;
3212
- }
3213
- if (!isChildTypeAllowed(parent, blockType)) {
3214
- const key = `${parent}:${blockType}`;
3215
- if (!warnedPairs.has(key)) {
3216
- warnedPairs.add(key);
3217
- const msg = `[lessonkit] Block "${blockType}" is not in the allowlist for "${parent}"`;
3218
- if (strict) throw new Error(msg);
3219
- console.warn(msg);
3220
- }
3221
- }
3222
- if (COMPOUND_CONTAINER_TYPES.has(blockType)) {
3223
- const maxDepth = COMPOUND_MAX_NESTING_DEPTH[parent];
3224
- if (depth >= maxDepth) {
3225
- warnOrThrow(
3226
- `[lessonkit] Block "${blockType}" exceeds max nesting depth (${maxDepth}) for "${parent}"`,
3227
- strict
3228
- );
3229
- }
3230
- const nestedParent = blockType;
3231
- validateNode(nestedParent, child.props.children, depth + 1, strict);
3232
- } else if (blockType === "Accordion") {
3233
- const sections = child.props.sections;
3234
- if (sections) validateAccordionSections(sections, strict);
3235
- } else if (child.props && typeof child.props === "object" && "children" in child.props) {
3236
- validateSubtreeForForbidden(
3237
- child.props.children,
3238
- ACCORDION_FORBIDDEN_CHILD_TYPES,
3239
- strict
3240
- );
3241
- }
3242
- });
3243
- }
3244
- function validateSubtreeForForbidden(node, forbidden, strict) {
3245
- React15.Children.forEach(node, (child) => {
3246
- if (!React15.isValidElement(child)) return;
3247
- const blockType = getLessonkitBlockType(child.type);
3248
- if (blockType && forbidden.includes(blockType)) {
3249
- warnOrThrow(`[lessonkit] Block "${blockType}" must not nest inside Accordion`, strict);
3250
- }
3251
- if (blockType === "Accordion") {
3252
- const sections = child.props.sections;
3253
- if (sections) validateAccordionSections(sections, strict);
3254
- return;
3255
- }
3256
- if (child.props && typeof child.props === "object" && "children" in child.props) {
3257
- validateSubtreeForForbidden(
3258
- child.props.children,
3259
- forbidden,
3260
- strict
3261
- );
3262
- }
3263
- });
3264
- }
3265
- function validateAccordionSections(sections, strict) {
3266
- if (!isDevEnvironment4() && !strict) return;
3267
- for (const section of sections) {
3268
- validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, strict);
3269
- }
3270
- }
3271
- function validateCompoundChildren(parent, children, strict) {
3272
- if (!isDevEnvironment4() && !strict) return;
3273
- validateNode(parent, children, 0, strict);
3274
- }
3275
-
3276
- // src/compound/warnPersistence.ts
3277
- var DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID = "assessment-sequence";
3278
- function warnSharedCompoundStorageKey(opts) {
3279
- if (!opts.persistEnabled || opts.hasExplicitBlockId || !isDevEnvironment4()) return;
3280
- console.warn(
3281
- `[lessonkit] <${opts.componentName}> without blockId shares one sessionStorage key when persistCompoundState is enabled; set a unique blockId per instance.`
3282
- );
3283
- }
3284
-
3285
- // src/blocks/AssessmentSequence.tsx
3286
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
3287
- var AssessmentSequenceInner = forwardRef7(
3288
- function AssessmentSequenceInner2(props, ref) {
3289
- const { compoundId, childArray, index, setIndex, persistEnabled } = props;
3290
- const sequential = props.sequential !== false;
3291
- const { config } = useLessonkit();
3292
- const { visibleIndex, goNext, goPrev, progress } = useCompoundShell({
3293
- courseId: config.courseId,
3294
- compoundId,
3295
- pageCount: childArray.length,
3296
- index,
3297
- setIndex,
3298
- persistEnabled,
3299
- ref,
3300
- enableSolutionsButton: props.enableSolutionsButton
3301
- });
3302
- validateCompoundChildren("AssessmentSequence", props.children);
3303
- if (!sequential) {
3304
- return /* @__PURE__ */ jsx13("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: props.children });
3305
- }
3306
- return /* @__PURE__ */ jsxs9("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
3307
- /* @__PURE__ */ jsxs9("p", { children: [
3308
- "Question ",
3309
- progress.current,
3310
- " of ",
3311
- progress.total
3312
- ] }),
3313
- /* @__PURE__ */ jsx13("div", { "data-testid": "assessment-sequence-step", children: childArray.map((child, i) => /* @__PURE__ */ jsx13("div", { hidden: i !== visibleIndex, children: /* @__PURE__ */ jsx13(CompoundPageIndexProvider, { pageIndex: i, children: child }) }, child.key ?? i)) }),
3314
- /* @__PURE__ */ jsxs9("nav", { "aria-label": "Sequence navigation", children: [
3315
- /* @__PURE__ */ jsx13(
3316
- "button",
3317
- {
3318
- type: "button",
3319
- "data-testid": "sequence-prev",
3320
- disabled: visibleIndex === 0 || childArray.length === 0,
3321
- onClick: goPrev,
3322
- children: "Previous"
3323
- }
3324
- ),
3325
- /* @__PURE__ */ jsx13(
3326
- "button",
3327
- {
3328
- type: "button",
3329
- "data-testid": "sequence-next",
3330
- disabled: visibleIndex >= childArray.length - 1 || childArray.length === 0,
3331
- onClick: goNext,
3332
- children: "Next"
3333
- }
3334
- )
3335
- ] })
3336
- ] });
3337
- }
3338
- );
3339
- var AssessmentSequence = forwardRef7(
3340
- function AssessmentSequence2(props, ref) {
3341
- const reactInstanceId = useId3();
3342
- const autoCompoundIdRef = useRef13(null);
3343
- if (!props.blockId && !autoCompoundIdRef.current) {
3344
- autoCompoundIdRef.current = deriveId(`assessment-sequence-${reactInstanceId}`);
3345
- }
3346
- const compoundId = useMemo13(
3347
- () => props.blockId ? normalizeComponentId(props.blockId, "blockId") : autoCompoundIdRef.current ?? DEFAULT_ASSESSMENT_SEQUENCE_COMPOUND_ID,
3348
- [props.blockId]
3349
- );
3350
- const childArray = React16.Children.toArray(props.children).filter(
3351
- React16.isValidElement
3352
- );
3353
- const { config, storage } = useLessonkit();
3354
- const persistEnabled = config.session?.persistCompoundState !== false;
3355
- useEffect12(() => {
3356
- warnSharedCompoundStorageKey({
3357
- persistEnabled,
3358
- hasExplicitBlockId: Boolean(props.blockId),
3359
- componentName: "AssessmentSequence"
3360
- });
3361
- }, [persistEnabled, props.blockId]);
3362
- const initialIndex = useCompoundInitialIndex({
3363
- courseId: config.courseId,
3364
- compoundId,
3365
- pageCount: childArray.length,
3366
- persistEnabled,
3367
- storage
3368
- });
3369
- const [index, setIndex] = useState10(initialIndex);
3370
- const setIndexStable = useCallback7((i) => setIndex(i), []);
3371
- useEffect12(() => {
3372
- setIndex(initialIndex);
3373
- }, [config.courseId, compoundId, initialIndex]);
3374
- return /* @__PURE__ */ jsx13(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx13(
3375
- AssessmentSequenceInner,
3376
- {
3377
- ...props,
3378
- ref,
3379
- compoundId,
3380
- childArray,
3381
- index,
3382
- setIndex,
3383
- persistEnabled
3384
- }
3385
- ) });
3386
- }
3387
- );
3388
- setLessonkitBlockType(AssessmentSequence, "AssessmentSequence");
3389
-
3390
- // src/blocks/Text.tsx
3391
- import "react";
3392
- import { jsx as jsx14 } from "react/jsx-runtime";
3393
- function Text(props) {
3394
- return /* @__PURE__ */ jsx14("p", { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `text-${props.blockId}` : "text", children: props.children });
3395
- }
3396
- setLessonkitBlockType(Text, "Text");
3397
-
3398
- // src/blocks/Heading.tsx
3399
- import { jsx as jsx15 } from "react/jsx-runtime";
3400
- function Heading(props) {
3401
- const Tag = `h${props.level}`;
3402
- return /* @__PURE__ */ jsx15(Tag, { "data-lk-block-id": props.blockId, "data-testid": props.blockId ? `heading-${props.blockId}` : "heading", children: props.children });
3403
- }
3404
- setLessonkitBlockType(Heading, "Heading");
3405
-
3406
- // src/blocks/Image.tsx
3407
- import { jsx as jsx16 } from "react/jsx-runtime";
3408
- function Image(props) {
3409
- return /* @__PURE__ */ jsx16(
3410
- "img",
3411
- {
3412
- src: props.src,
3413
- alt: props.alt,
3414
- "data-lk-block-id": props.blockId,
3415
- "data-testid": props.blockId ? `image-${props.blockId}` : "image",
3416
- style: { maxWidth: "100%", height: "auto" }
3417
- }
3418
- );
3419
- }
3420
- setLessonkitBlockType(Image, "Image");
3421
-
3422
- // src/blocks/Page.tsx
3423
- import { useEffect as useEffect13 } from "react";
3424
- import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
3425
- function Page(props) {
3426
- validateCompoundChildren("Page", props.children);
3427
- const { track } = useLessonkit();
3428
- const lessonId = useEnclosingLessonId();
3429
- useEffect13(() => {
3430
- if (props.hidden || !lessonId || props.parentType) return;
3431
- track(
3432
- "compound_page_viewed",
3433
- {
3434
- blockId: props.blockId,
3435
- pageIndex: props.pageIndex ?? 0,
3436
- parentType: props.parentType
3437
- },
3438
- { lessonId }
3439
- );
3440
- }, [props.hidden, props.pageIndex, props.parentType, props.blockId, lessonId, track]);
3441
- return /* @__PURE__ */ jsxs10(
3442
- "section",
3443
- {
3444
- "aria-label": props.title ?? "Page",
3445
- "data-lk-block-id": props.blockId,
3446
- "data-testid": `page-${props.blockId}`,
3447
- hidden: props.hidden ? true : void 0,
3448
- children: [
3449
- props.title ? /* @__PURE__ */ jsx17("h3", { children: props.title }) : null,
3450
- /* @__PURE__ */ jsx17(CompoundPageIndexProvider, { pageIndex: props.pageIndex ?? 0, children: /* @__PURE__ */ jsx17("div", { children: props.children }) })
3451
- ]
3452
- }
3453
- );
3454
- }
3455
- setLessonkitBlockType(Page, "Page");
3456
-
3457
- // src/blocks/InteractiveBook.tsx
3458
- import React19, { forwardRef as forwardRef8, useCallback as useCallback8, useEffect as useEffect14, useMemo as useMemo14, useState as useState11 } from "react";
3459
- import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
3460
- var InteractiveBookInner = forwardRef8(
3461
- function InteractiveBookInner2(props, ref) {
3462
- const { blockId, pages, index, setIndex, persistEnabled } = props;
3463
- validateCompoundChildren("InteractiveBook", pages);
3464
- const { config, track } = useLessonkit();
3465
- const lessonId = useEnclosingLessonId();
3466
- const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3467
- courseId: config.courseId,
3468
- compoundId: blockId,
3469
- pageCount: pages.length,
3470
- index,
3471
- setIndex,
3472
- persistEnabled,
3473
- ref
3474
- });
3475
- const pageTitles = useMemo14(
3476
- () => pages.map((page) => page.props.title),
3477
- [pages]
3478
- );
3479
- useEffect14(() => {
3480
- if (!lessonId || pages.length === 0) return;
3481
- track(
3482
- "book_page_viewed",
3483
- {
3484
- blockId,
3485
- pageIndex: visibleIndex,
3486
- pageTitle: pageTitles[visibleIndex]
3487
- },
3488
- { lessonId }
3489
- );
3490
- }, [visibleIndex, blockId, lessonId, pages.length, pageTitles, track]);
3491
- return /* @__PURE__ */ jsxs11("section", { "aria-label": props.title, "data-testid": "interactive-book", "data-lk-block-id": blockId, children: [
3492
- /* @__PURE__ */ jsx18("h3", { children: props.title }),
3493
- /* @__PURE__ */ jsxs11("p", { children: [
3494
- "Page ",
3495
- progress.current,
3496
- " of ",
3497
- progress.total
3498
- ] }),
3499
- props.showBookScore && ctx ? /* @__PURE__ */ jsxs11("p", { "data-testid": "book-score", children: [
3500
- "Score: ",
3501
- Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3502
- " /",
3503
- " ",
3504
- Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3505
- ] }) : null,
3506
- /* @__PURE__ */ jsx18("div", { "data-testid": "interactive-book-page", children: pages.map(
3507
- (page, i) => React19.cloneElement(page, {
3508
- key: page.key ?? page.props.blockId,
3509
- hidden: i !== visibleIndex,
3510
- pageIndex: i,
3511
- parentType: "InteractiveBook"
3512
- })
3513
- ) }),
3514
- /* @__PURE__ */ jsxs11("nav", { "aria-label": "Book navigation", children: [
3515
- /* @__PURE__ */ jsx18(
3516
- "button",
3517
- {
3518
- type: "button",
3519
- "data-testid": "book-prev",
3520
- disabled: visibleIndex === 0 || pages.length === 0,
3521
- onClick: goPrev,
3522
- children: "Previous"
3523
- }
3524
- ),
3525
- /* @__PURE__ */ jsx18(
3526
- "button",
3527
- {
3528
- type: "button",
3529
- "data-testid": "book-next",
3530
- disabled: visibleIndex >= pages.length - 1 || pages.length === 0,
3531
- onClick: goNext,
3532
- children: "Next"
3533
- }
3534
- )
3535
- ] })
3536
- ] });
3537
- }
3538
- );
3539
- var InteractiveBook = forwardRef8(function InteractiveBook2(props, ref) {
3540
- const blockId = useMemo14(
3541
- () => normalizeComponentId(props.blockId, "blockId"),
3542
- [props.blockId]
3543
- );
3544
- const pages = React19.Children.toArray(props.children).filter(
3545
- React19.isValidElement
3546
- );
3547
- const { config, storage } = useLessonkit();
3548
- const persistEnabled = config.session?.persistCompoundState !== false;
3549
- const initialIndex = useCompoundInitialIndex({
3550
- courseId: config.courseId,
3551
- compoundId: blockId,
3552
- pageCount: pages.length,
3553
- persistEnabled,
3554
- storage
3555
- });
3556
- const [index, setIndex] = useState11(initialIndex);
3557
- const setIndexStable = useCallback8((i) => setIndex(i), []);
3558
- useEffect14(() => {
3559
- setIndex(initialIndex);
3560
- }, [config.courseId, blockId, initialIndex]);
3561
- return /* @__PURE__ */ jsx18(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx18(
3562
- InteractiveBookInner,
3563
- {
3564
- ...props,
3565
- ref,
3566
- blockId,
3567
- pages,
3568
- index,
3569
- setIndex,
3570
- persistEnabled
3571
- }
3572
- ) });
3573
- });
3574
- setLessonkitBlockType(InteractiveBook, "InteractiveBook");
3575
-
3576
- // src/blocks/Slide.tsx
3577
- import { useEffect as useEffect15 } from "react";
3578
- import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3579
- function Slide(props) {
3580
- validateCompoundChildren("Slide", props.children);
3581
- const { track } = useLessonkit();
3582
- const lessonId = useEnclosingLessonId();
3583
- useEffect15(() => {
3584
- if (props.hidden || !lessonId || props.parentType) return;
3585
- track(
3586
- "compound_page_viewed",
3587
- {
3588
- blockId: props.blockId,
3589
- pageIndex: props.slideIndex ?? 0,
3590
- parentType: props.parentType
3591
- },
3592
- { lessonId }
3593
- );
3594
- }, [props.hidden, props.slideIndex, props.parentType, props.blockId, lessonId, track]);
3595
- return /* @__PURE__ */ jsxs12(
3596
- "section",
3597
- {
3598
- "aria-label": props.title ?? "Slide",
3599
- "data-lk-block-id": props.blockId,
3600
- "data-testid": `slide-${props.blockId}`,
3601
- hidden: props.hidden ? true : void 0,
3602
- children: [
3603
- props.title ? /* @__PURE__ */ jsx19("h3", { children: props.title }) : null,
3604
- /* @__PURE__ */ jsx19(CompoundPageIndexProvider, { pageIndex: props.slideIndex ?? 0, children: /* @__PURE__ */ jsx19("div", { children: props.children }) })
3605
- ]
3606
- }
3607
- );
3608
- }
3609
- setLessonkitBlockType(Slide, "Slide");
3610
-
3611
- // src/blocks/SlideDeck.tsx
3612
- import React21, { forwardRef as forwardRef9, useCallback as useCallback9, useEffect as useEffect17, useMemo as useMemo15, useRef as useRef14, useState as useState12 } from "react";
3613
-
3614
- // src/compound/useCompoundKeyboardNav.ts
3615
- import { useEffect as useEffect16 } from "react";
3616
- var INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "BUTTON"]);
3617
- function isEditableTarget(target) {
3618
- if (!(target instanceof HTMLElement)) return false;
3619
- if (INTERACTIVE_TAGS.has(target.tagName)) return true;
3620
- if (target.isContentEditable) return true;
3621
- if (target.closest("[role='slider'], [role='listbox'], [data-lk-assessment-interactive]")) {
3622
- return true;
3623
- }
3624
- return false;
66
+ );
67
+ return /* @__PURE__ */ jsx(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
68
+ /* @__PURE__ */ jsx("h1", { children: props.title }),
69
+ /* @__PURE__ */ jsx("div", { children: props.children })
70
+ ] }) });
3625
71
  }
3626
- function useCompoundKeyboardNav(opts) {
3627
- const { containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex } = opts;
3628
- useEffect16(() => {
3629
- const el = containerRef.current;
3630
- if (!el || pageCount === 0) return;
3631
- const onKeyDown = (event) => {
3632
- if (!el.contains(document.activeElement) && document.activeElement !== document.body) {
72
+ function Lesson(props) {
73
+ const lessonId = useMemo(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
74
+ const autoComplete = props.autoCompleteOnUnmount !== false;
75
+ const { setActiveLesson, config } = useLessonkit();
76
+ const { completeLesson } = useCompletion();
77
+ const lessonMountGenerationRef = useRef(0);
78
+ const liveCourseIdRef = useRef(config.courseId);
79
+ liveCourseIdRef.current = config.courseId;
80
+ useEffect(() => {
81
+ const unregister = registerLessonMount(lessonId);
82
+ const generation = ++lessonMountGenerationRef.current;
83
+ const mountedCourseId = config.courseId;
84
+ let effectSurvivedTick = false;
85
+ queueMicrotask(() => {
86
+ queueMicrotask(() => {
87
+ effectSurvivedTick = true;
88
+ });
89
+ });
90
+ setActiveLesson(lessonId);
91
+ return () => {
92
+ unregister();
93
+ if (getLessonMountCount(lessonId) > 0) {
3633
94
  return;
3634
95
  }
3635
- if (isEditableTarget(event.target)) return;
3636
- switch (event.key) {
3637
- case "ArrowRight":
3638
- case "ArrowDown":
3639
- if (visibleIndex < pageCount - 1) {
3640
- event.preventDefault();
3641
- goNext();
3642
- }
3643
- break;
3644
- case "ArrowLeft":
3645
- case "ArrowUp":
3646
- if (visibleIndex > 0) {
3647
- event.preventDefault();
3648
- goPrev();
3649
- }
3650
- break;
3651
- case "Home":
3652
- if (visibleIndex !== 0) {
3653
- event.preventDefault();
3654
- setIndex(0);
3655
- }
3656
- break;
3657
- case "End":
3658
- if (visibleIndex !== pageCount - 1) {
3659
- event.preventDefault();
3660
- setIndex(pageCount - 1);
3661
- }
3662
- break;
3663
- default:
3664
- break;
3665
- }
96
+ if (!autoComplete) return;
97
+ queueMicrotask(() => {
98
+ if (!effectSurvivedTick) return;
99
+ if (lessonMountGenerationRef.current !== generation) return;
100
+ if (liveCourseIdRef.current !== mountedCourseId) return;
101
+ completeLesson(lessonId, { courseId: mountedCourseId });
102
+ });
3666
103
  };
3667
- el.addEventListener("keydown", onKeyDown);
3668
- return () => el.removeEventListener("keydown", onKeyDown);
3669
- }, [containerRef, visibleIndex, pageCount, goNext, goPrev, setIndex]);
104
+ }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
105
+ return /* @__PURE__ */ jsx(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ jsxs("article", { "aria-label": props.title, children: [
106
+ /* @__PURE__ */ jsx("h2", { children: props.title }),
107
+ /* @__PURE__ */ jsx("div", { children: props.children })
108
+ ] }) });
3670
109
  }
3671
-
3672
- // src/blocks/SlideDeck.tsx
3673
- import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3674
- var SlideDeckInner = forwardRef9(function SlideDeckInner2(props, ref) {
3675
- const { blockId, slides, index, setIndex, persistEnabled } = props;
3676
- validateCompoundChildren("SlideDeck", slides);
3677
- const { config, track } = useLessonkit();
3678
- const lessonId = useEnclosingLessonId();
3679
- const containerRef = useRef14(null);
3680
- const { visibleIndex, goNext, goPrev, progress, ctx } = useCompoundShell({
3681
- courseId: config.courseId,
3682
- compoundId: blockId,
3683
- pageCount: slides.length,
3684
- index,
3685
- setIndex,
3686
- persistEnabled,
3687
- ref
3688
- });
3689
- const setIndexStable = useCallback9((i) => setIndex(i), [setIndex]);
3690
- useCompoundKeyboardNav({
3691
- containerRef,
3692
- visibleIndex,
3693
- pageCount: slides.length,
3694
- goNext,
3695
- goPrev,
3696
- setIndex: setIndexStable
3697
- });
3698
- const slideTitles = useMemo15(
3699
- () => slides.map((slide) => slide.props.title),
3700
- [slides]
3701
- );
3702
- useEffect17(() => {
3703
- if (!lessonId || slides.length === 0) return;
3704
- track(
3705
- "slide_viewed",
3706
- {
3707
- blockId,
3708
- slideIndex: visibleIndex,
3709
- slideTitle: slideTitles[visibleIndex]
3710
- },
3711
- { lessonId }
3712
- );
3713
- }, [visibleIndex, blockId, lessonId, slides.length, slideTitles, track]);
3714
- return /* @__PURE__ */ jsxs13(
3715
- "section",
3716
- {
3717
- ref: containerRef,
3718
- tabIndex: -1,
3719
- "aria-label": props.title,
3720
- "data-testid": "slide-deck",
3721
- "data-lk-block-id": blockId,
3722
- children: [
3723
- /* @__PURE__ */ jsx20("h3", { children: props.title }),
3724
- /* @__PURE__ */ jsxs13("p", { children: [
3725
- "Slide ",
3726
- progress.current,
3727
- " of ",
3728
- progress.total
3729
- ] }),
3730
- props.showDeckScore && ctx ? /* @__PURE__ */ jsxs13("p", { "data-testid": "deck-score", children: [
3731
- "Score: ",
3732
- Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getScore(), 0),
3733
- " /",
3734
- " ",
3735
- Array.from(ctx.getHandles().values()).reduce((s, h) => s + h.getMaxScore(), 0)
3736
- ] }) : null,
3737
- /* @__PURE__ */ jsx20("div", { "data-testid": "slide-deck-slide", children: slides.map(
3738
- (slide, i) => React21.cloneElement(slide, {
3739
- key: slide.key ?? slide.props.blockId,
3740
- hidden: i !== visibleIndex,
3741
- slideIndex: i,
3742
- parentType: "SlideDeck"
3743
- })
3744
- ) }),
3745
- /* @__PURE__ */ jsxs13("nav", { "aria-label": "Slide navigation", children: [
3746
- /* @__PURE__ */ jsx20(
3747
- "button",
3748
- {
3749
- type: "button",
3750
- "data-testid": "slide-prev",
3751
- disabled: visibleIndex === 0 || slides.length === 0,
3752
- onClick: goPrev,
3753
- children: "Previous slide"
3754
- }
3755
- ),
3756
- /* @__PURE__ */ jsx20(
3757
- "button",
3758
- {
3759
- type: "button",
3760
- "data-testid": "slide-next",
3761
- disabled: visibleIndex >= slides.length - 1 || slides.length === 0,
3762
- onClick: goNext,
3763
- children: "Next slide"
3764
- }
3765
- )
3766
- ] })
3767
- ]
3768
- }
3769
- );
3770
- });
3771
- var SlideDeck = forwardRef9(function SlideDeck2(props, ref) {
3772
- const blockId = useMemo15(
3773
- () => normalizeComponentId(props.blockId, "blockId"),
110
+ function Scenario(props) {
111
+ const blockId = useMemo(
112
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
3774
113
  [props.blockId]
3775
114
  );
3776
- const slides = React21.Children.toArray(props.children).filter(
3777
- React21.isValidElement
3778
- );
3779
- const { config, storage } = useLessonkit();
3780
- const persistEnabled = config.session?.persistCompoundState !== false;
3781
- const initialIndex = useCompoundInitialIndex({
3782
- courseId: config.courseId,
3783
- compoundId: blockId,
3784
- pageCount: slides.length,
3785
- persistEnabled,
3786
- storage
3787
- });
3788
- const [index, setIndex] = useState12(initialIndex);
3789
- const setIndexStable = useCallback9((i) => setIndex(i), []);
3790
- useEffect17(() => {
3791
- setIndex(initialIndex);
3792
- }, [config.courseId, blockId, initialIndex]);
3793
- return /* @__PURE__ */ jsx20(CompoundProvider, { activePageIndex: index, onActivePageIndexChange: setIndexStable, children: /* @__PURE__ */ jsx20(
3794
- SlideDeckInner,
3795
- {
3796
- ...props,
3797
- ref,
3798
- blockId,
3799
- slides,
3800
- index,
3801
- setIndex,
3802
- persistEnabled
3803
- }
3804
- ) });
3805
- });
3806
- setLessonkitBlockType(SlideDeck, "SlideDeck");
3807
-
3808
- // src/blocks/Accordion.tsx
3809
- import { useId as useId4, useState as useState13 } from "react";
3810
- import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
3811
- function Accordion(props) {
3812
- if (isDevEnvironment4()) {
3813
- validateAccordionSections(props.sections);
3814
- }
3815
- const [open, setOpen] = useState13(/* @__PURE__ */ new Set());
3816
- const { track } = useLessonkit();
3817
- const lessonId = useEnclosingLessonId();
3818
- const baseId = useId4();
3819
- const toggle = (sectionId) => {
3820
- setOpen((prev) => {
3821
- const next = new Set(prev);
3822
- const expanded = !next.has(sectionId);
3823
- if (expanded) next.add(sectionId);
3824
- else next.delete(sectionId);
3825
- track(
3826
- "accordion_section_toggled",
3827
- { blockId: props.blockId, sectionId, expanded },
3828
- lessonId ? { lessonId } : void 0
3829
- );
3830
- return next;
3831
- });
3832
- };
3833
- return /* @__PURE__ */ jsx21("section", { "aria-label": "Accordion", "data-lk-block-id": props.blockId, "data-testid": "accordion", children: props.sections.map((section) => {
3834
- const expanded = open.has(section.id);
3835
- const panelId = `${baseId}-${section.id}`;
3836
- const triggerId = `${baseId}-trigger-${section.id}`;
3837
- return /* @__PURE__ */ jsxs14("div", { "data-testid": `accordion-section-${section.id}`, children: [
3838
- /* @__PURE__ */ jsx21("h4", { children: /* @__PURE__ */ jsx21(
3839
- "button",
3840
- {
3841
- id: triggerId,
3842
- type: "button",
3843
- "aria-expanded": expanded,
3844
- "aria-controls": panelId,
3845
- "data-testid": `accordion-trigger-${section.id}`,
3846
- onClick: () => toggle(section.id),
3847
- children: section.title
3848
- }
3849
- ) }),
3850
- expanded ? /* @__PURE__ */ jsx21("div", { id: panelId, role: "region", "aria-labelledby": triggerId, children: section.content }) : null
3851
- ] }, section.id);
3852
- }) });
3853
- }
3854
- setLessonkitBlockType(Accordion, "Accordion");
3855
-
3856
- // src/blocks/DialogCards.tsx
3857
- import { useState as useState14 } from "react";
3858
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
3859
- function DialogCards(props) {
3860
- const [index, setIndex] = useState14(0);
3861
- const [flipped, setFlipped] = useState14(false);
3862
- const card = props.cards[index];
3863
- if (!card) return null;
3864
- return /* @__PURE__ */ jsxs15("section", { "aria-label": "Dialog cards", "data-lk-block-id": props.blockId, "data-testid": "dialog-cards", children: [
3865
- /* @__PURE__ */ jsxs15("p", { children: [
3866
- "Card ",
3867
- index + 1,
3868
- " of ",
3869
- props.cards.length
3870
- ] }),
3871
- /* @__PURE__ */ jsx22(
3872
- "button",
3873
- {
3874
- type: "button",
3875
- "data-testid": "dialog-card-flip",
3876
- "aria-pressed": flipped,
3877
- onClick: () => setFlipped((f) => !f),
3878
- style: { minHeight: "6rem", width: "100%" },
3879
- children: flipped ? card.back : card.front
3880
- }
3881
- ),
3882
- /* @__PURE__ */ jsxs15("nav", { "aria-label": "Card navigation", children: [
3883
- /* @__PURE__ */ jsx22(
3884
- "button",
3885
- {
3886
- type: "button",
3887
- "data-testid": "dialog-prev",
3888
- disabled: index === 0,
3889
- onClick: () => {
3890
- setIndex((i) => i - 1);
3891
- setFlipped(false);
3892
- },
3893
- children: "Previous"
3894
- }
3895
- ),
3896
- /* @__PURE__ */ jsx22(
3897
- "button",
3898
- {
3899
- type: "button",
3900
- "data-testid": "dialog-next",
3901
- disabled: index >= props.cards.length - 1,
3902
- onClick: () => {
3903
- setIndex((i) => i + 1);
3904
- setFlipped(false);
3905
- },
3906
- children: "Next"
3907
- }
3908
- )
3909
- ] })
3910
- ] });
115
+ return /* @__PURE__ */ jsx("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
3911
116
  }
3912
- setLessonkitBlockType(DialogCards, "DialogCards");
3913
-
3914
- // src/blocks/Flashcards.tsx
3915
- import { useState as useState15 } from "react";
3916
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
3917
- function Flashcards(props) {
3918
- const [index, setIndex] = useState15(0);
3919
- const [face, setFace] = useState15("front");
3920
- const { track } = useLessonkit();
3921
- const lessonId = useEnclosingLessonId();
3922
- const card = props.cards[index];
3923
- if (!card) return null;
3924
- const flip = () => {
3925
- const next = face === "front" ? "back" : "front";
3926
- setFace(next);
3927
- track(
3928
- "flashcard_flipped",
3929
- { blockId: props.blockId, cardIndex: index, face: next },
3930
- lessonId ? { lessonId } : void 0
3931
- );
117
+ function Reflection(props) {
118
+ const blockId = useMemo(
119
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
120
+ [props.blockId]
121
+ );
122
+ const promptId = useId();
123
+ const hintId = useId();
124
+ const [internalValue, setInternalValue] = useState("");
125
+ const isControlled = props.value !== void 0;
126
+ const value = isControlled ? props.value : internalValue;
127
+ const handleChange = (event) => {
128
+ if (!isControlled) setInternalValue(event.target.value);
129
+ props.onChange?.(event.target.value);
3932
130
  };
3933
- return /* @__PURE__ */ jsxs16("section", { "aria-label": "Flashcards", "data-lk-block-id": props.blockId, "data-testid": "flashcards", children: [
3934
- /* @__PURE__ */ jsx23("button", { type: "button", "data-testid": "flashcard-flip", onClick: flip, style: { minHeight: "6rem", width: "100%" }, children: face === "front" ? card.front : card.back }),
3935
- props.selfScore ? /* @__PURE__ */ jsx23("p", { "data-testid": "flashcard-self-score", children: "Self-score mode enabled" }) : null,
3936
- /* @__PURE__ */ jsx23(
3937
- "button",
131
+ return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
132
+ props.prompt ? /* @__PURE__ */ jsx("p", { id: promptId, children: props.prompt }) : null,
133
+ props.hint ? /* @__PURE__ */ jsx("p", { id: hintId, style: visuallyHiddenStyle, children: props.hint }) : null,
134
+ props.children,
135
+ /* @__PURE__ */ jsx(
136
+ "textarea",
3938
137
  {
3939
- type: "button",
3940
- "data-testid": "flashcard-next",
3941
- disabled: index >= props.cards.length - 1,
3942
- onClick: () => {
3943
- setIndex((i) => i + 1);
3944
- setFace("front");
3945
- },
3946
- children: "Next card"
138
+ value,
139
+ onChange: handleChange,
140
+ "aria-labelledby": props.prompt ? promptId : void 0,
141
+ "aria-describedby": props.hint ? hintId : void 0,
142
+ "aria-label": props.prompt ? void 0 : "Reflection response"
3947
143
  }
3948
144
  )
3949
145
  ] });
3950
146
  }
3951
- setLessonkitBlockType(Flashcards, "Flashcards");
3952
-
3953
- // src/blocks/ImageHotspots.tsx
3954
- import { useState as useState16 } from "react";
3955
- import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3956
- function ImageHotspots(props) {
3957
- const [active, setActive] = useState16(null);
3958
- const { track } = useLessonkit();
3959
- const lessonId = useEnclosingLessonId();
3960
- const open = (hotspotId) => {
3961
- setActive(hotspotId);
3962
- track(
3963
- "hotspot_opened",
3964
- { blockId: props.blockId, hotspotId },
3965
- lessonId ? { lessonId } : void 0
3966
- );
3967
- };
3968
- return /* @__PURE__ */ jsxs17("section", { "aria-label": "Image hotspots", "data-lk-block-id": props.blockId, "data-testid": "image-hotspots", children: [
3969
- /* @__PURE__ */ jsxs17("div", { style: { position: "relative", display: "inline-block" }, children: [
3970
- /* @__PURE__ */ jsx24("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
3971
- props.hotspots.map((h) => /* @__PURE__ */ jsx24(
3972
- "button",
3973
- {
3974
- type: "button",
3975
- "aria-expanded": active === h.id,
3976
- "aria-label": h.label,
3977
- "data-testid": `hotspot-${h.id}`,
3978
- style: {
3979
- position: "absolute",
3980
- left: `${h.x}%`,
3981
- top: `${h.y}%`,
3982
- transform: "translate(-50%, -50%)"
3983
- },
3984
- onClick: () => open(h.id),
3985
- children: "+"
3986
- },
3987
- h.id
3988
- ))
3989
- ] }),
3990
- active ? /* @__PURE__ */ jsxs17("div", { role: "dialog", "aria-label": "Hotspot details", "data-testid": "hotspot-popover", children: [
3991
- props.hotspots.find((h) => h.id === active)?.content,
3992
- /* @__PURE__ */ jsx24("button", { type: "button", onClick: () => setActive(null), children: "Close" })
3993
- ] }) : null
3994
- ] });
3995
- }
3996
- setLessonkitBlockType(ImageHotspots, "ImageHotspots");
3997
-
3998
- // src/blocks/ImageSlider.tsx
3999
- import { useState as useState17 } from "react";
4000
- import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
4001
- function ImageSlider(props) {
4002
- const [index, setIndex] = useState17(0);
4003
- const { track } = useLessonkit();
4004
- const lessonId = useEnclosingLessonId();
4005
- const slide = props.slides[index];
4006
- if (!slide) return null;
4007
- const goTo = (next) => {
4008
- setIndex(next);
4009
- track(
4010
- "image_slider_changed",
4011
- { blockId: props.blockId, slideIndex: next },
4012
- lessonId ? { lessonId } : void 0
4013
- );
4014
- };
4015
- return /* @__PURE__ */ jsxs18("section", { "aria-label": "Image slider", "data-lk-block-id": props.blockId, "data-testid": "image-slider", children: [
4016
- /* @__PURE__ */ jsx25("img", { src: slide.src, alt: slide.alt, style: { maxWidth: "100%" } }),
4017
- slide.caption ? /* @__PURE__ */ jsx25("p", { children: slide.caption }) : null,
4018
- /* @__PURE__ */ jsxs18("nav", { "aria-label": "Slide navigation", children: [
4019
- /* @__PURE__ */ jsx25(
4020
- "button",
4021
- {
4022
- type: "button",
4023
- "data-testid": "slider-prev",
4024
- disabled: index === 0,
4025
- onClick: () => goTo(index - 1),
4026
- children: "Previous"
4027
- }
4028
- ),
4029
- /* @__PURE__ */ jsxs18("span", { children: [
4030
- index + 1,
4031
- " / ",
4032
- props.slides.length
4033
- ] }),
4034
- /* @__PURE__ */ jsx25(
4035
- "button",
4036
- {
4037
- type: "button",
4038
- "data-testid": "slider-next",
4039
- disabled: index >= props.slides.length - 1,
4040
- onClick: () => goTo(index + 1),
4041
- children: "Next"
4042
- }
4043
- )
4044
- ] })
4045
- ] });
4046
- }
4047
- setLessonkitBlockType(ImageSlider, "ImageSlider");
4048
-
4049
- // src/blocks/FindHotspot.tsx
4050
- import { forwardRef as forwardRef10, useEffect as useEffect18, useMemo as useMemo16, useRef as useRef15, useState as useState18 } from "react";
4051
- import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
4052
- var INTERACTION6 = "findHotspot";
4053
- function FindHotspotInner(props, ref) {
4054
- const checkId = useMemo16(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4055
- const [selected, setSelected] = useState18(null);
4056
- const [checked, setChecked] = useState18(false);
4057
- const telemetryReplayedRef = useRef15(false);
4058
- const assessment = useAssessmentState(props.enclosingLessonId);
4059
- const targetIdsKey = props.targets.map((t) => t.id).join("\0");
4060
- useEffect18(() => {
4061
- setSelected(null);
4062
- setChecked(false);
4063
- telemetryReplayedRef.current = false;
4064
- }, [checkId, props.correctTargetId, targetIdsKey]);
4065
- const correct = selected === props.correctTargetId;
4066
- const replayTelemetry = (nextSelected, nextChecked, nextCorrect) => {
4067
- if (telemetryReplayedRef.current || !nextChecked || nextSelected === null) return;
4068
- telemetryReplayedRef.current = true;
4069
- assessment.answer({
4070
- checkId,
4071
- interactionType: INTERACTION6,
4072
- response: nextSelected,
4073
- correct: nextCorrect
4074
- });
4075
- if (nextCorrect) {
4076
- assessment.complete({
4077
- checkId,
4078
- interactionType: INTERACTION6,
4079
- score: 1,
4080
- maxScore: 1,
4081
- passingScore: props.passingScore ?? 1
4082
- });
4083
- }
4084
- };
4085
- const handle = useMemo16(
4086
- () => buildAssessmentHandle({
4087
- checkId,
4088
- getScore: () => checked && correct ? 1 : 0,
4089
- getMaxScore: () => 1,
4090
- getAnswerGiven: () => selected !== null,
4091
- resetTask: () => {
4092
- setSelected(null);
4093
- setChecked(false);
4094
- telemetryReplayedRef.current = false;
4095
- },
4096
- showSolutions: () => setSelected(props.correctTargetId),
4097
- getXAPIData: () => ({
4098
- checkId,
4099
- interactionType: INTERACTION6,
4100
- response: selected ?? void 0,
4101
- correct: checked ? correct : void 0,
4102
- score: checked && correct ? 1 : 0,
4103
- maxScore: 1
4104
- }),
4105
- getCurrentState: () => ({ selected, checked }),
4106
- resume: (state) => {
4107
- let nextSelected = selected;
4108
- const rawSelected = readStringField(state, "selected");
4109
- if (typeof rawSelected === "string" || rawSelected === null) {
4110
- const valid = rawSelected === null || props.targets.some((t) => t.id === rawSelected);
4111
- nextSelected = valid ? rawSelected : null;
4112
- setSelected(nextSelected);
4113
- }
4114
- let nextChecked = checked;
4115
- readBooleanStateField(state, "checked", (value) => {
4116
- nextChecked = value;
4117
- setChecked(value);
4118
- });
4119
- const nextCorrect = nextSelected === props.correctTargetId;
4120
- replayTelemetry(nextSelected, nextChecked, nextCorrect);
4121
- }
4122
- }),
4123
- [assessment, checkId, checked, correct, props.correctTargetId, props.passingScore, props.targets, selected]
4124
- );
4125
- useAssessmentHandleRegistration(checkId, handle, ref);
4126
- const selectTarget = (id) => {
4127
- setSelected(id);
4128
- setChecked(false);
4129
- };
4130
- const submit = () => {
4131
- if (!selected || checked) return;
4132
- setChecked(true);
4133
- assessment.answer({
4134
- checkId,
4135
- interactionType: INTERACTION6,
4136
- response: selected,
4137
- correct
4138
- });
4139
- if (correct) {
4140
- assessment.complete({
4141
- checkId,
4142
- interactionType: INTERACTION6,
4143
- score: 1,
4144
- maxScore: 1,
4145
- passingScore: props.passingScore ?? 1
4146
- });
4147
- }
4148
- };
4149
- return /* @__PURE__ */ jsxs19("section", { "aria-label": "Find the hotspot", "data-lk-check-id": checkId, "data-testid": "find-hotspot", children: [
4150
- /* @__PURE__ */ jsxs19("div", { style: { position: "relative", display: "inline-block" }, children: [
4151
- /* @__PURE__ */ jsx26("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4152
- props.targets.map((t) => /* @__PURE__ */ jsx26(
4153
- "button",
4154
- {
4155
- type: "button",
4156
- "aria-label": t.label,
4157
- "aria-pressed": selected === t.id,
4158
- "data-testid": `target-${t.id}`,
4159
- style: {
4160
- position: "absolute",
4161
- left: `${t.x}%`,
4162
- top: `${t.y}%`,
4163
- transform: "translate(-50%, -50%)"
4164
- },
4165
- onClick: () => selectTarget(t.id),
4166
- children: t.label
4167
- },
4168
- t.id
4169
- ))
4170
- ] }),
4171
- /* @__PURE__ */ jsx26("button", { type: "button", "data-testid": "check-hotspot", disabled: !selected, onClick: submit, children: "Check" }),
4172
- checked ? /* @__PURE__ */ jsx26("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4173
- ] });
4174
- }
4175
- var FindHotspotInnerForwarded = forwardRef10(FindHotspotInner);
4176
- var FindHotspot = forwardRef10(function FindHotspot2(props, ref) {
4177
- return /* @__PURE__ */ jsx26(AssessmentLessonGuard, { blockLabel: "FindHotspot", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx26(FindHotspotInnerForwarded, { ...props, enclosingLessonId, ref }) });
4178
- });
4179
- setLessonkitBlockType(FindHotspot, "FindHotspot");
4180
-
4181
- // src/blocks/FindMultipleHotspots.tsx
4182
- import { forwardRef as forwardRef11, useMemo as useMemo17, useState as useState19 } from "react";
4183
- import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
4184
- var INTERACTION7 = "findMultipleHotspots";
4185
- function FindMultipleHotspotsInner(props, ref) {
4186
- const checkId = useMemo17(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
4187
- const [selected, setSelected] = useState19(/* @__PURE__ */ new Set());
4188
- const [checked, setChecked] = useState19(false);
4189
- const assessment = useAssessmentState(props.enclosingLessonId);
4190
- const toggle = (id) => {
4191
- setSelected((prev) => {
4192
- const next = new Set(prev);
4193
- if (next.has(id)) next.delete(id);
4194
- else next.add(id);
4195
- return next;
4196
- });
4197
- setChecked(false);
4198
- };
4199
- const correct = selected.size === props.correctTargetIds.length && props.correctTargetIds.every((id) => selected.has(id));
4200
- const handle = useMemo17(
4201
- () => buildAssessmentHandle({
4202
- checkId,
4203
- getScore: () => checked && correct ? 1 : 0,
4204
- getMaxScore: () => 1,
4205
- getAnswerGiven: () => selected.size > 0,
4206
- resetTask: () => {
4207
- setSelected(/* @__PURE__ */ new Set());
4208
- setChecked(false);
4209
- },
4210
- showSolutions: () => setSelected(new Set(props.correctTargetIds)),
4211
- getXAPIData: () => ({
4212
- checkId,
4213
- interactionType: INTERACTION7,
4214
- response: [...selected],
4215
- correct: checked ? correct : void 0,
4216
- score: checked && correct ? 1 : 0,
4217
- maxScore: 1
4218
- }),
4219
- getCurrentState: () => ({ selected: [...selected], checked }),
4220
- resume: (state) => {
4221
- const raw = state.selected;
4222
- if (Array.isArray(raw)) setSelected(new Set(raw.filter((id) => typeof id === "string")));
4223
- readBooleanStateField(state, "checked", setChecked);
147
+ function ProgressTracker(props) {
148
+ const { progress } = useLessonkit();
149
+ const completed = progress.completedLessonIds.size;
150
+ if (props.totalLessons != null) {
151
+ const total = props.totalLessons;
152
+ const displayed = Math.min(completed, total);
153
+ return /* @__PURE__ */ jsx("aside", { "aria-label": "Progress", children: /* @__PURE__ */ jsx(
154
+ "div",
155
+ {
156
+ role: "progressbar",
157
+ "aria-valuemin": 0,
158
+ "aria-valuemax": total,
159
+ "aria-valuenow": displayed,
160
+ "aria-label": "Lessons completed",
161
+ children: /* @__PURE__ */ jsxs("p", { children: [
162
+ "Lessons completed: ",
163
+ displayed,
164
+ " of ",
165
+ total
166
+ ] })
4224
167
  }
4225
- }),
4226
- [checkId, selected, checked, correct, props.correctTargetIds]
4227
- );
4228
- useAssessmentHandleRegistration(checkId, handle, ref);
4229
- const submit = () => {
4230
- if (selected.size === 0 || checked) return;
4231
- setChecked(true);
4232
- assessment.answer({
4233
- checkId,
4234
- interactionType: INTERACTION7,
4235
- response: [...selected],
4236
- correct
4237
- });
4238
- if (correct) {
4239
- assessment.complete({
4240
- checkId,
4241
- interactionType: INTERACTION7,
4242
- score: 1,
4243
- maxScore: 1,
4244
- passingScore: props.passingScore ?? 1
4245
- });
4246
- }
4247
- };
4248
- return /* @__PURE__ */ jsxs20("section", { "aria-label": "Find multiple hotspots", "data-lk-check-id": checkId, "data-testid": "find-multiple-hotspots", children: [
4249
- /* @__PURE__ */ jsxs20("div", { style: { position: "relative", display: "inline-block" }, children: [
4250
- /* @__PURE__ */ jsx27("img", { src: props.src, alt: props.alt, style: { maxWidth: "100%" } }),
4251
- props.targets.map((t) => /* @__PURE__ */ jsx27(
4252
- "button",
4253
- {
4254
- type: "button",
4255
- "aria-label": t.label,
4256
- "aria-pressed": selected.has(t.id),
4257
- "data-testid": `target-${t.id}`,
4258
- style: {
4259
- position: "absolute",
4260
- left: `${t.x}%`,
4261
- top: `${t.y}%`,
4262
- transform: "translate(-50%, -50%)"
4263
- },
4264
- onClick: () => toggle(t.id),
4265
- children: t.label
4266
- },
4267
- t.id
4268
- ))
4269
- ] }),
4270
- /* @__PURE__ */ jsx27("button", { type: "button", "data-testid": "check-hotspots", disabled: selected.size === 0, onClick: submit, children: "Check" }),
4271
- checked ? /* @__PURE__ */ jsx27("p", { role: "status", children: correct ? "Correct" : "Try again" }) : null
4272
- ] });
4273
- }
4274
- var FindMultipleHotspotsInnerForwarded = forwardRef11(FindMultipleHotspotsInner);
4275
- var FindMultipleHotspots = forwardRef11(
4276
- function FindMultipleHotspots2(props, ref) {
4277
- return /* @__PURE__ */ jsx27(AssessmentLessonGuard, { blockLabel: "FindMultipleHotspots", checkId: props.checkId, children: (enclosingLessonId) => /* @__PURE__ */ jsx27(FindMultipleHotspotsInnerForwarded, { ...props, enclosingLessonId, ref }) });
168
+ ) });
4278
169
  }
4279
- );
4280
- setLessonkitBlockType(FindMultipleHotspots, "FindMultipleHotspots");
170
+ return /* @__PURE__ */ jsx("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ jsxs("p", { children: [
171
+ "Lessons completed: ",
172
+ completed
173
+ ] }) });
174
+ }
4281
175
 
4282
176
  // src/index.tsx
4283
177
  import {
4284
- buildTelemetryEvent as buildTelemetryEvent2,
4285
- createLessonkitRuntime as createLessonkitRuntime2,
4286
- createPluginRegistry as createPluginRegistry2,
4287
- createTelemetryPipeline as createTelemetryPipeline2,
178
+ buildTelemetryEvent,
179
+ createLessonkitRuntime,
180
+ createPluginRegistry,
181
+ createTelemetryPipeline,
4288
182
  defineAssessmentPlugin,
4289
183
  defineLifecyclePlugin,
4290
184
  defineTelemetryPlugin
4291
185
  } from "@lessonkit/core";
4292
186
 
4293
187
  // src/theme/ThemeProvider.tsx
4294
- import React29, {
4295
- createContext as createContext6,
4296
- useCallback as useCallback10,
4297
- useContext as useContext8,
4298
- useLayoutEffect as useLayoutEffect2,
4299
- useMemo as useMemo18,
4300
- useRef as useRef16,
4301
- useState as useState20
188
+ import React2, {
189
+ createContext,
190
+ useCallback,
191
+ useContext,
192
+ useLayoutEffect,
193
+ useMemo as useMemo2,
194
+ useRef as useRef2,
195
+ useState as useState2
4302
196
  } from "react";
4303
197
  import {
4304
198
  brandThemeOverrides,
@@ -4325,11 +219,11 @@ function applyCssVariables(target, vars, previousKeys) {
4325
219
  }
4326
220
 
4327
221
  // src/theme/ThemeProvider.tsx
4328
- import { jsx as jsx28 } from "react/jsx-runtime";
4329
- var ThemeContext = createContext6(null);
4330
- var useIsoLayoutEffect2 = (
222
+ import { jsx as jsx2 } from "react/jsx-runtime";
223
+ var ThemeContext = createContext(null);
224
+ var useIsoLayoutEffect = (
4331
225
  /* v8 ignore next -- SSR uses useEffect when window is unavailable */
4332
- typeof window !== "undefined" ? useLayoutEffect2 : React29.useEffect
226
+ typeof window !== "undefined" ? useLayoutEffect : React2.useEffect
4333
227
  );
4334
228
  function getSystemMode() {
4335
229
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -4348,10 +242,10 @@ function ThemeProvider(props) {
4348
242
  const preset = props.preset ?? "default";
4349
243
  const mode = props.mode ?? "light";
4350
244
  const targetKind = props.target ?? "document";
4351
- const [resolvedMode, setResolvedMode] = useState20(
245
+ const [resolvedMode, setResolvedMode] = useState2(
4352
246
  () => mode === "system" ? getSystemMode() : mode
4353
247
  );
4354
- useIsoLayoutEffect2(() => {
248
+ useIsoLayoutEffect(() => {
4355
249
  if (mode !== "system") {
4356
250
  setResolvedMode(mode);
4357
251
  return;
@@ -4364,26 +258,26 @@ function ThemeProvider(props) {
4364
258
  return () => mq.removeEventListener("change", onChange);
4365
259
  }, [mode]);
4366
260
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
4367
- const effectiveTheme = useMemo18(() => {
261
+ const effectiveTheme = useMemo2(() => {
4368
262
  const modeBase = resolveModeBase(mode, dataTheme);
4369
263
  const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
4370
264
  return mergeThemes(base, props.theme ?? {});
4371
265
  }, [preset, mode, dataTheme, props.theme]);
4372
- const hostRef = useRef16(null);
4373
- const appliedKeysRef = useRef16(/* @__PURE__ */ new Set());
4374
- useIsoLayoutEffect2(() => {
266
+ const hostRef = useRef2(null);
267
+ const appliedKeysRef = useRef2(/* @__PURE__ */ new Set());
268
+ useIsoLayoutEffect(() => {
4375
269
  if (targetKind === "document" && typeof document !== "undefined") {
4376
270
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
4377
271
  return () => document.documentElement.removeAttribute("data-lk-theme");
4378
272
  }
4379
273
  }, [targetKind, dataTheme]);
4380
- const inject = useCallback10(() => {
274
+ const inject = useCallback(() => {
4381
275
  const vars = themeToCssVariables(effectiveTheme);
4382
276
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
4383
277
  if (!el) return;
4384
278
  appliedKeysRef.current = applyCssVariables(el, vars, appliedKeysRef.current);
4385
279
  }, [effectiveTheme, targetKind]);
4386
- useIsoLayoutEffect2(() => {
280
+ useIsoLayoutEffect(() => {
4387
281
  inject();
4388
282
  return () => {
4389
283
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
@@ -4394,7 +288,7 @@ function ThemeProvider(props) {
4394
288
  appliedKeysRef.current = /* @__PURE__ */ new Set();
4395
289
  };
4396
290
  }, [inject, targetKind]);
4397
- const value = useMemo18(
291
+ const value = useMemo2(
4398
292
  () => ({
4399
293
  theme: effectiveTheme,
4400
294
  preset,
@@ -4404,12 +298,12 @@ function ThemeProvider(props) {
4404
298
  [effectiveTheme, preset, mode, dataTheme]
4405
299
  );
4406
300
  if (targetKind === "document") {
4407
- return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
301
+ return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx2("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
4408
302
  }
4409
- return /* @__PURE__ */ jsx28(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx28("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
303
+ return /* @__PURE__ */ jsx2(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx2("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
4410
304
  }
4411
305
  function useTheme() {
4412
- const ctx = useContext8(ThemeContext);
306
+ const ctx = useContext(ThemeContext);
4413
307
  if (!ctx) {
4414
308
  throw new Error("useTheme must be used within a ThemeProvider");
4415
309
  }
@@ -4420,10 +314,12 @@ function useTheme() {
4420
314
  import {
4421
315
  ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
4422
316
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
317
+ INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
4423
318
  PAGE_ALLOWED_CHILD_TYPES,
4424
319
  SLIDE_ALLOWED_CHILD_TYPES,
4425
320
  SLIDE_DECK_ALLOWED_CHILD_TYPES,
4426
- COMPOUND_MAX_NESTING_DEPTH as COMPOUND_MAX_NESTING_DEPTH2
321
+ TIMED_CUE_ALLOWED_CHILD_TYPES,
322
+ COMPOUND_MAX_NESTING_DEPTH
4427
323
  } from "@lessonkit/core";
4428
324
  var COMPOUND_PARENTS = [
4429
325
  "Lesson",
@@ -4431,6 +327,8 @@ var COMPOUND_PARENTS = [
4431
327
  "InteractiveBook",
4432
328
  "Slide",
4433
329
  "SlideDeck",
330
+ "TimedCue",
331
+ "InteractiveVideo",
4434
332
  "AssessmentSequence"
4435
333
  ];
4436
334
  function extendParents(entry) {
@@ -4489,6 +387,23 @@ var v3CompoundAndContentEntries = [
4489
387
  theming: { surface: "global-inherit", stylingNotes: "Responsive max-width." },
4490
388
  telemetry: { emits: [] }
4491
389
  },
390
+ {
391
+ type: "Video",
392
+ category: "content",
393
+ description: "Self-hosted video with native controls and optional captions.",
394
+ props: [
395
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
396
+ { name: "src", type: "string", required: true, description: "Video URL." },
397
+ { name: "poster", type: "string", required: false, description: "Poster image URL." },
398
+ { name: "captions", type: "string", required: false, description: "WebVTT captions URL." },
399
+ { name: "title", type: "string", required: false, description: "Accessible title." }
400
+ ],
401
+ requiredIds: ["blockId"],
402
+ parentConstraints: [...COMPOUND_PARENTS],
403
+ a11y: { element: "video", ariaLabel: "Video", keyboard: "Native video controls.", notes: "No autoplay with sound." },
404
+ theming: { surface: "global-inherit", stylingNotes: "Responsive video." },
405
+ telemetry: { emits: [] }
406
+ },
4492
407
  {
4493
408
  type: "Page",
4494
409
  category: "container",
@@ -4497,7 +412,7 @@ var v3CompoundAndContentEntries = [
4497
412
  h5pAlias: "Column",
4498
413
  description: "Column layout container (H5P Column / Page).",
4499
414
  allowedChildTypes: [...PAGE_ALLOWED_CHILD_TYPES],
4500
- maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Page,
415
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.Page,
4501
416
  props: [
4502
417
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4503
418
  { name: "title", type: "string", required: false, description: "Page title." },
@@ -4518,7 +433,7 @@ var v3CompoundAndContentEntries = [
4518
433
  h5pAlias: "Interactive Book",
4519
434
  description: "Multi-page book with chapter navigation.",
4520
435
  allowedChildTypes: [...INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES],
4521
- maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.InteractiveBook,
436
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.InteractiveBook,
4522
437
  props: [
4523
438
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4524
439
  { name: "title", type: "string", required: true, description: "Book title." },
@@ -4542,9 +457,9 @@ var v3CompoundAndContentEntries = [
4542
457
  compoundContract: true,
4543
458
  h5pMachineName: "H5P.CoursePresentation",
4544
459
  h5pAlias: "Course Presentation slide",
4545
- description: "Single slide row in a SlideDeck. Planned allowlist expansion: Video, Summary.",
460
+ description: "Single slide row in a SlideDeck. Supports Video, Summary, and 1.4 blocks.",
4546
461
  allowedChildTypes: [...SLIDE_ALLOWED_CHILD_TYPES],
4547
- maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.Slide,
462
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.Slide,
4548
463
  props: [
4549
464
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4550
465
  { name: "title", type: "string", required: false, description: "Slide title." },
@@ -4565,7 +480,7 @@ var v3CompoundAndContentEntries = [
4565
480
  h5pAlias: "Course Presentation",
4566
481
  description: "Multi-slide presentation with keyboard navigation.",
4567
482
  allowedChildTypes: [...SLIDE_DECK_ALLOWED_CHILD_TYPES],
4568
- maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.SlideDeck,
483
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.SlideDeck,
4569
484
  props: [
4570
485
  { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
4571
486
  { name: "title", type: "string", required: true, description: "Deck title." },
@@ -4583,6 +498,121 @@ var v3CompoundAndContentEntries = [
4583
498
  theming: { surface: "global-inherit", stylingNotes: "Deck chrome." },
4584
499
  telemetry: { emits: ["slide_viewed"], requiresActiveLesson: true }
4585
500
  },
501
+ {
502
+ type: "TimedCue",
503
+ category: "container",
504
+ compoundContract: true,
505
+ h5pMachineName: "H5P.InteractiveVideo",
506
+ h5pAlias: "Interactive Video timed cue",
507
+ description: "Timed overlay cue within InteractiveVideo.",
508
+ allowedChildTypes: [...TIMED_CUE_ALLOWED_CHILD_TYPES],
509
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.TimedCue,
510
+ props: [
511
+ { name: "atSeconds", type: "number", required: true, description: "Cue time in seconds." },
512
+ { name: "label", type: "string", required: false, description: "Cue label." },
513
+ { name: "mustComplete", type: "boolean", required: false, description: "Block seek until completed." },
514
+ { name: "children", type: "ReactNode", required: true, description: "Single allowed child block." }
515
+ ],
516
+ requiredIds: [],
517
+ parentConstraints: ["InteractiveVideo"],
518
+ a11y: { element: "dialog", ariaLabel: "Timed cue", keyboard: "Focus moves to overlay content.", notes: "Pauses parent video." },
519
+ theming: { surface: "global-inherit", stylingNotes: "Overlay panel." },
520
+ telemetry: { emits: [] }
521
+ },
522
+ {
523
+ type: "InteractiveVideo",
524
+ category: "container",
525
+ compoundContract: true,
526
+ h5pMachineName: "H5P.InteractiveVideo",
527
+ h5pAlias: "Interactive Video",
528
+ description: "Video with timed interaction overlays.",
529
+ allowedChildTypes: [...INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES],
530
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.InteractiveVideo,
531
+ props: [
532
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
533
+ { name: "title", type: "string", required: true, description: "Video title." },
534
+ { name: "src", type: "string", required: true, description: "Video URL." },
535
+ { name: "poster", type: "string", required: false, description: "Poster image." },
536
+ { name: "captions", type: "string", required: false, description: "WebVTT captions." },
537
+ { name: "showVideoScore", type: "boolean", required: false, description: "Show aggregate score." },
538
+ { name: "children", type: "TimedCue[]", required: true, description: "Timed cues." }
539
+ ],
540
+ requiredIds: ["blockId"],
541
+ parentConstraints: ["Lesson"],
542
+ a11y: {
543
+ element: "section",
544
+ ariaLabel: "Interactive video",
545
+ keyboard: "Native video controls; overlay interactions when paused.",
546
+ notes: "H5P Interactive Video equivalent."
547
+ },
548
+ theming: { surface: "global-inherit", stylingNotes: "Video + overlay." },
549
+ telemetry: { emits: ["video_cue_reached", "video_segment_completed"], requiresActiveLesson: true }
550
+ },
551
+ {
552
+ type: "Questionnaire",
553
+ category: "content",
554
+ h5pMachineName: "H5P.Questionnaire",
555
+ h5pAlias: "Questionnaire",
556
+ description: "Unscored multi-field survey.",
557
+ props: [
558
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
559
+ { name: "fields", type: "QuestionnaireField[]", required: true, description: "Form fields." }
560
+ ],
561
+ requiredIds: ["blockId"],
562
+ parentConstraints: [...COMPOUND_PARENTS],
563
+ a11y: { element: "form", ariaLabel: "Questionnaire", keyboard: "Tab through fields.", notes: "Unscored survey." },
564
+ theming: { surface: "global-inherit", stylingNotes: "Form layout." },
565
+ telemetry: { emits: ["questionnaire_submitted"], requiresActiveLesson: true }
566
+ },
567
+ {
568
+ type: "MemoryGame",
569
+ category: "content",
570
+ h5pMachineName: "H5P.MemoryGame",
571
+ h5pAlias: "Memory Game",
572
+ description: "Card flip memory matching game.",
573
+ props: [
574
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
575
+ { name: "pairs", type: "MemoryPair[]", required: true, description: "Card pairs." },
576
+ { name: "selfScore", type: "boolean", required: false, description: "Optional self-score mode." }
577
+ ],
578
+ requiredIds: ["blockId"],
579
+ parentConstraints: [...COMPOUND_PARENTS],
580
+ a11y: { element: "section", ariaLabel: "Memory game", keyboard: "Flip cards with buttons.", notes: "Reduced motion safe." },
581
+ theming: { surface: "global-inherit", stylingNotes: "Card grid." },
582
+ telemetry: { emits: ["memory_card_flipped"] }
583
+ },
584
+ {
585
+ type: "InformationWall",
586
+ category: "content",
587
+ h5pMachineName: "H5P.InformationWall",
588
+ h5pAlias: "Information Wall",
589
+ description: "Searchable information panels.",
590
+ props: [
591
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
592
+ { name: "panels", type: "InformationPanel[]", required: true, description: "Content panels." }
593
+ ],
594
+ requiredIds: ["blockId"],
595
+ parentConstraints: [...COMPOUND_PARENTS],
596
+ a11y: { element: "section", ariaLabel: "Information wall", keyboard: "Search and browse panels.", notes: "Filterable grid." },
597
+ theming: { surface: "global-inherit", stylingNotes: "Panel grid." },
598
+ telemetry: { emits: ["information_wall_search"] }
599
+ },
600
+ {
601
+ type: "ParallaxSlideshow",
602
+ category: "content",
603
+ h5pMachineName: "H5P.ImpressivePresentation",
604
+ h5pAlias: "Slideshow (parallax)",
605
+ description: "Slideshow with parallax; static fallback when reduced motion.",
606
+ props: [
607
+ { name: "blockId", type: "BlockId", required: true, description: "Stable block id." },
608
+ { name: "slides", type: "ParallaxSlide[]", required: true, description: "Slides." }
609
+ ],
610
+ requiredIds: ["blockId"],
611
+ parentConstraints: [...COMPOUND_PARENTS],
612
+ a11y: { element: "section", ariaLabel: "Slideshow", keyboard: "Previous/next slide.", notes: "prefers-reduced-motion fallback." },
613
+ theming: { surface: "global-inherit", stylingNotes: "Slide deck." },
614
+ telemetry: { emits: ["parallax_slide_viewed"] }
615
+ },
4586
616
  {
4587
617
  type: "Accordion",
4588
618
  category: "content",
@@ -4717,7 +747,7 @@ function buildV3CatalogFromV2(v2) {
4717
747
  ...base,
4718
748
  compoundContract: true,
4719
749
  allowedChildTypes: [...ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES],
4720
- maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH2.AssessmentSequence
750
+ maxNestingDepth: COMPOUND_MAX_NESTING_DEPTH.AssessmentSequence
4721
751
  };
4722
752
  }
4723
753
  return base;
@@ -5064,6 +1094,100 @@ var v2AssessmentEntries = [
5064
1094
  },
5065
1095
  theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
5066
1096
  telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
1097
+ },
1098
+ {
1099
+ type: "Summary",
1100
+ category: "assessment",
1101
+ assessmentContract: true,
1102
+ h5pMachineName: "H5P.Summary",
1103
+ h5pAlias: "Summary",
1104
+ description: "Construct a summary from a statement bank in correct order.",
1105
+ props: [
1106
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
1107
+ { name: "statements", type: "string[]", required: true, description: "Available statements." },
1108
+ { name: "correct", type: "string[]", required: true, description: "Correct ordered summary." },
1109
+ ...assessmentBehaviourProps2
1110
+ ],
1111
+ requiredIds: ["checkId"],
1112
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
1113
+ a11y: { element: "section", ariaLabel: "Summary", keyboard: "Select statements in order.", notes: "H5P Summary equivalent." },
1114
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
1115
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
1116
+ },
1117
+ {
1118
+ type: "ImagePairing",
1119
+ category: "assessment",
1120
+ assessmentContract: true,
1121
+ h5pMachineName: "H5P.ImagePair",
1122
+ h5pAlias: "Image Pairing",
1123
+ description: "Match image pairs in a memory-style task.",
1124
+ props: [
1125
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
1126
+ { name: "pairs", type: "ImagePair[]", required: true, description: "Image pairs to match." },
1127
+ ...assessmentBehaviourProps2
1128
+ ],
1129
+ requiredIds: ["checkId"],
1130
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
1131
+ a11y: { element: "section", ariaLabel: "Image Pairing", keyboard: "Select two cards to match.", notes: "H5P Image Pairing equivalent." },
1132
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
1133
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
1134
+ },
1135
+ {
1136
+ type: "ImageSequencing",
1137
+ category: "assessment",
1138
+ assessmentContract: true,
1139
+ h5pMachineName: "H5P.ImageSequencing",
1140
+ h5pAlias: "Image Sequencing",
1141
+ description: "Order images in the correct sequence.",
1142
+ props: [
1143
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
1144
+ { name: "images", type: "SequencingImage[]", required: true, description: "Images to order." },
1145
+ { name: "correctOrder", type: "string[]", required: true, description: "Correct id order." },
1146
+ ...assessmentBehaviourProps2
1147
+ ],
1148
+ requiredIds: ["checkId"],
1149
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
1150
+ a11y: { element: "section", ariaLabel: "Image Sequencing", keyboard: "Reorder with up/down.", notes: "H5P Image Sequencing equivalent." },
1151
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
1152
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
1153
+ },
1154
+ {
1155
+ type: "ArithmeticQuiz",
1156
+ category: "assessment",
1157
+ assessmentContract: true,
1158
+ h5pMachineName: "H5P.ArithmeticQuiz",
1159
+ h5pAlias: "Arithmetic Quiz",
1160
+ description: "Timed arithmetic problems with optional timer.",
1161
+ props: [
1162
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
1163
+ { name: "problems", type: "ArithmeticProblem[]", required: true, description: "Math problems." },
1164
+ { name: "timeLimitSeconds", type: "number", required: false, description: "Optional time limit." },
1165
+ ...assessmentBehaviourProps2
1166
+ ],
1167
+ requiredIds: ["checkId"],
1168
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
1169
+ a11y: { element: "section", ariaLabel: "Arithmetic Quiz", keyboard: "Text input per problem.", notes: "H5P Arithmetic Quiz equivalent." },
1170
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
1171
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
1172
+ },
1173
+ {
1174
+ type: "Essay",
1175
+ category: "assessment",
1176
+ assessmentContract: true,
1177
+ h5pMachineName: "H5P.Essay",
1178
+ h5pAlias: "Essay",
1179
+ description: "Open text response; manual or plugin grading.",
1180
+ props: [
1181
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
1182
+ { name: "question", type: "string", required: true, description: "Essay prompt." },
1183
+ { name: "minLength", type: "number", required: false, description: "Minimum character length." },
1184
+ ...assessmentBehaviourProps2
1185
+ ],
1186
+ requiredIds: ["checkId"],
1187
+ parentConstraints: ["Lesson", "AssessmentSequence", "TimedCue"],
1188
+ a11y: { element: "section", ariaLabel: "Essay", keyboard: "Textarea input.", notes: "H5P Essay equivalent." },
1189
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
1190
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
5067
1191
  }
5068
1192
  ];
5069
1193
  var BLOCK_CATALOG_V2 = [
@@ -5102,6 +1226,7 @@ function getBlockCatalogEntry(type, opts) {
5102
1226
  }
5103
1227
  export {
5104
1228
  Accordion,
1229
+ ArithmeticQuiz,
5105
1230
  AssessmentSequence,
5106
1231
  BLOCK_CATALOG,
5107
1232
  BLOCK_CATALOG_V2,
@@ -5110,6 +1235,7 @@ export {
5110
1235
  DialogCards,
5111
1236
  DragAndDrop,
5112
1237
  DragTheWords,
1238
+ Essay,
5113
1239
  FillInTheBlanks,
5114
1240
  FindHotspot,
5115
1241
  FindMultipleHotspots,
@@ -5117,36 +1243,48 @@ export {
5117
1243
  Heading,
5118
1244
  Image,
5119
1245
  ImageHotspots,
1246
+ ImagePairing,
1247
+ ImageSequencing,
5120
1248
  ImageSlider,
1249
+ InformationWall,
5121
1250
  InteractiveBook,
1251
+ InteractiveVideo,
5122
1252
  KnowledgeCheck,
5123
1253
  Lesson,
5124
1254
  LessonkitProvider,
5125
1255
  MarkTheWords,
1256
+ MemoryGame,
5126
1257
  Page,
1258
+ ParallaxSlideshow,
5127
1259
  ProgressTracker,
1260
+ Questionnaire,
5128
1261
  Quiz,
5129
1262
  Reflection,
5130
1263
  Scenario,
5131
1264
  Slide,
5132
1265
  SlideDeck,
1266
+ Summary,
5133
1267
  Text,
5134
1268
  ThemeProvider,
1269
+ TimedCue,
5135
1270
  TrueFalse,
1271
+ Video,
1272
+ assertProductionCourseConfig,
5136
1273
  blockCatalogV2Version,
5137
1274
  blockCatalogV3Version,
5138
1275
  blockCatalogVersion,
5139
1276
  buildBlockCatalog,
5140
- buildTelemetryEvent2 as buildTelemetryEvent,
5141
- createLessonkitRuntime2 as createLessonkitRuntime,
5142
- createPluginRegistry2 as createPluginRegistry,
5143
- createTelemetryPipeline2 as createTelemetryPipeline,
1277
+ buildTelemetryEvent,
1278
+ createLessonkitRuntime,
1279
+ createPluginRegistry,
1280
+ createTelemetryPipeline,
5144
1281
  defineAssessmentPlugin,
5145
1282
  defineLifecyclePlugin,
5146
1283
  defineTelemetryPlugin,
5147
1284
  getBlockCatalogEntry,
5148
1285
  resetAssessmentWarningsForTests,
5149
1286
  resetQuizWarningsForTests,
1287
+ shouldEnforceProductionGuard,
5150
1288
  useAssessmentState,
5151
1289
  useCompletion,
5152
1290
  useLessonkit,