@lessonkit/react 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,16 +30,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AssessmentSequence: () => AssessmentSequence,
33
34
  BLOCK_CATALOG: () => BLOCK_CATALOG,
35
+ BLOCK_CATALOG_V2: () => BLOCK_CATALOG_V2,
34
36
  Course: () => Course,
37
+ DragAndDrop: () => DragAndDrop,
38
+ DragTheWords: () => DragTheWords,
39
+ FillInTheBlanks: () => FillInTheBlanks,
35
40
  KnowledgeCheck: () => KnowledgeCheck,
36
41
  Lesson: () => Lesson,
37
42
  LessonkitProvider: () => LessonkitProvider,
43
+ MarkTheWords: () => MarkTheWords,
38
44
  ProgressTracker: () => ProgressTracker,
39
45
  Quiz: () => Quiz,
40
46
  Reflection: () => Reflection,
41
47
  Scenario: () => Scenario,
42
48
  ThemeProvider: () => ThemeProvider,
49
+ TrueFalse: () => TrueFalse,
50
+ blockCatalogV2Version: () => blockCatalogV2Version,
43
51
  blockCatalogVersion: () => blockCatalogVersion,
44
52
  buildBlockCatalog: () => buildBlockCatalog,
45
53
  buildTelemetryEvent: () => import_core10.buildTelemetryEvent,
