@lessonkit/react 0.7.0 → 0.8.1

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,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.tsx
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BLOCK_CATALOG: () => BLOCK_CATALOG,
33
34
  Course: () => Course,
34
35
  KnowledgeCheck: () => KnowledgeCheck,
35
36
  Lesson: () => Lesson,
@@ -39,6 +40,9 @@ __export(index_exports, {
39
40
  Reflection: () => Reflection,
40
41
  Scenario: () => Scenario,
41
42
  ThemeProvider: () => ThemeProvider,
43
+ blockCatalogVersion: () => blockCatalogVersion,
44
+ buildBlockCatalog: () => buildBlockCatalog,
45
+ getBlockCatalogEntry: () => getBlockCatalogEntry,
42
46
  useCompletion: () => useCompletion,
43
47
  useLessonkit: () => useLessonkit,
44
48
  useProgress: () => useProgress,
@@ -54,7 +58,7 @@ var import_accessibility = require("@lessonkit/accessibility");
54
58
 
55
59
  // src/context.tsx
56
60
  var import_react = require("react");
57
- var import_core3 = require("@lessonkit/core");
61
+ var import_core4 = require("@lessonkit/core");
58
62
  var import_xapi3 = require("@lessonkit/xapi");
59
63
  var import_xapi4 = require("@lessonkit/xapi");
60
64
 
@@ -94,7 +98,10 @@ function forwardTelemetryToLxpack(event, mode = "auto") {
94
98
  bridge.submitAssessment?.({
95
99
  id: data.checkId,
96
100
  score: scaled,
97
- passingScore: (0, import_bridge.normalizeAssessmentPassingScore)(data.passingScore)
101
+ passingScore: (0, import_bridge.normalizeAssessmentPassingScore)({
102
+ passingScore: data.passingScore,
103
+ maxScore: data.maxScore
104
+ })
98
105
  });
99
106
  return;
100
107
  }
@@ -225,6 +232,12 @@ function createSessionStoragePort() {
225
232
  sessionStorage.setItem(key, value);
226
233
  } catch {
227
234
  }
235
+ },
236
+ removeItem: (key) => {
237
+ try {
238
+ sessionStorage.removeItem(key);
239
+ } catch {
240
+ }
228
241
  }
229
242
  };
230
243
  }
