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