@@ -50,7 +58,9 @@ __export(index_exports, {
50
58
  defineLifecyclePlugin: () => import_core10.defineLifecyclePlugin,
51
59
  defineTelemetryPlugin: () => import_core10.defineTelemetryPlugin,
52
60
  getBlockCatalogEntry: () => getBlockCatalogEntry,
61
+ resetAssessmentWarningsForTests: () => resetAssessmentWarningsForTests,
53
62
  resetQuizWarningsForTests: () => resetQuizWarningsForTests,
63
+ useAssessmentState: () => useAssessmentState,
54
64
  useCompletion: () => useCompletion,
55
65
  useLessonkit: () => useLessonkit,
56
66
  useProgress: () => useProgress,
@@ -61,9 +71,32 @@ __export(index_exports, {
61
71
  module.exports = __toCommonJS(index_exports);
62
72
 
63
73
  // src/components.tsx
64
- var import_react5 = require("react");
74
+ var import_react6 = require("react");
65
75
  var import_accessibility = require("@lessonkit/accessibility");
66
76
 
77
+ // src/assessment/scoring.ts
78
+ function resolvePassingThreshold(passingScore, maxScore) {
79
+ return passingScore ?? maxScore;
80
+ }
81
+ function meetsPassingThreshold(score, maxScore, passingScore) {
82
+ const threshold = resolvePassingThreshold(passingScore, maxScore);
83
+ return score >= threshold;
84
+ }
85
+ function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
86
+ const maxScore = custom?.maxScore ?? fallbackMax;
87
+ if (custom?.passed !== void 0) {
88
+ const score2 = custom.passed ? custom.score ?? maxScore : custom.score ?? 0;
89
+ return { score: score2, maxScore, passed: custom.passed };
90
+ }
91
+ if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
92
+ const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
93
+ return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
94
+ }
95
+ const score = fallbackCorrect ? maxScore : 0;
96
+ const passed = meetsPassingThreshold(score, maxScore, passingScore);
97
+ return { score, maxScore, passed };
98
+ }
99
+
67
100
  // src/context.tsx
68
101
  var import_react2 = require("react");
69
102
 
@@ -269,7 +302,10 @@ async function disposeTrackingClient(client) {
269
302
  }
270
303
 
271
304
  // src/provider/useLessonkitProviderRuntime.ts
272
- var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
305
+ var useIsoLayoutEffect = (
306
+ /* v8 ignore next -- SSR uses useEffect when window is unavailable */
307
+ typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect
308
+ );
273
309
  var defaultStorage = (0, import_core3.createSessionStoragePort)();
274
310
  var courseStartedTrackingFlightKey = null;
275
311
  function isTrackingActive(tracking) {
@@ -342,22 +378,15 @@ async function emitCourseStartedPipelineOnly(opts) {
342
378
  async function emitCourseStarted(opts) {
343
379
  const event = buildCourseStartedEvent(opts);
344
380
  if (event === null) return "filtered";
345
- const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
381
+ const tracked = await emitCourseStartedToTracking(
382
+ opts.tracking,
346
383
  opts.storage,
347
384
  opts.sessionId,
348
- opts.courseId
385
+ opts.courseId,
386
+ event,
387
+ opts.shouldCommit
349
388
  );
350
- if (!trackingAlreadyEmitted) {
351
- const tracked = await emitCourseStartedToTracking(
352
- opts.tracking,
353
- opts.storage,
354
- opts.sessionId,
355
- opts.courseId,
356
- event,
357
- opts.shouldCommit
358
- );
359
- if (!tracked) return "failed";
360
- }
389
+ if (!tracked) return "failed";
361
390
  return emitCourseStartedPipelineOnly({
362
391
  ...opts,
363
392
  event,
@@ -369,22 +398,15 @@ async function emitCourseStarted(opts) {
369
398
  async function emitCourseStartedToTrackingOnly(opts) {
370
399
  const event = buildCourseStartedEvent(opts);
371
400
  if (event === null) return "filtered";
372
- const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
401
+ const tracked = await emitCourseStartedToTracking(
402
+ opts.tracking,
373
403
  opts.storage,
374
404
  opts.sessionId,
375
- opts.courseId
405
+ opts.courseId,
406
+ event,
407
+ opts.shouldCommit
376
408
  );
377
- if (!trackingAlreadyEmitted) {
378
- const tracked = await emitCourseStartedToTracking(
379
- opts.tracking,
380
- opts.storage,
381
- opts.sessionId,
382
- opts.courseId,
383
- event,
384
- opts.shouldCommit
385
- );
386
- if (!tracked) return "failed";
387
- }
409
+ if (!tracked) return "failed";
388
410
  try {
389
411
  if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
390
412
  await emitCourseStartedNonTrackingPipeline({
@@ -423,6 +445,9 @@ async function emitPendingCourseStarted(opts) {
423
445
  opts.sessionId,
424
446
  opts.courseId
425
447
  );
448
+ if (sessionStarted && trackingEmitted && pipelineDelivered) {
449
+ return "emitted";
450
+ }
426
451
  if (sessionStarted && trackingEmitted && !pipelineDelivered) {
427
452
  const event = buildCourseStartedEvent(opts);
428
453
  if (event === null) return "filtered";
@@ -626,7 +651,10 @@ function useLessonkitProviderRuntime(config) {
626
651
  const baseSink = normalizedConfig.tracking?.sink;
627
652
  const userBatchSink = normalizedConfig.tracking?.batchSink;
628
653
  assertTrackingSinkConfig(normalizedConfig.tracking);
629
- const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
654
+ const sink = pluginHostRef.current && baseSink ? (
655
+ /* v8 ignore next -- composeTrackingSink may return null; fall back to base sink */
656
+ pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink
657
+ ) : baseSink;
630
658
  const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
631
659
  const host = pluginHostRef.current;
632
660
  const ctx = buildCurrentPluginCtx();
@@ -963,9 +991,29 @@ function LessonkitProvider(props) {
963
991
  }
964
992
 
965
993
  // src/hooks.ts
994
+ var import_react4 = require("react");
995
+
996
+ // src/assessment/useAssessmentState.ts
966
997
  var import_react3 = require("react");
998
+ function useAssessmentState(enclosingLessonId) {
999
+ const { track } = useLessonkit();
1000
+ const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
1001
+ return (0, import_react3.useMemo)(
1002
+ () => ({
1003
+ answer: (data) => {
1004
+ track("assessment_answered", data, trackOpts);
1005
+ },
1006
+ complete: (data) => {
1007
+ track("assessment_completed", data, trackOpts);
1008
+ }
1009
+ }),
1010
+ [track, enclosingLessonId]
1011
+ );
1012
+ }
1013
+
1014
+ // src/hooks.ts
967
1015
  function useLessonkit() {
968
- const ctx = (0, import_react3.useContext)(LessonkitContext);
1016
+ const ctx = (0, import_react4.useContext)(LessonkitContext);
969
1017
  if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
970
1018
  return ctx;
971
1019
  }
@@ -975,16 +1023,16 @@ function useProgress() {
975
1023
  }
976
1024
  function useTracking() {
977
1025
  const { track } = useLessonkit();
978
- return (0, import_react3.useMemo)(() => ({ track }), [track]);
1026
+ return (0, import_react4.useMemo)(() => ({ track }), [track]);
979
1027
  }
980
1028
  function useCompletion() {
981
1029
  const { completeLesson, completeCourse } = useLessonkit();
982
- return (0, import_react3.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
1030
+ return (0, import_react4.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
983
1031
  }
984
1032
  function useQuizState(enclosingLessonId) {
985
1033
  const { track } = useLessonkit();
986
1034
  const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
987
- return (0, import_react3.useMemo)(
1035
+ return (0, import_react4.useMemo)(
988
1036
  () => ({
989
1037
  answer: (opts) => {
990
1038
  track("quiz_answered", opts, trackOpts);
@@ -998,10 +1046,10 @@ function useQuizState(enclosingLessonId) {
998
1046
  }
999
1047
 
1000
1048
  // src/lessonContext.tsx
1001
- var import_react4 = require("react");
1002
- var LessonContext = (0, import_react4.createContext)(void 0);
1049
+ var import_react5 = require("react");
1050
+ var LessonContext = (0, import_react5.createContext)(void 0);
1003
1051
  function useEnclosingLessonId() {
1004
- return (0, import_react4.useContext)(LessonContext);
1052
+ return (0, import_react5.useContext)(LessonContext);
1005
1053
  }
1006
1054
 
1007
1055
  // src/runtime/validateComponentId.ts
@@ -1049,8 +1097,8 @@ function resetQuizWarningsForTests() {
1049
1097
  warnedQuizOutsideLesson = false;
1050
1098
  }
1051
1099
  function Course(props) {
1052
- const courseId = (0, import_react5.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1053
- const providerConfig = (0, import_react5.useMemo)(
1100
+ const courseId = (0, import_react6.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
1101
+ const providerConfig = (0, import_react6.useMemo)(
1054
1102
  () => ({ ...props.config, courseId }),
1055
1103
  [props.config, courseId]
1056
1104
  );
@@ -1060,14 +1108,14 @@ function Course(props) {
1060
1108
  ] }) });
1061
1109
  }
1062
1110
  function Lesson(props) {
1063
- const lessonId = (0, import_react5.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1111
+ const lessonId = (0, import_react6.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
1064
1112
  const autoComplete = props.autoCompleteOnUnmount !== false;
1065
1113
  const { setActiveLesson, config } = useLessonkit();
1066
1114
  const { completeLesson } = useCompletion();
1067
- const lessonMountGenerationRef = (0, import_react5.useRef)(0);
1068
- const liveCourseIdRef = (0, import_react5.useRef)(config.courseId);
1115
+ const lessonMountGenerationRef = (0, import_react6.useRef)(0);
1116
+ const liveCourseIdRef = (0, import_react6.useRef)(config.courseId);
1069
1117
  liveCourseIdRef.current = config.courseId;
1070
- (0, import_react5.useEffect)(() => {
1118
+ (0, import_react6.useEffect)(() => {
1071
1119
  const unregister = registerLessonMount(lessonId);
1072
1120
  const generation = ++lessonMountGenerationRef.current;
1073
1121
  const mountedCourseId = config.courseId;
@@ -1098,20 +1146,20 @@ function Lesson(props) {
1098
1146
  ] }) });
1099
1147
  }
1100
1148
  function Scenario(props) {
1101
- const blockId = (0, import_react5.useMemo)(
1149
+ const blockId = (0, import_react6.useMemo)(
1102
1150
  () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1103
1151
  [props.blockId]
1104
1152
  );
1105
1153
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
1106
1154
  }
1107
1155
  function Reflection(props) {
1108
- const blockId = (0, import_react5.useMemo)(
1156
+ const blockId = (0, import_react6.useMemo)(
1109
1157
  () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
1110
1158
  [props.blockId]
1111
1159
  );
1112
- const promptId = (0, import_react5.useId)();
1113
- const hintId = (0, import_react5.useId)();
1114
- const [internalValue, setInternalValue] = (0, import_react5.useState)("");
1160
+ const promptId = (0, import_react6.useId)();
1161
+ const hintId = (0, import_react6.useId)();
1162
+ const [internalValue, setInternalValue] = (0, import_react6.useState)("");
1115
1163
  const isControlled = props.value !== void 0;
1116
1164
  const value = isControlled ? props.value : internalValue;
1117
1165
  const handleChange = (event) => {
@@ -1149,7 +1197,7 @@ function KnowledgeCheck(props) {
1149
1197
  function Quiz(props) {
1150
1198
  const enclosingLessonId = useEnclosingLessonId();
1151
1199
  const missingLesson = enclosingLessonId === void 0;
1152
- (0, import_react5.useEffect)(() => {
1200
+ (0, import_react6.useEffect)(() => {
1153
1201
  if (!missingLesson || isDevEnvironment4()) return;
1154
1202
  if (!warnedQuizOutsideLesson) {
1155
1203
  warnedQuizOutsideLesson = true;
@@ -1168,16 +1216,16 @@ function Quiz(props) {
1168
1216
  }
1169
1217
  function QuizInner(props) {
1170
1218
  const { enclosingLessonId } = props;
1171
- const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1219
+ const checkId = (0, import_react6.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1172
1220
  const quiz = useQuizState(enclosingLessonId);
1173
1221
  const { plugins, config, session } = useLessonkit();
1174
- const [selected, setSelected] = (0, import_react5.useState)(null);
1175
- const [selectionCorrect, setSelectionCorrect] = (0, import_react5.useState)(null);
1176
- const [quizPassed, setQuizPassed] = (0, import_react5.useState)(false);
1177
- const completedRef = (0, import_react5.useRef)(false);
1178
- const questionId = (0, import_react5.useId)();
1222
+ const [selected, setSelected] = (0, import_react6.useState)(null);
1223
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react6.useState)(null);
1224
+ const [quizPassed, setQuizPassed] = (0, import_react6.useState)(false);
1225
+ const completedRef = (0, import_react6.useRef)(false);
1226
+ const questionId = (0, import_react6.useId)();
1179
1227
  const choicesKey = props.choices.join("\0");
1180
- (0, import_react5.useEffect)(() => {
1228
+ (0, import_react6.useEffect)(() => {
1181
1229
  completedRef.current = false;
1182
1230
  setQuizPassed(false);
1183
1231
  setSelected(null);
@@ -1186,8 +1234,8 @@ function QuizInner(props) {
1186
1234
  const isChoiceCorrect = (choice, custom) => {
1187
1235
  if (!custom) return choice === props.answer;
1188
1236
  if (custom.passed !== void 0) return custom.passed;
1189
- if (custom.maxScore != null && custom.maxScore > 0) {
1190
- return custom.score / custom.maxScore >= 1;
1237
+ if (custom.maxScore != null && custom.maxScore > 0 && custom.score != null) {
1238
+ return meetsPassingThreshold(custom.score, custom.maxScore, props.passingScore);
1191
1239
  }
1192
1240
  return choice === props.answer;
1193
1241
  };
@@ -1237,7 +1285,7 @@ function QuizInner(props) {
1237
1285
  const maxScore = custom?.maxScore ?? 1;
1238
1286
  quiz.complete({
1239
1287
  checkId,
1240
- score: custom?.score ?? 1,
1288
+ score: custom?.score ?? maxScore,
1241
1289
  maxScore,
1242
1290
  passingScore: props.passingScore ?? maxScore
1243
1291
  });
@@ -1280,11 +1328,864 @@ function ProgressTracker(props) {
1280
1328
  ] }) });
1281
1329
  }
1282
1330
 
1331
+ // src/blocks/TrueFalse.tsx
1332
+ var import_react9 = __toESM(require("react"), 1);
1333
+
1334
+ // src/assessment/AssessmentLessonGuard.tsx
1335
+ var import_react7 = require("react");
1336
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1337
+ var warnedAssessmentOutsideLesson = false;
1338
+ function resetAssessmentWarningsForTests() {
1339
+ warnedAssessmentOutsideLesson = false;
1340
+ }
1341
+ function AssessmentLessonGuard(props) {
1342
+ const enclosingLessonId = useEnclosingLessonId();
1343
+ const missingLesson = enclosingLessonId === void 0;
1344
+ (0, import_react7.useEffect)(() => {
1345
+ if (!missingLesson || isDevEnvironment4()) return;
1346
+ if (!warnedAssessmentOutsideLesson) {
1347
+ warnedAssessmentOutsideLesson = true;
1348
+ console.error(
1349
+ `[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>; assessment telemetry will not be emitted.`
1350
+ );
1351
+ }
1352
+ }, [missingLesson, props.blockLabel]);
1353
+ if (missingLesson && isDevEnvironment4()) {
1354
+ throw new Error(`[lessonkit] <${props.blockLabel}> must be wrapped in <Lesson>`);
1355
+ }
1356
+ if (missingLesson) {
1357
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("section", { role: "alert", "aria-label": `${props.blockLabel} configuration error`, "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { children: [
1358
+ props.blockLabel,
1359
+ " must be placed inside a Lesson."
1360
+ ] }) });
1361
+ }
1362
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: props.children(enclosingLessonId) });
1363
+ }
1364
+
1365
+ // src/assessment/AssessmentSequenceContext.tsx
1366
+ var import_react8 = __toESM(require("react"), 1);
1367
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1368
+ var AssessmentSequenceContext = (0, import_react8.createContext)(null);
1369
+ function AssessmentSequenceProvider({ children }) {
1370
+ const registryRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
1371
+ const register = (0, import_react8.useCallback)((checkId, handle) => {
1372
+ registryRef.current.set(checkId, handle);
1373
+ return () => {
1374
+ registryRef.current.delete(checkId);
1375
+ };
1376
+ }, []);
1377
+ const value = (0, import_react8.useMemo)(
1378
+ () => ({
1379
+ register,
1380
+ getHandles: () => registryRef.current
1381
+ }),
1382
+ [register]
1383
+ );
1384
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AssessmentSequenceContext.Provider, { value, children });
1385
+ }
1386
+ function useAssessmentSequenceRegistry() {
1387
+ return (0, import_react8.useContext)(AssessmentSequenceContext);
1388
+ }
1389
+ function useRegisterAssessmentHandle(checkId, handle) {
1390
+ const ctx = useAssessmentSequenceRegistry();
1391
+ import_react8.default.useEffect(() => {
1392
+ if (!ctx || !handle) return;
1393
+ return ctx.register(checkId, handle);
1394
+ }, [ctx, checkId, handle]);
1395
+ }
1396
+
1397
+ // src/blocks/TrueFalse.tsx
1398
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1399
+ var INTERACTION = "trueFalse";
1400
+ function TrueFalseInner(props, ref) {
1401
+ const { enclosingLessonId } = props;
1402
+ const checkId = (0, import_react9.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1403
+ const assessment = useAssessmentState(enclosingLessonId);
1404
+ const { plugins, config, session } = useLessonkit();
1405
+ const [selected, setSelected] = (0, import_react9.useState)(null);
1406
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react9.useState)(null);
1407
+ const [showSolutions, setShowSolutions] = (0, import_react9.useState)(false);
1408
+ const [passed, setPassed] = (0, import_react9.useState)(false);
1409
+ const completedRef = (0, import_react9.useRef)(false);
1410
+ const questionId = import_react9.default.useId();
1411
+ const reset = () => {
1412
+ completedRef.current = false;
1413
+ setPassed(false);
1414
+ setSelected(null);
1415
+ setSelectionCorrect(null);
1416
+ setShowSolutions(false);
1417
+ };
1418
+ (0, import_react9.useEffect)(() => {
1419
+ reset();
1420
+ }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId]);
1421
+ const handle = (0, import_react9.useMemo)(() => {
1422
+ const maxScore = 1;
1423
+ const score = passed ? maxScore : selected === null ? 0 : selected === props.answer ? maxScore : 0;
1424
+ return {
1425
+ getScore: () => score,
1426
+ getMaxScore: () => maxScore,
1427
+ getAnswerGiven: () => selected !== null,
1428
+ resetTask: reset,
1429
+ showSolutions: () => setShowSolutions(true),
1430
+ getXAPIData: () => ({
1431
+ checkId,
1432
+ interactionType: INTERACTION,
1433
+ response: selected ?? void 0,
1434
+ correct: selected === props.answer,
1435
+ score,
1436
+ maxScore
1437
+ })
1438
+ };
1439
+ }, [checkId, passed, props.answer, selected]);
1440
+ (0, import_react9.useImperativeHandle)(ref, () => handle, [handle]);
1441
+ useRegisterAssessmentHandle(checkId, handle);
1442
+ const submit = (value) => {
1443
+ if (passed && !props.enableRetry) return;
1444
+ setSelected(value);
1445
+ const pluginCtx = buildPluginContext({
1446
+ courseId: config.courseId,
1447
+ sessionId: session.sessionId,
1448
+ attemptId: session.attemptId,
1449
+ user: session.user
1450
+ });
1451
+ const custom = plugins?.scoreAssessment(
1452
+ { checkId, lessonId: enclosingLessonId, response: value },
1453
+ pluginCtx
1454
+ ) ?? null;
1455
+ const correct = value === props.answer;
1456
+ const scored = scoreFromCustom(custom, correct, 1, props.passingScore);
1457
+ setSelectionCorrect(scored.passed);
1458
+ assessment.answer({
1459
+ checkId,
1460
+ interactionType: INTERACTION,
1461
+ question: props.question,
1462
+ response: value,
1463
+ correct: scored.passed
1464
+ });
1465
+ if (scored.passed && !completedRef.current) {
1466
+ completedRef.current = true;
1467
+ setPassed(true);
1468
+ assessment.complete({
1469
+ checkId,
1470
+ interactionType: INTERACTION,
1471
+ score: scored.score,
1472
+ maxScore: scored.maxScore,
1473
+ passingScore: props.passingScore ?? scored.maxScore
1474
+ });
1475
+ }
1476
+ };
1477
+ const reveal = showSolutions || passed && props.enableSolutionsButton;
1478
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": "True or False", "data-lk-check-id": checkId, children: [
1479
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: questionId, children: props.question }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
1481
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("legend", { className: "lk-visually-hidden", children: "True or False" }),
1482
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "block", marginRight: "1rem" }, children: [
1483
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1484
+ "input",
1485
+ {
1486
+ type: "radio",
1487
+ name: `${questionId}-tf`,
1488
+ checked: selected === true,
1489
+ disabled: passed && !props.enableRetry,
1490
+ onChange: () => submit(true)
1491
+ }
1492
+ ),
1493
+ "True"
1494
+ ] }),
1495
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "block" }, children: [
1496
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1497
+ "input",
1498
+ {
1499
+ type: "radio",
1500
+ name: `${questionId}-tf`,
1501
+ checked: selected === false,
1502
+ disabled: passed && !props.enableRetry,
1503
+ onChange: () => submit(false)
1504
+ }
1505
+ ),
1506
+ "False"
1507
+ ] })
1508
+ ] }),
1509
+ reveal ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { children: [
1510
+ "Correct answer: ",
1511
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: props.answer ? "True" : "False" })
1512
+ ] }) : null,
1513
+ selected !== null && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null,
1514
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1515
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1516
+ ] });
1517
+ }
1518
+ var TrueFalseInnerForwarded = (0, import_react9.forwardRef)(TrueFalseInner);
1519
+ var TrueFalse = (0, import_react9.forwardRef)(function TrueFalse2(props, ref) {
1520
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AssessmentLessonGuard, { blockLabel: "TrueFalse", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TrueFalseInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1521
+ });
1522
+
1523
+ // src/blocks/MarkTheWords.tsx
1524
+ var import_react10 = __toESM(require("react"), 1);
1525
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1526
+ var INTERACTION2 = "markTheWords";
1527
+ function tokenize(text) {
1528
+ return text.split(/(\s+)/).filter((t) => t.length > 0);
1529
+ }
1530
+ function MarkTheWordsInner(props, ref) {
1531
+ const checkId = (0, import_react10.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1532
+ const assessment = useAssessmentState(props.enclosingLessonId);
1533
+ const tokens = (0, import_react10.useMemo)(() => tokenize(props.text), [props.text]);
1534
+ const correctSet = (0, import_react10.useMemo)(
1535
+ () => new Set(props.correctWords.map((w) => w.toLowerCase())),
1536
+ [props.correctWords]
1537
+ );
1538
+ const [marked, setMarked] = (0, import_react10.useState)(() => /* @__PURE__ */ new Set());
1539
+ const [passed, setPassed] = (0, import_react10.useState)(false);
1540
+ const [showSolutions, setShowSolutions] = (0, import_react10.useState)(false);
1541
+ const completedRef = (0, import_react10.useRef)(false);
1542
+ const reset = () => {
1543
+ completedRef.current = false;
1544
+ setPassed(false);
1545
+ setMarked(/* @__PURE__ */ new Set());
1546
+ setShowSolutions(false);
1547
+ };
1548
+ (0, import_react10.useEffect)(() => {
1549
+ reset();
1550
+ }, [checkId, props.text, props.correctWords.join("\0")]);
1551
+ const selectableIndices = (0, import_react10.useMemo)(() => {
1552
+ const indices = [];
1553
+ tokens.forEach((t, i) => {
1554
+ if (!/^\s+$/.test(t) && correctSet.has(t.toLowerCase())) indices.push(i);
1555
+ });
1556
+ return indices;
1557
+ }, [tokens, correctSet]);
1558
+ const hasTargets = selectableIndices.length > 0;
1559
+ const allMarked = hasTargets && selectableIndices.every((i) => marked.has(i));
1560
+ const maxScore = selectableIndices.length;
1561
+ const score = allMarked ? maxScore : marked.size;
1562
+ const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1563
+ const handle = (0, import_react10.useMemo)(() => {
1564
+ const handleMax = maxScore || 1;
1565
+ return {
1566
+ getScore: () => score,
1567
+ getMaxScore: () => handleMax,
1568
+ getAnswerGiven: () => marked.size > 0,
1569
+ resetTask: reset,
1570
+ showSolutions: () => setShowSolutions(true),
1571
+ getXAPIData: () => ({
1572
+ checkId,
1573
+ interactionType: INTERACTION2,
1574
+ response: [...marked].map((i) => tokens[i]),
1575
+ correct: passedThreshold,
1576
+ score,
1577
+ maxScore: handleMax
1578
+ })
1579
+ };
1580
+ }, [checkId, marked, maxScore, passedThreshold, score, tokens]);
1581
+ (0, import_react10.useImperativeHandle)(ref, () => handle, [handle]);
1582
+ useRegisterAssessmentHandle(checkId, handle);
1583
+ const toggle = (index) => {
1584
+ if (passed && !props.enableRetry) return;
1585
+ setMarked((prev) => {
1586
+ const next = new Set(prev);
1587
+ if (next.has(index)) next.delete(index);
1588
+ else next.add(index);
1589
+ return next;
1590
+ });
1591
+ };
1592
+ (0, import_react10.useEffect)(() => {
1593
+ if (!hasTargets) {
1594
+ if (isDevEnvironment4()) {
1595
+ console.warn(
1596
+ "[lessonkit] MarkTheWords: no tokens match correctWords",
1597
+ props.correctWords
1598
+ );
1599
+ }
1600
+ return;
1601
+ }
1602
+ if (!passedThreshold || completedRef.current) return;
1603
+ completedRef.current = true;
1604
+ setPassed(true);
1605
+ assessment.answer({
1606
+ checkId,
1607
+ interactionType: INTERACTION2,
1608
+ question: props.text,
1609
+ response: [...marked].map((i) => tokens[i]),
1610
+ correct: true
1611
+ });
1612
+ assessment.complete({
1613
+ checkId,
1614
+ interactionType: INTERACTION2,
1615
+ score,
1616
+ maxScore,
1617
+ passingScore: props.passingScore ?? maxScore
1618
+ });
1619
+ }, [
1620
+ assessment,
1621
+ checkId,
1622
+ hasTargets,
1623
+ marked,
1624
+ maxScore,
1625
+ passedThreshold,
1626
+ props.passingScore,
1627
+ props.correctWords,
1628
+ props.text,
1629
+ score,
1630
+ tokens
1631
+ ]);
1632
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { "aria-label": "Mark the Words", "data-lk-check-id": checkId, children: [
1633
+ !hasTargets ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { role: "alert", children: [
1634
+ "No words in this sentence match ",
1635
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { children: "correctWords" }),
1636
+ ". Check spelling and capitalization in the source text."
1637
+ ] }) : null,
1638
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { id: `${checkId}-hint`, children: "Select the correct words in the sentence." }),
1639
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { "aria-describedby": `${checkId}-hint`, children: tokens.map((token, i) => {
1640
+ const isWord = !/^\s+$/.test(token);
1641
+ const isTarget = isWord && correctSet.has(token.toLowerCase());
1642
+ if (!isTarget) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react10.default.Fragment, { children: token }, i);
1643
+ const selected = marked.has(i);
1644
+ const solution = showSolutions || passed && props.enableSolutionsButton;
1645
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1646
+ "button",
1647
+ {
1648
+ type: "button",
1649
+ "data-testid": `mark-word-${i}`,
1650
+ "aria-pressed": selected,
1651
+ disabled: passed && !props.enableRetry,
1652
+ onClick: () => toggle(i),
1653
+ style: {
1654
+ margin: "0 0.1em",
1655
+ textDecoration: solution ? "underline" : void 0,
1656
+ fontWeight: selected || solution ? "bold" : void 0
1657
+ },
1658
+ children: token
1659
+ },
1660
+ i
1661
+ );
1662
+ }) }),
1663
+ allMarked ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { role: "status", "aria-live": "polite", children: "Correct" }) : null,
1664
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1665
+ props.enableSolutionsButton && !showSolutions ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1666
+ ] });
1667
+ }
1668
+ var MarkTheWordsInnerForwarded = (0, import_react10.forwardRef)(MarkTheWordsInner);
1669
+ var MarkTheWords = (0, import_react10.forwardRef)(function MarkTheWords2(props, ref) {
1670
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AssessmentLessonGuard, { blockLabel: "MarkTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MarkTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1671
+ });
1672
+
1673
+ // src/blocks/FillInTheBlanks.tsx
1674
+ var import_react11 = __toESM(require("react"), 1);
1675
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1676
+ var INTERACTION3 = "fillInBlanks";
1677
+ function parseTemplate(template) {
1678
+ const parts = [];
1679
+ const blanks = [];
1680
+ const re = /\*([^*]+)\*/g;
1681
+ let last = 0;
1682
+ let match;
1683
+ let n = 0;
1684
+ while ((match = re.exec(template)) !== null) {
1685
+ parts.push(template.slice(last, match.index));
1686
+ const id = `blank-${n++}`;
1687
+ blanks.push({ id, answer: match[1].trim() });
1688
+ parts.push(id);
1689
+ last = match.index + match[0].length;
1690
+ }
1691
+ parts.push(template.slice(last));
1692
+ return { parts, blanks };
1693
+ }
1694
+ function FillInTheBlanksInner(props, ref) {
1695
+ const checkId = (0, import_react11.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1696
+ const assessment = useAssessmentState(props.enclosingLessonId);
1697
+ const parsed = (0, import_react11.useMemo)(() => parseTemplate(props.template), [props.template]);
1698
+ const blanks = props.blanks ?? parsed.blanks;
1699
+ const [values, setValues] = (0, import_react11.useState)(
1700
+ () => Object.fromEntries(blanks.map((b) => [b.id, ""]))
1701
+ );
1702
+ const [passed, setPassed] = (0, import_react11.useState)(false);
1703
+ const [showSolutions, setShowSolutions] = (0, import_react11.useState)(false);
1704
+ const completedRef = (0, import_react11.useRef)(false);
1705
+ const answeredRef = (0, import_react11.useRef)(false);
1706
+ const reset = () => {
1707
+ completedRef.current = false;
1708
+ answeredRef.current = false;
1709
+ setPassed(false);
1710
+ setValues(Object.fromEntries(blanks.map((b) => [b.id, ""])));
1711
+ setShowSolutions(false);
1712
+ };
1713
+ (0, import_react11.useEffect)(() => {
1714
+ reset();
1715
+ }, [checkId, props.template, blanks.map((b) => b.answer).join("\0")]);
1716
+ const hasBlanks = blanks.length > 0;
1717
+ const allFilled = hasBlanks && blanks.every((b) => (values[b.id] ?? "").trim().length > 0);
1718
+ let score = 0;
1719
+ blanks.forEach((b) => {
1720
+ if ((values[b.id] ?? "").trim().toLowerCase() === b.answer.toLowerCase()) score += 1;
1721
+ });
1722
+ const maxScore = blanks.length;
1723
+ const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1724
+ const handle = (0, import_react11.useMemo)(() => {
1725
+ const handleMax = maxScore || 1;
1726
+ return {
1727
+ getScore: () => score,
1728
+ getMaxScore: () => handleMax,
1729
+ getAnswerGiven: () => allFilled,
1730
+ resetTask: reset,
1731
+ showSolutions: () => setShowSolutions(true),
1732
+ getXAPIData: () => ({
1733
+ checkId,
1734
+ interactionType: INTERACTION3,
1735
+ response: values,
1736
+ correct: passedThreshold,
1737
+ score,
1738
+ maxScore: handleMax
1739
+ })
1740
+ };
1741
+ }, [allFilled, blanks.length, checkId, maxScore, passedThreshold, score, values]);
1742
+ (0, import_react11.useImperativeHandle)(ref, () => handle, [handle]);
1743
+ useRegisterAssessmentHandle(checkId, handle);
1744
+ const check = () => {
1745
+ if (!hasBlanks) {
1746
+ if (isDevEnvironment4()) {
1747
+ console.warn("[lessonkit] FillInTheBlanks has no blanks in template");
1748
+ }
1749
+ return;
1750
+ }
1751
+ if (!allFilled) return;
1752
+ if (!answeredRef.current) {
1753
+ answeredRef.current = true;
1754
+ assessment.answer({
1755
+ checkId,
1756
+ interactionType: INTERACTION3,
1757
+ question: props.template,
1758
+ response: values,
1759
+ correct: passedThreshold
1760
+ });
1761
+ }
1762
+ if (passedThreshold && !completedRef.current) {
1763
+ completedRef.current = true;
1764
+ setPassed(true);
1765
+ assessment.complete({
1766
+ checkId,
1767
+ interactionType: INTERACTION3,
1768
+ score,
1769
+ maxScore,
1770
+ passingScore: props.passingScore ?? maxScore
1771
+ });
1772
+ }
1773
+ };
1774
+ (0, import_react11.useEffect)(() => {
1775
+ if (!allFilled) answeredRef.current = false;
1776
+ }, [allFilled]);
1777
+ (0, import_react11.useEffect)(() => {
1778
+ if (props.autoCheck && allFilled) check();
1779
+ }, [allFilled, props.autoCheck, values, passedThreshold]);
1780
+ const reveal = showSolutions || passed && props.enableSolutionsButton;
1781
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "aria-label": "Fill in the Blanks", "data-lk-check-id": checkId, children: [
1782
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: parsed.parts.map((part, i) => {
1783
+ const blank = blanks.find((b) => b.id === part);
1784
+ if (!blank) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react11.default.Fragment, { children: part }, i);
1785
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { margin: "0 0.25em" }, children: [
1786
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "lk-visually-hidden", children: blank.answer }),
1787
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1788
+ "input",
1789
+ {
1790
+ type: "text",
1791
+ "data-testid": `blank-${blank.id}`,
1792
+ "aria-label": `Blank ${blank.id}`,
1793
+ value: reveal ? blank.answer : values[blank.id] ?? "",
1794
+ readOnly: reveal,
1795
+ disabled: passed && !props.enableRetry,
1796
+ onChange: (e) => setValues((v) => ({ ...v, [blank.id]: e.target.value })),
1797
+ onBlur: () => props.autoCheck && check(),
1798
+ size: Math.max(8, blank.answer.length + 2)
1799
+ }
1800
+ )
1801
+ ] }, blank.id);
1802
+ }) }),
1803
+ !props.autoCheck ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", "data-testid": "check-blanks", disabled: !allFilled || passed, onClick: check, children: "Check" }) : null,
1804
+ !hasBlanks ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", children: "This activity has no blanks. Add text wrapped in asterisks, e.g. The *answer* here." }) : null,
1805
+ allFilled ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null,
1806
+ props.enableRetry && passed ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: reset, children: "Try again" }) : null,
1807
+ props.enableSolutionsButton && !reveal ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => setShowSolutions(true), children: "Show solution" }) : null
1808
+ ] });
1809
+ }
1810
+ var FillInTheBlanksInnerForwarded = (0, import_react11.forwardRef)(FillInTheBlanksInner);
1811
+ var FillInTheBlanks = (0, import_react11.forwardRef)(
1812
+ function FillInTheBlanks2(props, ref) {
1813
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssessmentLessonGuard, { blockLabel: "FillInTheBlanks", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FillInTheBlanksInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1814
+ }
1815
+ );
1816
+
1817
+ // src/blocks/DragTheWords.tsx
1818
+ var import_react12 = __toESM(require("react"), 1);
1819
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1820
+ var INTERACTION4 = "dragTheWords";
1821
+ function parseZones(template) {
1822
+ const parts = [];
1823
+ const answers = [];
1824
+ const re = /\*([^*]+)\*/g;
1825
+ let last = 0;
1826
+ let match;
1827
+ let n = 0;
1828
+ while ((match = re.exec(template)) !== null) {
1829
+ parts.push(template.slice(last, match.index));
1830
+ answers.push(match[1].trim());
1831
+ parts.push(`zone-${n++}`);
1832
+ last = match.index + match[0].length;
1833
+ }
1834
+ parts.push(template.slice(last));
1835
+ return { parts, answers };
1836
+ }
1837
+ function DragTheWordsInner(props, ref) {
1838
+ const checkId = (0, import_react12.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1839
+ const assessment = useAssessmentState(props.enclosingLessonId);
1840
+ const { parts, answers } = (0, import_react12.useMemo)(() => parseZones(props.template), [props.template]);
1841
+ const [zones, setZones] = (0, import_react12.useState)(
1842
+ () => Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""]))
1843
+ );
1844
+ const [pool, setPool] = (0, import_react12.useState)(() => [...props.words]);
1845
+ const [keyboardWord, setKeyboardWord] = (0, import_react12.useState)(null);
1846
+ const [passed, setPassed] = (0, import_react12.useState)(false);
1847
+ const completedRef = (0, import_react12.useRef)(false);
1848
+ const answeredRef = (0, import_react12.useRef)(false);
1849
+ const reset = () => {
1850
+ completedRef.current = false;
1851
+ answeredRef.current = false;
1852
+ setPassed(false);
1853
+ setZones(Object.fromEntries(answers.map((_, i) => [`zone-${i}`, ""])));
1854
+ setPool([...props.words]);
1855
+ setKeyboardWord(null);
1856
+ };
1857
+ (0, import_react12.useEffect)(() => {
1858
+ reset();
1859
+ }, [checkId, props.template, props.words.join("\0")]);
1860
+ const hasZones = answers.length > 0;
1861
+ const allFilled = hasZones && answers.every((_, i) => (zones[`zone-${i}`] ?? "").length > 0);
1862
+ let score = 0;
1863
+ answers.forEach((ans, i) => {
1864
+ if ((zones[`zone-${i}`] ?? "").trim().toLowerCase() === ans.toLowerCase()) score += 1;
1865
+ });
1866
+ const maxScore = answers.length;
1867
+ const passedThreshold = meetsPassingThreshold(score, maxScore || 1, props.passingScore);
1868
+ const handle = (0, import_react12.useMemo)(() => {
1869
+ const handleMax = maxScore || 1;
1870
+ return {
1871
+ getScore: () => score,
1872
+ getMaxScore: () => handleMax,
1873
+ getAnswerGiven: () => allFilled,
1874
+ resetTask: reset,
1875
+ showSolutions: () => {
1876
+ },
1877
+ getXAPIData: () => ({
1878
+ checkId,
1879
+ interactionType: INTERACTION4,
1880
+ response: zones,
1881
+ correct: passedThreshold,
1882
+ score,
1883
+ maxScore: handleMax
1884
+ })
1885
+ };
1886
+ }, [allFilled, answers.length, checkId, maxScore, passedThreshold, score, zones]);
1887
+ (0, import_react12.useImperativeHandle)(ref, () => handle, [handle]);
1888
+ useRegisterAssessmentHandle(checkId, handle);
1889
+ const placeInZone = (zoneId, word) => {
1890
+ if (passed && !props.enableRetry) return;
1891
+ const prev = zones[zoneId];
1892
+ setZones((z) => ({ ...z, [zoneId]: word }));
1893
+ setPool((p) => {
1894
+ const next = p.filter((w) => w !== word);
1895
+ if (prev) next.push(prev);
1896
+ return next;
1897
+ });
1898
+ setKeyboardWord(null);
1899
+ };
1900
+ const onDragStart = (word) => (e) => {
1901
+ e.dataTransfer.setData("text/plain", word);
1902
+ };
1903
+ const onDrop = (zoneId) => (e) => {
1904
+ e.preventDefault();
1905
+ const word = e.dataTransfer.getData("text/plain");
1906
+ if (word) placeInZone(zoneId, word);
1907
+ };
1908
+ const check = () => {
1909
+ if (!hasZones) {
1910
+ if (isDevEnvironment4()) {
1911
+ console.warn("[lessonkit] DragTheWords has no drop zones in template");
1912
+ }
1913
+ return;
1914
+ }
1915
+ if (!allFilled) return;
1916
+ if (!answeredRef.current) {
1917
+ answeredRef.current = true;
1918
+ assessment.answer({
1919
+ checkId,
1920
+ interactionType: INTERACTION4,
1921
+ question: props.template,
1922
+ response: zones,
1923
+ correct: passedThreshold
1924
+ });
1925
+ }
1926
+ if (passedThreshold && !completedRef.current) {
1927
+ completedRef.current = true;
1928
+ setPassed(true);
1929
+ assessment.complete({
1930
+ checkId,
1931
+ interactionType: INTERACTION4,
1932
+ score,
1933
+ maxScore,
1934
+ passingScore: props.passingScore ?? maxScore
1935
+ });
1936
+ }
1937
+ };
1938
+ (0, import_react12.useEffect)(() => {
1939
+ if (!allFilled) answeredRef.current = false;
1940
+ }, [allFilled]);
1941
+ (0, import_react12.useEffect)(() => {
1942
+ if (props.autoCheck && allFilled) check();
1943
+ }, [allFilled, props.autoCheck, zones, passedThreshold]);
1944
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { "aria-label": "Drag the Words", "data-lk-check-id": checkId, children: [
1945
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: "Drag words into the blanks (or select a word, then activate a blank)." }),
1946
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "list", "aria-label": "Word bank", "data-testid": "word-bank", children: pool.map((word) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1947
+ "button",
1948
+ {
1949
+ type: "button",
1950
+ draggable: true,
1951
+ "data-testid": `word-${word}`,
1952
+ "aria-pressed": keyboardWord === word,
1953
+ onDragStart: onDragStart(word),
1954
+ onClick: () => setKeyboardWord(keyboardWord === word ? null : word),
1955
+ style: { margin: "0.25rem" },
1956
+ children: word
1957
+ },
1958
+ word
1959
+ )) }),
1960
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: parts.map((part, i) => {
1961
+ if (!part.startsWith("zone-")) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react12.default.Fragment, { children: part }, i);
1962
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1963
+ "span",
1964
+ {
1965
+ role: "button",
1966
+ tabIndex: 0,
1967
+ "data-testid": part,
1968
+ onDragOver: (e) => e.preventDefault(),
1969
+ onDrop: onDrop(part),
1970
+ onClick: () => keyboardWord && placeInZone(part, keyboardWord),
1971
+ onKeyDown: (e) => {
1972
+ if (e.key === "Enter" && keyboardWord) placeInZone(part, keyboardWord);
1973
+ },
1974
+ style: {
1975
+ display: "inline-block",
1976
+ minWidth: "6em",
1977
+ border: "1px dashed currentColor",
1978
+ padding: "0.2em 0.5em",
1979
+ margin: "0 0.2em"
1980
+ },
1981
+ children: zones[part] || "___"
1982
+ },
1983
+ part
1984
+ );
1985
+ }) }),
1986
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", "data-testid": "check-drag-words", disabled: !allFilled || passed, onClick: check, children: "Check" }),
1987
+ !hasZones ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", children: "This activity has no drop zones. Wrap answers in asterisks in the template." }) : null,
1988
+ allFilled ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "status", "aria-live": "polite", children: passed || passedThreshold ? "Correct" : "Try again" }) : null
1989
+ ] });
1990
+ }
1991
+ var DragTheWordsInnerForwarded = (0, import_react12.forwardRef)(DragTheWordsInner);
1992
+ var DragTheWords = (0, import_react12.forwardRef)(function DragTheWords2(props, ref) {
1993
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AssessmentLessonGuard, { blockLabel: "DragTheWords", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DragTheWordsInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
1994
+ });
1995
+
1996
+ // src/blocks/DragAndDrop.tsx
1997
+ var import_react13 = require("react");
1998
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1999
+ var INTERACTION5 = "dragAndDrop";
2000
+ function DragAndDropInner(props, ref) {
2001
+ const checkId = (0, import_react13.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
2002
+ const assessment = useAssessmentState(props.enclosingLessonId);
2003
+ const [assignments, setAssignments] = (0, import_react13.useState)(
2004
+ () => Object.fromEntries(props.targets.map((t) => [t.id, ""]))
2005
+ );
2006
+ const [pool, setPool] = (0, import_react13.useState)(() => props.items.map((i) => i.id));
2007
+ const [keyboardItem, setKeyboardItem] = (0, import_react13.useState)(null);
2008
+ const [passed, setPassed] = (0, import_react13.useState)(false);
2009
+ const completedRef = (0, import_react13.useRef)(false);
2010
+ const reset = () => {
2011
+ completedRef.current = false;
2012
+ setPassed(false);
2013
+ setAssignments(Object.fromEntries(props.targets.map((t) => [t.id, ""])));
2014
+ setPool(props.items.map((i) => i.id));
2015
+ setKeyboardItem(null);
2016
+ };
2017
+ (0, import_react13.useEffect)(() => {
2018
+ reset();
2019
+ }, [checkId, props.items.map((i) => i.id).join(","), props.targets.map((t) => t.id).join(",")]);
2020
+ const allFilled = props.targets.every((t) => (assignments[t.id] ?? "").length > 0);
2021
+ const allCorrect = props.targets.every((t) => assignments[t.id] === t.accepts);
2022
+ const handle = (0, import_react13.useMemo)(() => {
2023
+ const maxScore = props.targets.length || 1;
2024
+ let score = 0;
2025
+ props.targets.forEach((t) => {
2026
+ if (assignments[t.id] === t.accepts) score += 1;
2027
+ });
2028
+ return {
2029
+ getScore: () => score,
2030
+ getMaxScore: () => maxScore,
2031
+ getAnswerGiven: () => allFilled,
2032
+ resetTask: reset,
2033
+ showSolutions: () => {
2034
+ },
2035
+ getXAPIData: () => ({
2036
+ checkId,
2037
+ interactionType: INTERACTION5,
2038
+ response: assignments,
2039
+ correct: allCorrect,
2040
+ score,
2041
+ maxScore
2042
+ })
2043
+ };
2044
+ }, [allCorrect, allFilled, assignments, checkId, props.targets]);
2045
+ (0, import_react13.useImperativeHandle)(ref, () => handle, [handle]);
2046
+ useRegisterAssessmentHandle(checkId, handle);
2047
+ const place = (targetId, itemId) => {
2048
+ if (passed && !props.enableRetry) return;
2049
+ const prev = assignments[targetId];
2050
+ setAssignments((a) => ({ ...a, [targetId]: itemId }));
2051
+ setPool((p) => {
2052
+ const next = p.filter((id) => id !== itemId);
2053
+ if (prev) next.push(prev);
2054
+ return next;
2055
+ });
2056
+ setKeyboardItem(null);
2057
+ };
2058
+ const check = () => {
2059
+ if (!allFilled) return;
2060
+ assessment.answer({
2061
+ checkId,
2062
+ interactionType: INTERACTION5,
2063
+ response: assignments,
2064
+ correct: allCorrect
2065
+ });
2066
+ if (allCorrect && !completedRef.current) {
2067
+ completedRef.current = true;
2068
+ setPassed(true);
2069
+ assessment.complete({
2070
+ checkId,
2071
+ interactionType: INTERACTION5,
2072
+ score: props.targets.length,
2073
+ maxScore: props.targets.length,
2074
+ passingScore: props.passingScore ?? props.targets.length
2075
+ });
2076
+ }
2077
+ };
2078
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Drag and Drop", "data-lk-check-id": checkId, children: [
2079
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: "Match each item to the correct target (drag or use keyboard: select item, then activate target)." }),
2080
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { role: "list", "aria-label": "Draggable items", children: pool.map((id) => {
2081
+ const item = props.items.find((i) => i.id === id);
2082
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2083
+ "button",
2084
+ {
2085
+ type: "button",
2086
+ draggable: true,
2087
+ "data-testid": `drag-item-${id}`,
2088
+ "aria-pressed": keyboardItem === id,
2089
+ onDragStart: (e) => e.dataTransfer.setData("text/plain", id),
2090
+ onClick: () => setKeyboardItem(keyboardItem === id ? null : id),
2091
+ style: { margin: "0.25rem" },
2092
+ children: item.label
2093
+ },
2094
+ id
2095
+ );
2096
+ }) }),
2097
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { children: props.targets.map((target) => {
2098
+ const assigned = assignments[target.id];
2099
+ const label = assigned ? props.items.find((i) => i.id === assigned)?.label ?? assigned : "Drop here";
2100
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("li", { children: [
2101
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: target.label }),
2102
+ " ",
2103
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2104
+ "span",
2105
+ {
2106
+ role: "button",
2107
+ tabIndex: 0,
2108
+ "data-testid": `drop-${target.id}`,
2109
+ onDragOver: (e) => e.preventDefault(),
2110
+ onDrop: (e) => {
2111
+ e.preventDefault();
2112
+ const id = e.dataTransfer.getData("text/plain");
2113
+ if (id) place(target.id, id);
2114
+ },
2115
+ onClick: () => keyboardItem && place(target.id, keyboardItem),
2116
+ onKeyDown: (e) => {
2117
+ if (e.key === "Enter" && keyboardItem) place(target.id, keyboardItem);
2118
+ },
2119
+ style: {
2120
+ display: "inline-block",
2121
+ minWidth: "8em",
2122
+ border: "1px dashed currentColor",
2123
+ padding: "0.25em"
2124
+ },
2125
+ children: label
2126
+ }
2127
+ )
2128
+ ] }, target.id);
2129
+ }) }),
2130
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", "data-testid": "check-drag-drop", disabled: !allFilled || passed, onClick: check, children: "Check" }),
2131
+ allFilled ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "status", "aria-live": "polite", children: passed || allCorrect ? "Correct" : "Try again" }) : null
2132
+ ] });
2133
+ }
2134
+ var DragAndDropInnerForwarded = (0, import_react13.forwardRef)(DragAndDropInner);
2135
+ var DragAndDrop = (0, import_react13.forwardRef)(function DragAndDrop2(props, ref) {
2136
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AssessmentLessonGuard, { blockLabel: "DragAndDrop", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DragAndDropInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
2137
+ });
2138
+
2139
+ // src/blocks/AssessmentSequence.tsx
2140
+ var import_react14 = __toESM(require("react"), 1);
2141
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2142
+ function AssessmentSequence(props) {
2143
+ const sequential = props.sequential !== false;
2144
+ const childArray = import_react14.default.Children.toArray(props.children).filter(import_react14.default.isValidElement);
2145
+ const [index, setIndex] = (0, import_react14.useState)(0);
2146
+ const current = childArray[index] ?? null;
2147
+ const goNext = (0, import_react14.useCallback)(() => {
2148
+ setIndex((i) => Math.min(i + 1, childArray.length - 1));
2149
+ }, [childArray.length]);
2150
+ const goPrev = (0, import_react14.useCallback)(() => {
2151
+ setIndex((i) => Math.max(i - 1, 0));
2152
+ }, []);
2153
+ const progress = (0, import_react14.useMemo)(
2154
+ () => ({ current: index + 1, total: childArray.length }),
2155
+ [index, childArray.length]
2156
+ );
2157
+ if (!sequential) {
2158
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentSequenceProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("section", { "aria-label": "Assessment sequence", children: props.children }) });
2159
+ }
2160
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AssessmentSequenceProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { "aria-label": "Assessment sequence", "data-testid": "assessment-sequence", children: [
2161
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { children: [
2162
+ "Question ",
2163
+ progress.current,
2164
+ " of ",
2165
+ progress.total
2166
+ ] }),
2167
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { "data-testid": "assessment-sequence-step", children: current }),
2168
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("nav", { "aria-label": "Sequence navigation", children: [
2169
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", "data-testid": "sequence-prev", disabled: index === 0, onClick: goPrev, children: "Previous" }),
2170
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2171
+ "button",
2172
+ {
2173
+ type: "button",
2174
+ "data-testid": "sequence-next",
2175
+ disabled: index >= childArray.length - 1,
2176
+ onClick: goNext,
2177
+ children: "Next"
2178
+ }
2179
+ )
2180
+ ] })
2181
+ ] }) });
2182
+ }
2183
+
1283
2184
  // src/index.tsx
