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