@@ -272,6 +285,8 @@ function createXapiClientFromConfig(config, queue) {
272
285
  if (config.xapi?.enabled === false) return null;
273
286
  if (config.xapi?.client) return config.xapi.client;
274
287
  if (!config.courseId) return null;
288
+ const hasTransport = typeof config.xapi?.transport === "function";
289
+ if (!hasTransport && config.xapi?.enabled !== true) return null;
275
290
  return (0, import_xapi2.createXAPIClient)({
276
291
  courseId: config.courseId,
277
292
  transport: config.xapi?.transport,
@@ -282,6 +297,9 @@ function createXapiClientFromConfig(config, queue) {
282
297
  // src/runtime/session.ts
283
298
  var import_core2 = require("@lessonkit/core");
284
299
  var SESSION_STORAGE_KEY = "lessonkit:sessionId";
300
+ function getTabSessionId(storage) {
301
+ return storage.getItem(SESSION_STORAGE_KEY);
302
+ }
285
303
  var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
286
304
  function resolveSessionId(storage, provided) {
287
305
  if (provided) return provided;
@@ -302,30 +320,47 @@ function markCourseStarted(storage, sessionId, courseId) {
302
320
  if (!courseId) return;
303
321
  storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
304
322
  }
305
-
306
- // src/context.tsx
307
- var import_jsx_runtime = require("react/jsx-runtime");
308
- var LessonkitContext = (0, import_react.createContext)(null);
309
- var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
310
- function disposeTrackingClient(client) {
311
- client?.flush?.();
312
- client?.dispose?.();
323
+ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
324
+ if (!courseId || fromSessionId === toSessionId) return;
325
+ if (hasCourseStarted(storage, fromSessionId, courseId)) {
326
+ markCourseStarted(storage, toSessionId, courseId);
327
+ storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
328
+ }
313
329
  }
314
- var defaultStorage = createSessionStoragePort();
330
+
331
+ // src/runtime/telemetry.ts
332
+ var import_core3 = require("@lessonkit/core");
315
333
  function createTrackingClientFromConfig(config) {
316
- if (config.tracking?.enabled === false) {
317
- return (0, import_core3.createTrackingClient)();
318
- }
334
+ if (config.tracking?.enabled === false) return (0, import_core3.createTrackingClient)();
335
+ if (config.tracking?.createClient) return config.tracking.createClient();
319
336
  return (0, import_core3.createTrackingClient)({
320
337
  sink: config.tracking?.sink,
321
338
  batchSink: config.tracking?.batchSink,
322
339
  batch: config.tracking?.batch
323
340
  });
324
341
  }
342
+ function disposeTrackingClient(client) {
343
+ client?.flush?.();
344
+ client?.dispose?.();
345
+ }
346
+
347
+ // src/context.tsx
348
+ var import_jsx_runtime = require("react/jsx-runtime");
349
+ var LessonkitContext = (0, import_react.createContext)(null);
350
+ var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
351
+ var defaultStorage = createSessionStoragePort();
352
+ function isTrackingActive(tracking) {
353
+ return tracking?.enabled !== false;
354
+ }
325
355
  function LessonkitProvider(props) {
326
356
  const config = props.config;
327
357
  const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
328
- if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
358
+ const prevConfiguredSessionIdRef = (0, import_react.useRef)(config.session?.sessionId);
359
+ if (config.session?.sessionId) {
360
+ sessionIdRef.current = config.session.sessionId;
361
+ } else if (prevConfiguredSessionIdRef.current) {
362
+ sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
363
+ }
329
364
  const attemptIdRef = (0, import_react.useRef)(config.session?.attemptId);
330
365
  const userRef = (0, import_react.useRef)(config.session?.user);
331
366
  attemptIdRef.current = config.session?.attemptId;
@@ -335,6 +370,15 @@ function LessonkitProvider(props) {
335
370
  const lxpackBridgeModeRef = (0, import_react.useRef)(config.lxpack?.bridge ?? "auto");
336
371
  lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
337
372
  const progressRef = (0, import_react.useRef)(createProgressController());
373
+ const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
374
+ const prevCourseIdForProgressRef = (0, import_react.useRef)(config.courseId);
375
+ const pendingCourseIdResetRef = (0, import_react.useRef)(false);
376
+ if (prevCourseIdForProgressRef.current !== config.courseId) {
377
+ prevCourseIdForProgressRef.current = config.courseId;
378
+ progressRef.current = createProgressController();
379
+ pendingCourseIdResetRef.current = true;
380
+ courseStartedEmittedToSinkRef.current = false;
381
+ }
338
382
  const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
339
383
  const syncProgress = (0, import_react.useCallback)(() => {
340
384
  setProgress(progressRef.current.getState());
@@ -344,11 +388,16 @@ function LessonkitProvider(props) {
344
388
  const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
345
389
  const xapiRef = (0, import_react.useRef)(null);
346
390
  const [xapi, setXapi] = (0, import_react.useState)(null);
391
+ const prevXapiCourseIdRef = (0, import_react.useRef)(config.courseId);
347
392
  const xapiEnabled = config.xapi?.enabled;
348
393
  const xapiClient = config.xapi?.client;
349
394
  const xapiTransport = config.xapi?.transport;
350
395
  const courseId = config.courseId;
351
396
  useIsoLayoutEffect(() => {
397
+ if (prevXapiCourseIdRef.current !== courseId) {
398
+ xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
399
+ prevXapiCourseIdRef.current = courseId;
400
+ }
352
401
  const prev = xapiRef.current;
353
402
  const next = createXapiClientFromConfig(config, xapiQueueRef.current);
354
403
  xapiRef.current = next;
@@ -356,22 +405,21 @@ function LessonkitProvider(props) {
356
405
  if (next && !prev) {
357
406
  const sessionId = sessionIdRef.current;
358
407
  const cid = courseIdRef.current;
359
- if (hasCourseStarted(defaultStorage, sessionId, cid)) {
360
- try {
361
- const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
362
- buildTrackEvent({
363
- name: "course_started",
364
- courseId: cid,
365
- sessionId,
366
- attemptId: attemptIdRef.current,
367
- user: userRef.current
368
- })
369
- );
370
- if (statement) next.send(statement);
371
- } catch {
372
- }
408
+ try {
409
+ const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
410
+ buildTrackEvent({
411
+ name: "course_started",
412
+ courseId: cid,
413
+ sessionId,
414
+ attemptId: attemptIdRef.current,
415
+ user: userRef.current
416
+ })
417
+ );
418
+ if (statement) next.send(statement);
419
+ } catch {
373
420
  }
374
421
  }
422
+ let cancelled = false;
375
423
  void (async () => {
376
424
  if (prev) {
377
425
  try {
@@ -379,16 +427,19 @@ function LessonkitProvider(props) {
379
427
  } catch {
380
428
  }
381
429
  }
430
+ if (cancelled) return;
382
431
  try {
383
432
  await next?.flush();
384
433
  } catch {
385
434
  }
386
435
  })();
387
436
  return () => {
437
+ cancelled = true;
388
438
  void prev?.flush();
389
439
  };
390
440
  }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
391
- const trackingRef = (0, import_react.useRef)((0, import_core3.createTrackingClient)());
441
+ const trackingRef = (0, import_react.useRef)((0, import_core4.createTrackingClient)());
442
+ const trackingClientForUnmountRef = (0, import_react.useRef)(trackingRef.current);
392
443
  const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
393
444
  const trackingEnabled = config.tracking?.enabled;
394
445
  const trackingSink = config.tracking?.sink;
@@ -398,12 +449,16 @@ function LessonkitProvider(props) {
398
449
  const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
399
450
  useIsoLayoutEffect(() => {
400
451
  const prev = trackingRef.current;
401
- const next = createTrackingClientFromConfig(config);
452
+ const next = createTrackingClientFromConfig({ tracking: config.tracking });
402
453
  trackingRef.current = next;
454
+ trackingClientForUnmountRef.current = next;
403
455
  setTracking(next);
404
456
  const sessionId = sessionIdRef.current;
405
457
  const cid = courseIdRef.current;
406
- if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
458
+ const trackingActive = isTrackingActive(config.tracking);
459
+ if (!trackingActive) {
460
+ courseStartedEmittedToSinkRef.current = false;
461
+ } else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
407
462
  markCourseStarted(defaultStorage, sessionId, cid);
408
463
  emitTelemetry(
409
464
  next,
@@ -417,6 +472,9 @@ function LessonkitProvider(props) {
417
472
  }),
418
473
  { lxpackBridge: lxpackBridgeModeRef.current }
419
474
  );
475
+ courseStartedEmittedToSinkRef.current = true;
476
+ } else if (trackingActive) {
477
+ courseStartedEmittedToSinkRef.current = true;
420
478
  }
421
479
  return () => {
422
480
  if (prev !== trackingRef.current) {
@@ -455,21 +513,14 @@ function LessonkitProvider(props) {
455
513
  },
456
514
  [emitWithBridge]
457
515
  );
458
- const prevCourseIdRef = (0, import_react.useRef)(config.courseId);
459
- (0, import_react.useEffect)(() => {
460
- if (prevCourseIdRef.current === config.courseId) return;
461
- const previousActiveLesson = progressRef.current.getState().activeLessonId;
462
- prevCourseIdRef.current = config.courseId;
463
- progressRef.current = createProgressController();
516
+ (0, import_react.useLayoutEffect)(() => {
517
+ if (!pendingCourseIdResetRef.current) return;
518
+ pendingCourseIdResetRef.current = false;
464
519
  syncProgress();
465
- if (previousActiveLesson) {
466
- progressRef.current.setActiveLesson(previousActiveLesson, Date.now());
467
- syncProgress();
468
- track("lesson_started", { lessonId: previousActiveLesson }, { lessonId: previousActiveLesson });
469
- }
520
+ if (!isTrackingActive(config.tracking)) return;
470
521
  const sessionId = sessionIdRef.current;
471
- const cid = config.courseId;
472
- if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
522
+ const cid = courseIdRef.current;
523
+ if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
473
524
  markCourseStarted(defaultStorage, sessionId, cid);
474
525
  emitTelemetry(
475
526
  trackingRef.current,
@@ -483,8 +534,9 @@ function LessonkitProvider(props) {
483
534
  }),
484
535
  { lxpackBridge: lxpackBridgeModeRef.current }
485
536
  );
537
+ courseStartedEmittedToSinkRef.current = true;
486
538
  }
487
- }, [config.courseId, syncProgress, track]);
539
+ }, [config.courseId, config.tracking?.enabled, syncProgress]);
488
540
  const emitLessonCompleted = (0, import_react.useCallback)(
489
541
  (lessonId, durationMs) => {
490
542
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -504,16 +556,21 @@ function LessonkitProvider(props) {
504
556
  },
505
557
  [syncProgress, emitLessonCompleted]
506
558
  );
559
+ const unmountTimerIdsRef = (0, import_react.useRef)([]);
507
560
  (0, import_react.useEffect)(() => {
508
561
  return () => {
509
- const client = trackingRef.current;
562
+ for (const id of unmountTimerIdsRef.current) clearTimeout(id);
563
+ unmountTimerIdsRef.current = [];
564
+ const client = trackingClientForUnmountRef.current;
510
565
  void xapiRef.current?.flush();
511
- setTimeout(() => {
566
+ const flushTimer = setTimeout(() => {
512
567
  client?.flush?.();
513
- setTimeout(() => {
568
+ const disposeTimer = setTimeout(() => {
514
569
  client?.dispose?.();
515
570
  }, 0);
571
+ unmountTimerIdsRef.current.push(disposeTimer);
516
572
  }, 0);
573
+ unmountTimerIdsRef.current.push(flushTimer);
517
574
  };
518
575
  }, []);
519
576
  const setActiveLesson = (0, import_react.useCallback)(
@@ -538,10 +595,34 @@ function LessonkitProvider(props) {
538
595
  if (!result.didComplete) return;
539
596
  syncProgress();
540
597
  track("course_completed");
598
+ void trackingRef.current?.flush?.();
541
599
  }, [track, syncProgress]);
542
600
  const sessionUser = config.session?.user;
543
601
  const sessionAttemptId = config.session?.attemptId;
544
602
  const sessionConfiguredId = config.session?.sessionId;
603
+ (0, import_react.useEffect)(() => {
604
+ const nextConfigured = config.session?.sessionId;
605
+ const prevConfigured = prevConfiguredSessionIdRef.current;
606
+ if (nextConfigured === prevConfigured) return;
607
+ prevConfiguredSessionIdRef.current = nextConfigured;
608
+ const cid = courseIdRef.current;
609
+ if (nextConfigured) {
610
+ const fromIds = /* @__PURE__ */ new Set();
611
+ if (prevConfigured) fromIds.add(prevConfigured);
612
+ const tabId = getTabSessionId(defaultStorage);
613
+ if (tabId) fromIds.add(tabId);
614
+ for (const fromId of fromIds) {
615
+ if (fromId !== nextConfigured) {
616
+ migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
617
+ }
618
+ }
619
+ sessionIdRef.current = nextConfigured;
620
+ } else if (prevConfigured) {
621
+ const nextAuto = resolveSessionId(defaultStorage, void 0);
622
+ migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
623
+ sessionIdRef.current = nextAuto;
624
+ }
625
+ }, [sessionConfiguredId, config.courseId]);
545
626
  const runtime = (0, import_react.useMemo)(
546
627
  () => ({
547
628
  config,
@@ -606,7 +687,7 @@ function useQuizState() {
606
687
  }
607
688
 
608
689
  // src/runtime/validateComponentId.ts
609
- var import_core4 = require("@lessonkit/core");
690
+ var import_core5 = require("@lessonkit/core");
610
691
  var warnedPaths = /* @__PURE__ */ new Set();
611
692
  function isDevEnvironment2() {
612
693
  const g = globalThis;
@@ -616,7 +697,7 @@ function warnInvalidComponentId(id, path) {
616
697
  if (!isDevEnvironment2()) return;
617
698
  const key = `${path}:${String(id)}`;
618
699
  if (warnedPaths.has(key)) return;
619
- const result = (0, import_core4.validateId)(id, path);
700
+ const result = (0, import_core5.validateId)(id, path);
620
701
  if (result.ok) return;
621
702
  warnedPaths.add(key);
622
703
  const detail = result.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
@@ -694,6 +775,10 @@ function Quiz(props) {
694
775
  const [selected, setSelected] = (0, import_react3.useState)(null);
695
776
  const completedRef = (0, import_react3.useRef)(false);
696
777
  const questionId = (0, import_react3.useId)();
778
+ (0, import_react3.useEffect)(() => {
779
+ completedRef.current = false;
780
+ setSelected(null);
781
+ }, [props.checkId, props.answer, props.question]);
697
782
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
698
783
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
699
784
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
@@ -844,8 +929,196 @@ function useTheme() {
844
929
  }
845
930
  return ctx;
846
931
  }
932
+
933
+ // src/blockCatalog.ts
934
+ var blockCatalogVersion = 1;
935
+ var BLOCK_CATALOG = [
936
+ {
937
+ type: "Course",
938
+ category: "container",
939
+ description: "Top-level course shell; wraps LessonkitProvider and emits course lifecycle telemetry.",
940
+ props: [
941
+ { name: "title", type: "string", required: true, description: "Course title shown in the h1." },
942
+ { name: "courseId", type: "CourseId", required: true, description: "Stable course identifier for telemetry and packaging." },
943
+ {
944
+ name: "config",
945
+ type: "Omit<LessonkitConfig, 'courseId'>",
946
+ required: false,
947
+ description: "Runtime config (tracking, xAPI, session, lxpack bridge). courseId is merged from props."
948
+ },
949
+ { name: "children", type: "ReactNode", required: true, description: "Lessons and course chrome." }
950
+ ],
951
+ requiredIds: ["courseId"],
952
+ a11y: {
953
+ element: "section",
954
+ ariaLabel: "title prop",
955
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
956
+ notes: "Renders h1 with course title. Wrap with ThemeProvider at app root for theming."
957
+ },
958
+ theming: {
959
+ surface: "global-inherit",
960
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider on document or scoped host."
961
+ },
962
+ telemetry: {
963
+ emits: ["course_started", "course_completed"]
964
+ }
965
+ },
966
+ {
967
+ type: "Lesson",
968
+ category: "container",
969
+ description: "Lesson container; sets active lesson on mount and completes on unmount.",
970
+ props: [
971
+ { name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
972
+ { name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
973
+ { name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
974
+ ],
975
+ requiredIds: ["lessonId"],
976
+ parentConstraints: ["Course"],
977
+ a11y: {
978
+ element: "article",
979
+ ariaLabel: "title prop",
980
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
981
+ notes: "Renders h2 with lesson title. Only one Lesson should be mounted as active at a time in typical SPA layouts."
982
+ },
983
+ theming: {
984
+ surface: "global-inherit",
985
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider."
986
+ },
987
+ telemetry: {
988
+ emits: ["lesson_started", "lesson_completed", "lesson_time_on_task"]
989
+ }
990
+ },
991
+ {
992
+ type: "Scenario",
993
+ category: "content",
994
+ description: "Scenario or narrative content region for branching stories and situational context.",
995
+ props: [
996
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
997
+ { name: "children", type: "ReactNode", required: true, description: "Scenario narrative and custom UI." }
998
+ ],
999
+ requiredIds: [],
1000
+ optionalIds: ["blockId"],
1001
+ parentConstraints: ["Lesson"],
1002
+ a11y: {
1003
+ element: "section",
1004
+ ariaLabel: "Scenario",
1005
+ keyboard: "No block-specific keyboard behavior; custom children may define their own.",
1006
+ notes: "Use for situational framing. Pair with useTracking() for branching interactions."
1007
+ },
1008
+ theming: {
1009
+ surface: "global-inherit",
1010
+ dataAttributes: ["data-lk-block-id"],
1011
+ stylingNotes: "Optional data-lk-block-id when blockId is set. Style via app CSS using --lk-* tokens."
1012
+ },
1013
+ telemetry: {
1014
+ emits: [],
1015
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload })"
1016
+ }
1017
+ },
1018
+ {
1019
+ type: "Reflection",
1020
+ category: "content",
1021
+ description: "Reflection prompt with a textarea for learner free-text responses.",
1022
+ props: [
1023
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
1024
+ { name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
1025
+ { name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
1026
+ ],
1027
+ requiredIds: [],
1028
+ optionalIds: ["blockId"],
1029
+ parentConstraints: ["Lesson"],
1030
+ a11y: {
1031
+ element: "section",
1032
+ ariaLabel: "Reflection",
1033
+ keyboard: "Textarea is keyboard-focusable; standard text entry.",
1034
+ notes: "When prompt is set, textarea uses aria-labelledby; otherwise aria-label='Reflection response'."
1035
+ },
1036
+ theming: {
1037
+ surface: "global-inherit",
1038
+ dataAttributes: ["data-lk-block-id"],
1039
+ stylingNotes: "Optional data-lk-block-id when blockId is set. Style textarea via app CSS."
1040
+ },
1041
+ telemetry: {
1042
+ emits: [],
1043
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
1044
+ }
1045
+ },
1046
+ {
1047
+ type: "Quiz",
1048
+ aliases: ["KnowledgeCheck"],
1049
+ category: "assessment",
1050
+ description: "Single-question multiple-choice assessment with automatic answer and completion telemetry.",
1051
+ props: [
1052
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
1053
+ { name: "question", type: "string", required: true, description: "Question text shown above choices." },
1054
+ { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
1055
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
1056
+ ],
1057
+ requiredIds: ["checkId"],
1058
+ parentConstraints: ["Lesson"],
1059
+ a11y: {
1060
+ element: "section",
1061
+ ariaLabel: "Quiz",
1062
+ keyboard: "Radio group navigable with arrow keys; one choice per question.",
1063
+ liveRegions: "role='status' aria-live='polite' for Correct / Try again feedback.",
1064
+ notes: "Fieldset with visually hidden legend. KnowledgeCheck is an alias that renders Quiz with identical behavior."
1065
+ },
1066
+ theming: {
1067
+ surface: "global-inherit",
1068
+ dataAttributes: ["data-lk-check-id"],
1069
+ stylingNotes: "data-lk-check-id set from checkId. Style labels and feedback via app CSS."
1070
+ },
1071
+ telemetry: {
1072
+ emits: ["quiz_answered", "quiz_completed"],
1073
+ requiresActiveLesson: true
1074
+ }
1075
+ },
1076
+ {
1077
+ type: "ProgressTracker",
1078
+ category: "chrome",
1079
+ description: "Displays count of completed lessons from runtime progress state.",
1080
+ props: [],
1081
+ requiredIds: [],
1082
+ parentConstraints: ["Course"],
1083
+ a11y: {
1084
+ element: "aside",
1085
+ ariaLabel: "Progress",
1086
+ keyboard: "Presentational; no interactive elements.",
1087
+ notes: "Shows 'Lessons completed: N' from progress.completedLessonIds."
1088
+ },
1089
+ theming: {
1090
+ surface: "global-inherit",
1091
+ stylingNotes: "Inherits --lk-* CSS variables; style via app CSS."
1092
+ },
1093
+ telemetry: {
1094
+ emits: []
1095
+ }
1096
+ }
1097
+ ];
1098
+ function buildBlockCatalog() {
1099
+ return BLOCK_CATALOG.map((entry) => ({
1100
+ ...entry,
1101
+ props: entry.props.map((p) => ({ ...p })),
1102
+ aliases: entry.aliases ? [...entry.aliases] : void 0,
1103
+ optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
1104
+ parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
1105
+ a11y: { ...entry.a11y },
1106
+ theming: {
1107
+ ...entry.theming,
1108
+ dataAttributes: entry.theming.dataAttributes ? [...entry.theming.dataAttributes] : void 0
1109
+ },
1110
+ telemetry: {
1111
+ ...entry.telemetry,
1112
+ emits: [...entry.telemetry.emits]
1113
+ }
1114
+ }));
1115
+ }
1116
+ function getBlockCatalogEntry(type) {
1117
+ return BLOCK_CATALOG.find((entry) => entry.type === type || entry.aliases?.includes(type));
1118
+ }
847
1119
  // Annotate the CommonJS export names for ESM import in node:
848
1120
  0 && (module.exports = {
1121
+ BLOCK_CATALOG,
849
1122
  Course,
850
1123
  KnowledgeCheck,
851
1124
  Lesson,
@@ -855,6 +1128,9 @@ function useTheme() {
855
1128
  Reflection,
856
1129
  Scenario,
857
1130
  ThemeProvider,
1131
+ blockCatalogVersion,
1132
+ buildBlockCatalog,
1133
+ getBlockCatalogEntry,
858
1134
  useCompletion,
859
1135
  useLessonkit,
860
1136
  useProgress,
package/dist/index.d.cts CHANGED
@@ -142,4 +142,42 @@ type ThemeContextValue = {
142
142
  declare function ThemeProvider(props: ThemeProviderProps): react_jsx_runtime.JSX.Element;
143
143
  declare function useTheme(): ThemeContextValue;
144
144
 
145
- export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
145
+ declare const blockCatalogVersion: 1;
146
+ type BlockPropSpec = {
147
+ name: string;
148
+ type: string;
149
+ required: boolean;
150
+ description: string;
151
+ };
152
+ type BlockCatalogEntry = {
153
+ type: string;
154
+ aliases?: string[];
155
+ category: "container" | "content" | "assessment" | "chrome";
156
+ description: string;
157
+ props: BlockPropSpec[];
158
+ requiredIds: string[];
159
+ optionalIds?: string[];
160
+ parentConstraints?: string[];
161
+ a11y: {
162
+ element: string;
163
+ ariaLabel: string;
164
+ keyboard: string;
165
+ liveRegions?: string;
166
+ notes: string;
167
+ };
168
+ theming: {
169
+ surface: "global-inherit";
170
+ dataAttributes?: string[];
171
+ stylingNotes: string;
172
+ };
173
+ telemetry: {
174
+ emits: string[];
175
+ requiresActiveLesson?: boolean;
176
+ manualTracking?: string;
177
+ };
178
+ };
179
+ declare const BLOCK_CATALOG: BlockCatalogEntry[];
180
+ declare function buildBlockCatalog(): BlockCatalogEntry[];
181
+ declare function getBlockCatalogEntry(type: string): BlockCatalogEntry | undefined;
182
+
183
+ export { BLOCK_CATALOG, type BlockCatalogEntry, type BlockPropSpec, Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, blockCatalogVersion, buildBlockCatalog, getBlockCatalogEntry, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
package/dist/index.d.ts CHANGED
@@ -142,4 +142,42 @@ type ThemeContextValue = {
142
142
  declare function ThemeProvider(props: ThemeProviderProps): react_jsx_runtime.JSX.Element;
143
143
  declare function useTheme(): ThemeContextValue;
144
144
 
145
- export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
145
+ declare const blockCatalogVersion: 1;
146
+ type BlockPropSpec = {
147
+ name: string;
148
+ type: string;
149
+ required: boolean;
150
+ description: string;
151
+ };
152
+ type BlockCatalogEntry = {
153
+ type: string;
154
+ aliases?: string[];
155
+ category: "container" | "content" | "assessment" | "chrome";
156
+ description: string;
157
+ props: BlockPropSpec[];
158
+ requiredIds: string[];
159
+ optionalIds?: string[];
160
+ parentConstraints?: string[];
161
+ a11y: {
162
+ element: string;
163
+ ariaLabel: string;
164
+ keyboard: string;
165
+ liveRegions?: string;
166
+ notes: string;
167
+ };
168
+ theming: {
169
+ surface: "global-inherit";
170
+ dataAttributes?: string[];
171
+ stylingNotes: string;
172
+ };
173
+ telemetry: {
174
+ emits: string[];
175
+ requiresActiveLesson?: boolean;
176
+ manualTracking?: string;
177
+ };
178
+ };
179
+ declare const BLOCK_CATALOG: BlockCatalogEntry[];
180
+ declare function buildBlockCatalog(): BlockCatalogEntry[];
181
+ declare function getBlockCatalogEntry(type: string): BlockCatalogEntry | undefined;
182
+
183
+ export { BLOCK_CATALOG, type BlockCatalogEntry, type BlockPropSpec, Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, blockCatalogVersion, buildBlockCatalog, getBlockCatalogEntry, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };