@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/README.md +16 -7
- package/block-catalog.v1.json +279 -0
- package/block-contract.v1.json +101 -0
- package/dist/index.cjs +327 -51
- package/dist/index.d.cts +39 -1
- package/dist/index.d.ts +39 -1
- package/dist/index.js +321 -49
- package/package.json +12 -8
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
|
|
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)(
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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 (
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|