@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.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  useRef,
13
13
  useState
14
14
  } from "react";
15
- import { createTrackingClient } from "@lessonkit/core";
15
+ import { createTrackingClient as createTrackingClient2 } from "@lessonkit/core";
16
16
  import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
17
17
  import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
18
18
 
@@ -55,7 +55,10 @@ function forwardTelemetryToLxpack(event, mode = "auto") {
55
55
  bridge.submitAssessment?.({
56
56
  id: data.checkId,
57
57
  score: scaled,
58
- passingScore: normalizeAssessmentPassingScore(data.passingScore)
58
+ passingScore: normalizeAssessmentPassingScore({
59
+ passingScore: data.passingScore,
60
+ maxScore: data.maxScore
61
+ })
59
62
  });
60
63
  return;
61
64
  }
@@ -186,6 +189,12 @@ function createSessionStoragePort() {
186
189
  sessionStorage.setItem(key, value);
187
190
  } catch {
188
191
  }
192
+ },
193
+ removeItem: (key) => {
194
+ try {
195
+ sessionStorage.removeItem(key);
196
+ } catch {
197
+ }
189
198
  }
190
199
  };
191
200
  }
@@ -233,6 +242,8 @@ function createXapiClientFromConfig(config, queue) {
233
242
  if (config.xapi?.enabled === false) return null;
234
243
  if (config.xapi?.client) return config.xapi.client;
235
244
  if (!config.courseId) return null;
245
+ const hasTransport = typeof config.xapi?.transport === "function";
246
+ if (!hasTransport && config.xapi?.enabled !== true) return null;
236
247
  return createXAPIClient({
237
248
  courseId: config.courseId,
238
249
  transport: config.xapi?.transport,
@@ -243,6 +254,9 @@ function createXapiClientFromConfig(config, queue) {
243
254
  // src/runtime/session.ts
244
255
  import { createSessionId } from "@lessonkit/core";
245
256
  var SESSION_STORAGE_KEY = "lessonkit:sessionId";
257
+ function getTabSessionId(storage) {
258
+ return storage.getItem(SESSION_STORAGE_KEY);
259
+ }
246
260
  var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
247
261
  function resolveSessionId(storage, provided) {
248
262
  if (provided) return provided;
@@ -263,30 +277,47 @@ function markCourseStarted(storage, sessionId, courseId) {
263
277
  if (!courseId) return;
264
278
  storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
265
279
  }
266
-
267
- // src/context.tsx
268
- import { jsx } from "react/jsx-runtime";
269
- var LessonkitContext = createContext(null);
270
- var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
271
- function disposeTrackingClient(client) {
272
- client?.flush?.();
273
- client?.dispose?.();
280
+ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
281
+ if (!courseId || fromSessionId === toSessionId) return;
282
+ if (hasCourseStarted(storage, fromSessionId, courseId)) {
283
+ markCourseStarted(storage, toSessionId, courseId);
284
+ storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
285
+ }
274
286
  }
275
- var defaultStorage = createSessionStoragePort();
287
+
288
+ // src/runtime/telemetry.ts
289
+ import { createTrackingClient } from "@lessonkit/core";
276
290
  function createTrackingClientFromConfig(config) {
277
- if (config.tracking?.enabled === false) {
278
- return createTrackingClient();
279
- }
291
+ if (config.tracking?.enabled === false) return createTrackingClient();
292
+ if (config.tracking?.createClient) return config.tracking.createClient();
280
293
  return createTrackingClient({
281
294
  sink: config.tracking?.sink,
282
295
  batchSink: config.tracking?.batchSink,
283
296
  batch: config.tracking?.batch
284
297
  });
285
298
  }
299
+ function disposeTrackingClient(client) {
300
+ client?.flush?.();
301
+ client?.dispose?.();
302
+ }
303
+
304
+ // src/context.tsx
305
+ import { jsx } from "react/jsx-runtime";
306
+ var LessonkitContext = createContext(null);
307
+ var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
308
+ var defaultStorage = createSessionStoragePort();
309
+ function isTrackingActive(tracking) {
310
+ return tracking?.enabled !== false;
311
+ }
286
312
  function LessonkitProvider(props) {
287
313
  const config = props.config;
288
314
  const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
289
- if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
315
+ const prevConfiguredSessionIdRef = useRef(config.session?.sessionId);
316
+ if (config.session?.sessionId) {
317
+ sessionIdRef.current = config.session.sessionId;
318
+ } else if (prevConfiguredSessionIdRef.current) {
319
+ sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
320
+ }
290
321
  const attemptIdRef = useRef(config.session?.attemptId);
291
322
  const userRef = useRef(config.session?.user);
292
323
  attemptIdRef.current = config.session?.attemptId;
@@ -296,6 +327,15 @@ function LessonkitProvider(props) {
296
327
  const lxpackBridgeModeRef = useRef(config.lxpack?.bridge ?? "auto");
297
328
  lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
298
329
  const progressRef = useRef(createProgressController());
330
+ const courseStartedEmittedToSinkRef = useRef(false);
331
+ const prevCourseIdForProgressRef = useRef(config.courseId);
332
+ const pendingCourseIdResetRef = useRef(false);
333
+ if (prevCourseIdForProgressRef.current !== config.courseId) {
334
+ prevCourseIdForProgressRef.current = config.courseId;
335
+ progressRef.current = createProgressController();
336
+ pendingCourseIdResetRef.current = true;
337
+ courseStartedEmittedToSinkRef.current = false;
338
+ }
299
339
  const [progress, setProgress] = useState(() => progressRef.current.getState());
300
340
  const syncProgress = useCallback(() => {
301
341
  setProgress(progressRef.current.getState());
@@ -305,11 +345,16 @@ function LessonkitProvider(props) {
305
345
  const xapiQueueRef = useRef(createInMemoryXAPIQueue());
306
346
  const xapiRef = useRef(null);
307
347
  const [xapi, setXapi] = useState(null);
348
+ const prevXapiCourseIdRef = useRef(config.courseId);
308
349
  const xapiEnabled = config.xapi?.enabled;
309
350
  const xapiClient = config.xapi?.client;
310
351
  const xapiTransport = config.xapi?.transport;
311
352
  const courseId = config.courseId;
312
353
  useIsoLayoutEffect(() => {
354
+ if (prevXapiCourseIdRef.current !== courseId) {
355
+ xapiQueueRef.current = createInMemoryXAPIQueue();
356
+ prevXapiCourseIdRef.current = courseId;
357
+ }
313
358
  const prev = xapiRef.current;
314
359
  const next = createXapiClientFromConfig(config, xapiQueueRef.current);
315
360
  xapiRef.current = next;
@@ -317,22 +362,21 @@ function LessonkitProvider(props) {
317
362
  if (next && !prev) {
318
363
  const sessionId = sessionIdRef.current;
319
364
  const cid = courseIdRef.current;
320
- if (hasCourseStarted(defaultStorage, sessionId, cid)) {
321
- try {
322
- const statement = telemetryEventToXAPIStatement2(
323
- buildTrackEvent({
324
- name: "course_started",
325
- courseId: cid,
326
- sessionId,
327
- attemptId: attemptIdRef.current,
328
- user: userRef.current
329
- })
330
- );
331
- if (statement) next.send(statement);
332
- } catch {
333
- }
365
+ try {
366
+ const statement = telemetryEventToXAPIStatement2(
367
+ buildTrackEvent({
368
+ name: "course_started",
369
+ courseId: cid,
370
+ sessionId,
371
+ attemptId: attemptIdRef.current,
372
+ user: userRef.current
373
+ })
374
+ );
375
+ if (statement) next.send(statement);
376
+ } catch {
334
377
  }
335
378
  }
379
+ let cancelled = false;
336
380
  void (async () => {
337
381
  if (prev) {
338
382
  try {
@@ -340,16 +384,19 @@ function LessonkitProvider(props) {
340
384
  } catch {
341
385
  }
342
386
  }
387
+ if (cancelled) return;
343
388
  try {
344
389
  await next?.flush();
345
390
  } catch {
346
391
  }
347
392
  })();
348
393
  return () => {
394
+ cancelled = true;
349
395
  void prev?.flush();
350
396
  };
351
397
  }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
352
- const trackingRef = useRef(createTrackingClient());
398
+ const trackingRef = useRef(createTrackingClient2());
399
+ const trackingClientForUnmountRef = useRef(trackingRef.current);
353
400
  const [tracking, setTracking] = useState(() => trackingRef.current);
354
401
  const trackingEnabled = config.tracking?.enabled;
355
402
  const trackingSink = config.tracking?.sink;
@@ -359,12 +406,16 @@ function LessonkitProvider(props) {
359
406
  const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
360
407
  useIsoLayoutEffect(() => {
361
408
  const prev = trackingRef.current;
362
- const next = createTrackingClientFromConfig(config);
409
+ const next = createTrackingClientFromConfig({ tracking: config.tracking });
363
410
  trackingRef.current = next;
411
+ trackingClientForUnmountRef.current = next;
364
412
  setTracking(next);
365
413
  const sessionId = sessionIdRef.current;
366
414
  const cid = courseIdRef.current;
367
- if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
415
+ const trackingActive = isTrackingActive(config.tracking);
416
+ if (!trackingActive) {
417
+ courseStartedEmittedToSinkRef.current = false;
418
+ } else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
368
419
  markCourseStarted(defaultStorage, sessionId, cid);
369
420
  emitTelemetry(
370
421
  next,
@@ -378,6 +429,9 @@ function LessonkitProvider(props) {
378
429
  }),
379
430
  { lxpackBridge: lxpackBridgeModeRef.current }
380
431
  );
432
+ courseStartedEmittedToSinkRef.current = true;
433
+ } else if (trackingActive) {
434
+ courseStartedEmittedToSinkRef.current = true;
381
435
  }
382
436
  return () => {
383
437
  if (prev !== trackingRef.current) {
@@ -416,21 +470,14 @@ function LessonkitProvider(props) {
416
470
  },
417
471
  [emitWithBridge]
418
472
  );
419
- const prevCourseIdRef = useRef(config.courseId);
420
- useEffect(() => {
421
- if (prevCourseIdRef.current === config.courseId) return;
422
- const previousActiveLesson = progressRef.current.getState().activeLessonId;
423
- prevCourseIdRef.current = config.courseId;
424
- progressRef.current = createProgressController();
473
+ useLayoutEffect(() => {
474
+ if (!pendingCourseIdResetRef.current) return;
475
+ pendingCourseIdResetRef.current = false;
425
476
  syncProgress();
426
- if (previousActiveLesson) {
427
- progressRef.current.setActiveLesson(previousActiveLesson, Date.now());
428
- syncProgress();
429
- track("lesson_started", { lessonId: previousActiveLesson }, { lessonId: previousActiveLesson });
430
- }
477
+ if (!isTrackingActive(config.tracking)) return;
431
478
  const sessionId = sessionIdRef.current;
432
- const cid = config.courseId;
433
- if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
479
+ const cid = courseIdRef.current;
480
+ if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
434
481
  markCourseStarted(defaultStorage, sessionId, cid);
435
482
  emitTelemetry(
436
483
  trackingRef.current,
@@ -444,8 +491,9 @@ function LessonkitProvider(props) {
444
491
  }),
445
492
  { lxpackBridge: lxpackBridgeModeRef.current }
446
493
  );
494
+ courseStartedEmittedToSinkRef.current = true;
447
495
  }
448
- }, [config.courseId, syncProgress, track]);
496
+ }, [config.courseId, config.tracking?.enabled, syncProgress]);
449
497
  const emitLessonCompleted = useCallback(
450
498
  (lessonId, durationMs) => {
451
499
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -465,16 +513,21 @@ function LessonkitProvider(props) {
465
513
  },
466
514
  [syncProgress, emitLessonCompleted]
467
515
  );
516
+ const unmountTimerIdsRef = useRef([]);
468
517
  useEffect(() => {
469
518
  return () => {
470
- const client = trackingRef.current;
519
+ for (const id of unmountTimerIdsRef.current) clearTimeout(id);
520
+ unmountTimerIdsRef.current = [];
521
+ const client = trackingClientForUnmountRef.current;
471
522
  void xapiRef.current?.flush();
472
- setTimeout(() => {
523
+ const flushTimer = setTimeout(() => {
473
524
  client?.flush?.();
474
- setTimeout(() => {
525
+ const disposeTimer = setTimeout(() => {
475
526
  client?.dispose?.();
476
527
  }, 0);
528
+ unmountTimerIdsRef.current.push(disposeTimer);
477
529
  }, 0);
530
+ unmountTimerIdsRef.current.push(flushTimer);
478
531
  };
479
532
  }, []);
480
533
  const setActiveLesson = useCallback(
@@ -499,10 +552,34 @@ function LessonkitProvider(props) {
499
552
  if (!result.didComplete) return;
500
553
  syncProgress();
501
554
  track("course_completed");
555
+ void trackingRef.current?.flush?.();
502
556
  }, [track, syncProgress]);
503
557
  const sessionUser = config.session?.user;
504
558
  const sessionAttemptId = config.session?.attemptId;
505
559
  const sessionConfiguredId = config.session?.sessionId;
560
+ useEffect(() => {
561
+ const nextConfigured = config.session?.sessionId;
562
+ const prevConfigured = prevConfiguredSessionIdRef.current;
563
+ if (nextConfigured === prevConfigured) return;
564
+ prevConfiguredSessionIdRef.current = nextConfigured;
565
+ const cid = courseIdRef.current;
566
+ if (nextConfigured) {
567
+ const fromIds = /* @__PURE__ */ new Set();
568
+ if (prevConfigured) fromIds.add(prevConfigured);
569
+ const tabId = getTabSessionId(defaultStorage);
570
+ if (tabId) fromIds.add(tabId);
571
+ for (const fromId of fromIds) {
572
+ if (fromId !== nextConfigured) {
573
+ migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
574
+ }
575
+ }
576
+ sessionIdRef.current = nextConfigured;
577
+ } else if (prevConfigured) {
578
+ const nextAuto = resolveSessionId(defaultStorage, void 0);
579
+ migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
580
+ sessionIdRef.current = nextAuto;
581
+ }
582
+ }, [sessionConfiguredId, config.courseId]);
506
583
  const runtime = useMemo(
507
584
  () => ({
508
585
  config,
@@ -655,6 +732,10 @@ function Quiz(props) {
655
732
  const [selected, setSelected] = useState2(null);
656
733
  const completedRef = useRef2(false);
657
734
  const questionId = useId();
735
+ useEffect2(() => {
736
+ completedRef.current = false;
737
+ setSelected(null);
738
+ }, [props.checkId, props.answer, props.question]);
658
739
  return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
659
740
  /* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
660
741
  /* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
@@ -820,7 +901,195 @@ function useTheme() {
820
901
  }
821
902
  return ctx;
822
903
  }
904
+
905
+ // src/blockCatalog.ts
906
+ var blockCatalogVersion = 1;
907
+ var BLOCK_CATALOG = [
908
+ {
909
+ type: "Course",
910
+ category: "container",
911
+ description: "Top-level course shell; wraps LessonkitProvider and emits course lifecycle telemetry.",
912
+ props: [
913
+ { name: "title", type: "string", required: true, description: "Course title shown in the h1." },
914
+ { name: "courseId", type: "CourseId", required: true, description: "Stable course identifier for telemetry and packaging." },
915
+ {
916
+ name: "config",
917
+ type: "Omit<LessonkitConfig, 'courseId'>",
918
+ required: false,
919
+ description: "Runtime config (tracking, xAPI, session, lxpack bridge). courseId is merged from props."
920
+ },
921
+ { name: "children", type: "ReactNode", required: true, description: "Lessons and course chrome." }
922
+ ],
923
+ requiredIds: ["courseId"],
924
+ a11y: {
925
+ element: "section",
926
+ ariaLabel: "title prop",
927
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
928
+ notes: "Renders h1 with course title. Wrap with ThemeProvider at app root for theming."
929
+ },
930
+ theming: {
931
+ surface: "global-inherit",
932
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider on document or scoped host."
933
+ },
934
+ telemetry: {
935
+ emits: ["course_started", "course_completed"]
936
+ }
937
+ },
938
+ {
939
+ type: "Lesson",
940
+ category: "container",
941
+ description: "Lesson container; sets active lesson on mount and completes on unmount.",
942
+ props: [
943
+ { name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
944
+ { name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
945
+ { name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
946
+ ],
947
+ requiredIds: ["lessonId"],
948
+ parentConstraints: ["Course"],
949
+ a11y: {
950
+ element: "article",
951
+ ariaLabel: "title prop",
952
+ keyboard: "No block-specific keyboard behavior; focus flows to child content.",
953
+ notes: "Renders h2 with lesson title. Only one Lesson should be mounted as active at a time in typical SPA layouts."
954
+ },
955
+ theming: {
956
+ surface: "global-inherit",
957
+ stylingNotes: "Inherits --lk-* CSS variables from ThemeProvider."
958
+ },
959
+ telemetry: {
960
+ emits: ["lesson_started", "lesson_completed", "lesson_time_on_task"]
961
+ }
962
+ },
963
+ {
964
+ type: "Scenario",
965
+ category: "content",
966
+ description: "Scenario or narrative content region for branching stories and situational context.",
967
+ props: [
968
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
969
+ { name: "children", type: "ReactNode", required: true, description: "Scenario narrative and custom UI." }
970
+ ],
971
+ requiredIds: [],
972
+ optionalIds: ["blockId"],
973
+ parentConstraints: ["Lesson"],
974
+ a11y: {
975
+ element: "section",
976
+ ariaLabel: "Scenario",
977
+ keyboard: "No block-specific keyboard behavior; custom children may define their own.",
978
+ notes: "Use for situational framing. Pair with useTracking() for branching interactions."
979
+ },
980
+ theming: {
981
+ surface: "global-inherit",
982
+ dataAttributes: ["data-lk-block-id"],
983
+ stylingNotes: "Optional data-lk-block-id when blockId is set. Style via app CSS using --lk-* tokens."
984
+ },
985
+ telemetry: {
986
+ emits: [],
987
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload })"
988
+ }
989
+ },
990
+ {
991
+ type: "Reflection",
992
+ category: "content",
993
+ description: "Reflection prompt with a textarea for learner free-text responses.",
994
+ props: [
995
+ { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
996
+ { name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
997
+ { name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
998
+ ],
999
+ requiredIds: [],
1000
+ optionalIds: ["blockId"],
1001
+ parentConstraints: ["Lesson"],
1002
+ a11y: {
1003
+ element: "section",
1004
+ ariaLabel: "Reflection",
1005
+ keyboard: "Textarea is keyboard-focusable; standard text entry.",
1006
+ notes: "When prompt is set, textarea uses aria-labelledby; otherwise aria-label='Reflection response'."
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 textarea via app CSS."
1012
+ },
1013
+ telemetry: {
1014
+ emits: [],
1015
+ manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
1016
+ }
1017
+ },
1018
+ {
1019
+ type: "Quiz",
1020
+ aliases: ["KnowledgeCheck"],
1021
+ category: "assessment",
1022
+ description: "Single-question multiple-choice assessment with automatic answer and completion telemetry.",
1023
+ props: [
1024
+ { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
1025
+ { name: "question", type: "string", required: true, description: "Question text shown above choices." },
1026
+ { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
1027
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
1028
+ ],
1029
+ requiredIds: ["checkId"],
1030
+ parentConstraints: ["Lesson"],
1031
+ a11y: {
1032
+ element: "section",
1033
+ ariaLabel: "Quiz",
1034
+ keyboard: "Radio group navigable with arrow keys; one choice per question.",
1035
+ liveRegions: "role='status' aria-live='polite' for Correct / Try again feedback.",
1036
+ notes: "Fieldset with visually hidden legend. KnowledgeCheck is an alias that renders Quiz with identical behavior."
1037
+ },
1038
+ theming: {
1039
+ surface: "global-inherit",
1040
+ dataAttributes: ["data-lk-check-id"],
1041
+ stylingNotes: "data-lk-check-id set from checkId. Style labels and feedback via app CSS."
1042
+ },
1043
+ telemetry: {
1044
+ emits: ["quiz_answered", "quiz_completed"],
1045
+ requiresActiveLesson: true
1046
+ }
1047
+ },
1048
+ {
1049
+ type: "ProgressTracker",
1050
+ category: "chrome",
1051
+ description: "Displays count of completed lessons from runtime progress state.",
1052
+ props: [],
1053
+ requiredIds: [],
1054
+ parentConstraints: ["Course"],
1055
+ a11y: {
1056
+ element: "aside",
1057
+ ariaLabel: "Progress",
1058
+ keyboard: "Presentational; no interactive elements.",
1059
+ notes: "Shows 'Lessons completed: N' from progress.completedLessonIds."
1060
+ },
1061
+ theming: {
1062
+ surface: "global-inherit",
1063
+ stylingNotes: "Inherits --lk-* CSS variables; style via app CSS."
1064
+ },
1065
+ telemetry: {
1066
+ emits: []
1067
+ }
1068
+ }
1069
+ ];
1070
+ function buildBlockCatalog() {
1071
+ return BLOCK_CATALOG.map((entry) => ({
1072
+ ...entry,
1073
+ props: entry.props.map((p) => ({ ...p })),
1074
+ aliases: entry.aliases ? [...entry.aliases] : void 0,
1075
+ optionalIds: entry.optionalIds ? [...entry.optionalIds] : void 0,
1076
+ parentConstraints: entry.parentConstraints ? [...entry.parentConstraints] : void 0,
1077
+ a11y: { ...entry.a11y },
1078
+ theming: {
1079
+ ...entry.theming,
1080
+ dataAttributes: entry.theming.dataAttributes ? [...entry.theming.dataAttributes] : void 0
1081
+ },
1082
+ telemetry: {
1083
+ ...entry.telemetry,
1084
+ emits: [...entry.telemetry.emits]
1085
+ }
1086
+ }));
1087
+ }
1088
+ function getBlockCatalogEntry(type) {
1089
+ return BLOCK_CATALOG.find((entry) => entry.type === type || entry.aliases?.includes(type));
1090
+ }
823
1091
  export {
1092
+ BLOCK_CATALOG,
824
1093
  Course,
825
1094
  KnowledgeCheck,
826
1095
  Lesson,
@@ -830,6 +1099,9 @@ export {
830
1099
  Reflection,
831
1100
  Scenario,
832
1101
  ThemeProvider,
1102
+ blockCatalogVersion,
1103
+ buildBlockCatalog,
1104
+ getBlockCatalogEntry,
833
1105
  useCompletion,
834
1106
  useLessonkit,
835
1107
  useProgress,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -31,10 +31,14 @@
31
31
  "types": "./dist/index.d.ts",
32
32
  "import": "./dist/index.js",
33
33
  "require": "./dist/index.cjs"
34
- }
34
+ },
35
+ "./block-catalog.v1.json": "./block-catalog.v1.json",
36
+ "./block-contract.v1.json": "./block-contract.v1.json"
35
37
  },
36
38
  "files": [
37
- "dist"
39
+ "dist",
40
+ "block-catalog.v1.json",
41
+ "block-contract.v1.json"
38
42
  ],
39
43
  "scripts": {
40
44
  "build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
@@ -50,11 +54,11 @@
50
54
  "react-dom": ">=18"
51
55
  },
52
56
  "dependencies": {
53
- "@lessonkit/accessibility": "0.7.0",
54
- "@lessonkit/core": "0.7.0",
55
- "@lessonkit/lxpack": "0.7.0",
56
- "@lessonkit/themes": "0.7.0",
57
- "@lessonkit/xapi": "0.7.0"
57
+ "@lessonkit/accessibility": "0.8.1",
58
+ "@lessonkit/core": "0.8.1",
59
+ "@lessonkit/lxpack": "0.8.1",
60
+ "@lessonkit/themes": "0.8.1",
61
+ "@lessonkit/xapi": "0.8.1"
58
62
  },
59
63
  "devDependencies": {
60
64
  "@testing-library/react": "^16.3.0",