1284
2185
  var import_core10 = require("@lessonkit/core");
1285
2186
 
1286
2187
  // src/theme/ThemeProvider.tsx
1287
- var import_react6 = __toESM(require("react"), 1);
2188
+ var import_react15 = __toESM(require("react"), 1);
1288
2189
  var import_themes = require("@lessonkit/themes");
1289
2190
 
1290
2191
  // src/theme/applyCssVariables.ts
@@ -1303,9 +2204,12 @@ function applyCssVariables(target, vars, previousKeys) {
1303
2204
  }
1304
2205
 
1305
2206
  // src/theme/ThemeProvider.tsx
1306
- var import_jsx_runtime3 = require("react/jsx-runtime");
1307
- var ThemeContext = (0, import_react6.createContext)(null);
1308
- var useIsoLayoutEffect2 = typeof window !== "undefined" ? import_react6.useLayoutEffect : import_react6.default.useEffect;
2207
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2208
+ var ThemeContext = (0, import_react15.createContext)(null);
2209
+ var useIsoLayoutEffect2 = (
2210
+ /* v8 ignore next -- SSR uses useEffect when window is unavailable */
2211
+ typeof window !== "undefined" ? import_react15.useLayoutEffect : import_react15.default.useEffect
2212
+ );
1309
2213
  function getSystemMode() {
1310
2214
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1311
2215
  return "light";
@@ -1323,7 +2227,7 @@ function ThemeProvider(props) {
1323
2227
  const preset = props.preset ?? "default";
1324
2228
  const mode = props.mode ?? "light";
1325
2229
  const targetKind = props.target ?? "document";
1326
- const [resolvedMode, setResolvedMode] = (0, import_react6.useState)(
2230
+ const [resolvedMode, setResolvedMode] = (0, import_react15.useState)(
1327
2231
  () => mode === "system" ? getSystemMode() : mode
1328
2232
  );
1329
2233
  useIsoLayoutEffect2(() => {
@@ -1339,20 +2243,20 @@ function ThemeProvider(props) {
1339
2243
  return () => mq.removeEventListener("change", onChange);
1340
2244
  }, [mode]);
1341
2245
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
1342
- const effectiveTheme = (0, import_react6.useMemo)(() => {
2246
+ const effectiveTheme = (0, import_react15.useMemo)(() => {
1343
2247
  const modeBase = resolveModeBase(mode, dataTheme);
1344
2248
  const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
1345
2249
  return (0, import_themes.mergeThemes)(base, props.theme ?? {});
1346
2250
  }, [preset, mode, dataTheme, props.theme]);
1347
- const hostRef = (0, import_react6.useRef)(null);
1348
- const appliedKeysRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
2251
+ const hostRef = (0, import_react15.useRef)(null);
2252
+ const appliedKeysRef = (0, import_react15.useRef)(/* @__PURE__ */ new Set());
1349
2253
  useIsoLayoutEffect2(() => {
1350
2254
  if (targetKind === "document" && typeof document !== "undefined") {
1351
2255
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
1352
2256
  return () => document.documentElement.removeAttribute("data-lk-theme");
1353
2257
  }
1354
2258
  }, [targetKind, dataTheme]);
1355
- const inject = (0, import_react6.useCallback)(() => {
2259
+ const inject = (0, import_react15.useCallback)(() => {
1356
2260
  const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
1357
2261
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
1358
2262
  if (!el) return;
@@ -1369,7 +2273,7 @@ function ThemeProvider(props) {
1369
2273
  appliedKeysRef.current = /* @__PURE__ */ new Set();
1370
2274
  };
1371
2275
  }, [inject, targetKind]);
1372
- const value = (0, import_react6.useMemo)(
2276
+ const value = (0, import_react15.useMemo)(
1373
2277
  () => ({
1374
2278
  theme: effectiveTheme,
1375
2279
  preset,
@@ -1379,12 +2283,12 @@ function ThemeProvider(props) {
1379
2283
  [effectiveTheme, preset, mode, dataTheme]
1380
2284
  );
1381
2285
  if (targetKind === "document") {
1382
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
2286
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
1383
2287
  }
1384
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
2288
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
1385
2289
  }
1386
2290
  function useTheme() {
1387
- const ctx = (0, import_react6.useContext)(ThemeContext);
2291
+ const ctx = (0, import_react15.useContext)(ThemeContext);
1388
2292
  if (!ctx) {
1389
2293
  throw new Error("useTheme must be used within a ThemeProvider");
1390
2294
  }
@@ -1393,6 +2297,7 @@ function useTheme() {
1393
2297
 
1394
2298
  // src/blockCatalog.ts
1395
2299
  var blockCatalogVersion = 1;
2300
+ var blockCatalogV2Version = 2;
1396
2301
  var BLOCK_CATALOG = [
1397
2302
  {
1398
2303
  type: "Course",
@@ -1579,8 +2484,163 @@ var BLOCK_CATALOG = [
1579
2484
  }
1580
2485
  }
1581
2486
  ];
1582
- function buildBlockCatalog() {
1583
- return BLOCK_CATALOG.map((entry) => ({
2487
+ var assessmentBehaviourProps = [
2488
+ { name: "enableRetry", type: "boolean", required: false, description: "Allow retry after completion." },
2489
+ { name: "enableSolutionsButton", type: "boolean", required: false, description: "Show solution control." },
2490
+ { name: "autoCheck", type: "boolean", required: false, description: "Check answers automatically when possible." },
2491
+ { name: "passingScore", type: "number", required: false, description: "Minimum score to pass." }
2492
+ ];
2493
+ var v2AssessmentEntries = [
2494
+ {
2495
+ type: "TrueFalse",
2496
+ category: "assessment",
2497
+ assessmentContract: true,
2498
+ h5pMachineName: "H5P.TrueFalse",
2499
+ h5pAlias: "True/False",
2500
+ description: "Binary true/false question with assessment contract.",
2501
+ props: [
2502
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2503
+ { name: "question", type: "string", required: true, description: "Question text." },
2504
+ { name: "answer", type: "boolean", required: true, description: "Correct answer." },
2505
+ ...assessmentBehaviourProps
2506
+ ],
2507
+ requiredIds: ["checkId"],
2508
+ parentConstraints: ["Lesson", "AssessmentSequence"],
2509
+ a11y: {
2510
+ element: "section",
2511
+ ariaLabel: "True or False",
2512
+ keyboard: "Radio group with True/False options.",
2513
+ liveRegions: "role='status' for feedback.",
2514
+ notes: "H5P True/False equivalent."
2515
+ },
2516
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
2517
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
2518
+ },
2519
+ {
2520
+ type: "FillInTheBlanks",
2521
+ category: "assessment",
2522
+ assessmentContract: true,
2523
+ h5pMachineName: "H5P.Blanks",
2524
+ h5pAlias: "Fill in the Blanks",
2525
+ description: "Fill-in-the-blank text with *answer* markers in template.",
2526
+ props: [
2527
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2528
+ { name: "template", type: "string", required: true, description: "Text with *blank* markers." },
2529
+ { name: "blanks", type: "FillInBlankSpec[]", required: false, description: "Explicit blank specs." },
2530
+ ...assessmentBehaviourProps
2531
+ ],
2532
+ requiredIds: ["checkId"],
2533
+ parentConstraints: ["Lesson", "AssessmentSequence"],
2534
+ a11y: {
2535
+ element: "section",
2536
+ ariaLabel: "Fill in the Blanks",
2537
+ keyboard: "Tab between text inputs.",
2538
+ notes: "H5P Fill in the Blanks equivalent."
2539
+ },
2540
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
2541
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
2542
+ },
2543
+ {
2544
+ type: "DragAndDrop",
2545
+ category: "assessment",
2546
+ assessmentContract: true,
2547
+ h5pMachineName: "H5P.DragQuestion",
2548
+ h5pAlias: "Drag and Drop",
2549
+ description: "Drag items onto labeled targets.",
2550
+ props: [
2551
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2552
+ { name: "items", type: "DragItem[]", required: true, description: "Draggable items." },
2553
+ { name: "targets", type: "DropTarget[]", required: true, description: "Drop targets." },
2554
+ ...assessmentBehaviourProps
2555
+ ],
2556
+ requiredIds: ["checkId"],
2557
+ parentConstraints: ["Lesson", "AssessmentSequence"],
2558
+ a11y: {
2559
+ element: "section",
2560
+ ariaLabel: "Drag and Drop",
2561
+ keyboard: "Select item then activate target; drag also supported.",
2562
+ notes: "H5P Drag and Drop equivalent."
2563
+ },
2564
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
2565
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
2566
+ },
2567
+ {
2568
+ type: "DragTheWords",
2569
+ category: "assessment",
2570
+ assessmentContract: true,
2571
+ h5pMachineName: "H5P.DragText",
2572
+ h5pAlias: "Drag the Words",
2573
+ description: "Drag words into inline blanks.",
2574
+ props: [
2575
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2576
+ { name: "template", type: "string", required: true, description: "Sentence with *blank* zones." },
2577
+ { name: "words", type: "string[]", required: true, description: "Draggable word bank." },
2578
+ ...assessmentBehaviourProps
2579
+ ],
2580
+ requiredIds: ["checkId"],
2581
+ parentConstraints: ["Lesson", "AssessmentSequence"],
2582
+ a11y: {
2583
+ element: "section",
2584
+ ariaLabel: "Drag the Words",
2585
+ keyboard: "Select word then activate zone.",
2586
+ notes: "H5P Drag the Words equivalent."
2587
+ },
2588
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
2589
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
2590
+ },
2591
+ {
2592
+ type: "MarkTheWords",
2593
+ category: "assessment",
2594
+ assessmentContract: true,
2595
+ h5pMachineName: "H5P.MarkTheWords",
2596
+ h5pAlias: "Mark the Words",
2597
+ description: "Select correct words in a sentence.",
2598
+ props: [
2599
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check id." },
2600
+ { name: "text", type: "string", required: true, description: "Source text." },
2601
+ { name: "correctWords", type: "string[]", required: true, description: "Words to mark." },
2602
+ ...assessmentBehaviourProps
2603
+ ],
2604
+ requiredIds: ["checkId"],
2605
+ parentConstraints: ["Lesson", "AssessmentSequence"],
2606
+ a11y: {
2607
+ element: "section",
2608
+ ariaLabel: "Mark the Words",
2609
+ keyboard: "Toggle words with buttons.",
2610
+ notes: "H5P Mark the Words equivalent."
2611
+ },
2612
+ theming: { surface: "global-inherit", dataAttributes: ["data-lk-check-id"], stylingNotes: "Uses data-lk-check-id." },
2613
+ telemetry: { emits: ["assessment_answered", "assessment_completed"], requiresActiveLesson: true }
2614
+ },
2615
+ {
2616
+ type: "AssessmentSequence",
2617
+ category: "container",
2618
+ h5pMachineName: "H5P.QuestionSet",
2619
+ h5pAlias: "Question Set",
2620
+ description: "Ordered sequence of contract-compliant assessments.",
2621
+ props: [
2622
+ { name: "children", type: "ReactNode", required: true, description: "Assessment blocks." },
2623
+ { name: "sequential", type: "boolean", required: false, description: "One question at a time." },
2624
+ ...assessmentBehaviourProps.filter((p) => p.name !== "passingScore")
2625
+ ],
2626
+ requiredIds: [],
2627
+ parentConstraints: ["Lesson"],
2628
+ a11y: {
2629
+ element: "section",
2630
+ ariaLabel: "Assessment sequence",
2631
+ keyboard: "Previous/Next navigation between steps.",
2632
+ notes: "H5P Question Set equivalent."
2633
+ },
2634
+ theming: { surface: "global-inherit", stylingNotes: "Container for assessments." },
2635
+ telemetry: { emits: [], manualTracking: "Child assessments emit assessment_* events." }
2636
+ }
2637
+ ];
2638
+ var BLOCK_CATALOG_V2 = [
2639
+ ...BLOCK_CATALOG,
2640
+ ...v2AssessmentEntries
2641
+ ];
2642
+ function cloneCatalogEntry(entry) {
2643
+ return {
1584
2644
  ...entry,
1585
2645
  props: entry.props.map((p) => ({ ...p })),
1586
2646
  aliases: entry.aliases ? [...entry.aliases] : void 0,
@@ -1595,23 +2655,38 @@ function buildBlockCatalog() {
1595
2655
  ...entry.telemetry,
1596
2656
  emits: [...entry.telemetry.emits]
1597
2657
  }
1598
- }));
2658
+ };
2659
+ }
2660
+ function buildBlockCatalog(opts) {
2661
+ const version = opts?.version ?? 2;
2662
+ const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2663
+ return source.map((entry) => cloneCatalogEntry(entry));
1599
2664
  }
1600
- function getBlockCatalogEntry(type) {
1601
- return BLOCK_CATALOG.find((entry) => entry.type === type || entry.aliases?.includes(type));
2665
+ function getBlockCatalogEntry(type, opts) {
2666
+ const version = opts?.version ?? 2;
2667
+ const source = version === 2 ? BLOCK_CATALOG_V2 : BLOCK_CATALOG;
2668
+ return source.find((entry) => entry.type === type || entry.aliases?.includes(type));
1602
2669
  }
1603
2670
  // Annotate the CommonJS export names for ESM import in node:
1604
2671
  0 && (module.exports = {
2672
+ AssessmentSequence,
1605
2673
  BLOCK_CATALOG,
2674
+ BLOCK_CATALOG_V2,
1606
2675
  Course,
2676
+ DragAndDrop,
2677
+ DragTheWords,
2678
+ FillInTheBlanks,
1607
2679
  KnowledgeCheck,
1608
2680
  Lesson,
1609
2681
  LessonkitProvider,
2682
+ MarkTheWords,
1610
2683
  ProgressTracker,
1611
2684
  Quiz,
1612
2685
  Reflection,
1613
2686
  Scenario,
1614
2687
  ThemeProvider,
2688
+ TrueFalse,
2689
+ blockCatalogV2Version,
1615
2690
  blockCatalogVersion,
1616
2691
  buildBlockCatalog,
1617
2692
  buildTelemetryEvent,
@@ -1622,7 +2697,9 @@ function getBlockCatalogEntry(type) {
1622
2697
  defineLifecyclePlugin,
1623
2698
  defineTelemetryPlugin,
1624
2699
  getBlockCatalogEntry,
2700
+ resetAssessmentWarningsForTests,
1625
2701
  resetQuizWarningsForTests,
2702
+ useAssessmentState,
1626
2703
  useCompletion,
1627
2704
  useLessonkit,
1628
2705
  useProgress,