@lessonkit/react 1.0.0 → 1.0.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.
@@ -247,6 +247,12 @@
247
247
  "type": "string",
248
248
  "required": true,
249
249
  "description": "Correct choice value (must match one choice)."
250
+ },
251
+ {
252
+ "name": "passingScore",
253
+ "type": "number",
254
+ "required": false,
255
+ "description": "Minimum score required to pass (defaults to maxScore when omitted)."
250
256
  }
251
257
  ],
252
258
  "requiredIds": [
package/dist/index.cjs CHANGED
@@ -70,8 +70,8 @@ var import_react2 = require("react");
70
70
  // src/provider/useLessonkitProviderRuntime.ts
71
71
  var import_react = require("react");
72
72
  var import_core8 = require("@lessonkit/core");
73
- var import_xapi3 = require("@lessonkit/xapi");
74
73
  var import_xapi4 = require("@lessonkit/xapi");
74
+ var import_xapi5 = require("@lessonkit/xapi");
75
75
 
76
76
  // src/runtime/emitTelemetry.ts
77
77
  var import_core2 = require("@lessonkit/core");
@@ -169,6 +169,29 @@ function createXapiClientFromConfig(config, queue) {
169
169
  // src/runtime/session.ts
170
170
  var import_core5 = require("@lessonkit/core");
171
171
 
172
+ // src/runtime/courseStartedPipeline.ts
173
+ var import_xapi3 = require("@lessonkit/xapi");
174
+ function emitCourseStartedNonTrackingPipeline(opts) {
175
+ let xapiStatementSent = false;
176
+ if (!opts.skipXapi && opts.xapi) {
177
+ const statement = (0, import_xapi3.telemetryEventToXAPIStatement)(opts.event);
178
+ if (statement) {
179
+ opts.xapi.send(statement);
180
+ xapiStatementSent = true;
181
+ }
182
+ }
183
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
184
+ const emitCtx = {
185
+ courseId: opts.event.courseId,
186
+ sessionId: opts.event.sessionId,
187
+ attemptId: opts.event.attemptId
188
+ };
189
+ for (const sink of opts.extraSinks ?? []) {
190
+ sink.emit(opts.event, emitCtx);
191
+ }
192
+ return { xapiStatementSent };
193
+ }
194
+
172
195
  // src/runtime/plugins.ts
173
196
  var import_core6 = require("@lessonkit/core");
174
197
  function createReactPluginHost(plugins) {
@@ -220,8 +243,9 @@ var defaultStorage = (0, import_core3.createSessionStoragePort)();
220
243
  function isTrackingActive(tracking) {
221
244
  return tracking?.enabled !== false;
222
245
  }
223
- var noopTrackingClient = { track: () => {
224
- } };
246
+ function isCourseStartedSinkSettled(result) {
247
+ return result === "emitted";
248
+ }
225
249
  function buildCourseStartedEvent(opts) {
226
250
  const pluginCtx = buildPluginContext({
227
251
  courseId: opts.courseId,
@@ -239,31 +263,27 @@ function buildCourseStartedEvent(opts) {
239
263
  return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
240
264
  }
241
265
  function emitCourseStartedPipelineOnly(opts) {
242
- const pluginCtx = buildPluginContext({
243
- courseId: opts.courseId,
244
- sessionId: opts.sessionId,
245
- attemptId: opts.attemptId,
246
- user: opts.user
247
- });
248
266
  try {
249
- emitTelemetryWithPlugins({
250
- pluginHost: null,
251
- tracking: noopTrackingClient,
252
- xapi: opts.xapi,
267
+ const { xapiStatementSent } = emitCourseStartedNonTrackingPipeline({
253
268
  event: opts.event,
254
- pluginCtx,
269
+ xapi: opts.xapi,
255
270
  lxpackBridge: opts.lxpackBridge,
256
- extraSinks: opts.extraSinks
271
+ extraSinks: opts.extraSinks,
272
+ skipXapi: opts.skipXapi
257
273
  });
258
274
  (0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
259
- return true;
275
+ (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
276
+ if (xapiStatementSent) {
277
+ opts.onXapiStatementSent?.();
278
+ }
279
+ return "emitted";
260
280
  } catch {
261
- return false;
281
+ return "failed";
262
282
  }
263
283
  }
264
284
  function emitCourseStarted(opts) {
265
285
  const event = buildCourseStartedEvent(opts);
266
- if (event === null) return true;
286
+ if (event === null) return "filtered";
267
287
  const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
268
288
  opts.storage,
269
289
  opts.sessionId,
@@ -274,14 +294,19 @@ function emitCourseStarted(opts) {
274
294
  opts.tracking.track(event);
275
295
  (0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
276
296
  } catch {
277
- return false;
297
+ return "failed";
278
298
  }
279
299
  }
280
- return emitCourseStartedPipelineOnly({ ...opts, event });
300
+ return emitCourseStartedPipelineOnly({
301
+ ...opts,
302
+ event,
303
+ skipXapi: opts.skipXapi,
304
+ onXapiStatementSent: opts.onXapiStatementSent
305
+ });
281
306
  }
282
307
  function emitCourseStartedToTrackingOnly(opts) {
283
308
  const event = buildCourseStartedEvent(opts);
284
- if (event === null) return true;
309
+ if (event === null) return "filtered";
285
310
  const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
286
311
  opts.storage,
287
312
  opts.sessionId,
@@ -292,28 +317,21 @@ function emitCourseStartedToTrackingOnly(opts) {
292
317
  opts.tracking.track(event);
293
318
  (0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
294
319
  } catch {
295
- return false;
320
+ return "failed";
296
321
  }
297
322
  }
298
- const pluginCtx = buildPluginContext({
299
- courseId: opts.courseId,
300
- sessionId: opts.sessionId,
301
- attemptId: opts.attemptId,
302
- user: opts.user
303
- });
304
323
  try {
305
- emitTelemetryWithPlugins({
306
- pluginHost: null,
307
- tracking: noopTrackingClient,
308
- xapi: null,
324
+ emitCourseStartedNonTrackingPipeline({
309
325
  event,
310
- pluginCtx,
326
+ xapi: null,
311
327
  lxpackBridge: opts.lxpackBridge,
312
- extraSinks: opts.extraSinks
328
+ extraSinks: opts.extraSinks,
329
+ skipXapi: true
313
330
  });
314
- return true;
331
+ (0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
332
+ return "emitted";
315
333
  } catch {
316
- return false;
334
+ return "failed";
317
335
  }
318
336
  }
319
337
  function emitPendingCourseStarted(opts) {
@@ -328,13 +346,28 @@ function emitPendingCourseStarted(opts) {
328
346
  }
329
347
  if (trackingEmitted && !sessionStarted) {
330
348
  const event = buildCourseStartedEvent(opts);
331
- if (event === null) return true;
349
+ if (event === null) return "filtered";
332
350
  return emitCourseStartedPipelineOnly({ ...opts, event });
333
351
  }
334
352
  if (!trackingEmitted && !sessionStarted) {
335
353
  return emitCourseStarted(opts);
336
354
  }
337
- return true;
355
+ const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
356
+ opts.storage,
357
+ opts.sessionId,
358
+ opts.courseId
359
+ );
360
+ if (sessionStarted && trackingEmitted && !pipelineDelivered) {
361
+ const event = buildCourseStartedEvent(opts);
362
+ if (event === null) return "filtered";
363
+ return emitCourseStartedPipelineOnly({
364
+ ...opts,
365
+ event,
366
+ skipXapi: opts.skipXapi,
367
+ onXapiStatementSent: opts.onXapiStatementSent
368
+ });
369
+ }
370
+ return "emitted";
338
371
  }
339
372
  function assertTrackingSinkConfig(tracking) {
340
373
  if (!tracking?.sink || !tracking?.batchSink) return;
@@ -421,7 +454,7 @@ function useLessonkitProviderRuntime(config) {
421
454
  }, []);
422
455
  const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
423
456
  activeLessonIdRef.current = progress.activeLessonId;
424
- const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
457
+ const xapiQueueRef = (0, import_react.useRef)((0, import_xapi4.createInMemoryXAPIQueue)());
425
458
  const xapiRef = (0, import_react.useRef)(null);
426
459
  const [xapi, setXapi] = (0, import_react.useState)(null);
427
460
  const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
@@ -442,7 +475,7 @@ function useLessonkitProviderRuntime(config) {
442
475
  }
443
476
  void xapiRef.current?.flush();
444
477
  }
445
- xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
478
+ xapiQueueRef.current = (0, import_xapi4.createInMemoryXAPIQueue)();
446
479
  prevXapiCourseIdRef.current = courseId;
447
480
  xapiCourseStartedSentOnClientRef.current = false;
448
481
  }
@@ -460,21 +493,24 @@ function useLessonkitProviderRuntime(config) {
460
493
  const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
461
494
  if (needsBootstrap) {
462
495
  try {
463
- const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
464
- (0, import_core2.buildTelemetryEvent)({
465
- name: "course_started",
466
- courseId: cid,
467
- sessionId,
468
- attemptId: attemptIdRef.current,
469
- user: userRef.current
470
- })
471
- );
472
- if (statement) {
473
- next.send(statement);
474
- if (!alreadyStarted) {
475
- (0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
496
+ const event = buildCourseStartedEvent({
497
+ pluginHost: pluginHostRef.current,
498
+ courseId: cid,
499
+ sessionId,
500
+ attemptId: attemptIdRef.current,
501
+ user: userRef.current,
502
+ lxpackBridge: lxpackBridgeModeRef.current
503
+ });
504
+ if (event === null) {
505
+ } else {
506
+ const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
507
+ if (statement) {
508
+ next.send(statement);
509
+ if (!alreadyStarted) {
510
+ (0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
511
+ }
512
+ xapiCourseStartedSentOnClientRef.current = true;
476
513
  }
477
- xapiCourseStartedSentOnClientRef.current = true;
478
514
  }
479
515
  } catch {
480
516
  }
@@ -548,7 +584,7 @@ function useLessonkitProviderRuntime(config) {
548
584
  if (!trackingActive) {
549
585
  courseStartedEmittedToSinkRef.current = false;
550
586
  } else if (!courseStartedEmittedToSinkRef.current) {
551
- const emitted = emitPendingCourseStarted({
587
+ const result = emitPendingCourseStarted({
552
588
  pluginHost: pluginHostRef.current,
553
589
  tracking: next,
554
590
  xapi: xapiRef.current,
@@ -558,12 +594,13 @@ function useLessonkitProviderRuntime(config) {
558
594
  attemptId: attemptIdRef.current,
559
595
  user: userRef.current,
560
596
  lxpackBridge: lxpackBridgeModeRef.current,
561
- extraSinks: extraSinksRef.current
597
+ extraSinks: extraSinksRef.current,
598
+ skipXapi: xapiCourseStartedSentOnClientRef.current,
599
+ onXapiStatementSent: () => {
600
+ xapiCourseStartedSentOnClientRef.current = true;
601
+ }
562
602
  });
563
- if (emitted) {
564
- (0, import_core5.markCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid);
565
- }
566
- courseStartedEmittedToSinkRef.current = emitted;
603
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
567
604
  } else if (trackingActive) {
568
605
  courseStartedEmittedToSinkRef.current = true;
569
606
  }
@@ -644,7 +681,7 @@ function useLessonkitProviderRuntime(config) {
644
681
  } catch {
645
682
  }
646
683
  if (!courseStartedEmittedToSinkRef.current) {
647
- const emitted = emitPendingCourseStarted({
684
+ const result = emitPendingCourseStarted({
648
685
  pluginHost: pluginHostRef.current,
649
686
  tracking: trackingRef.current,
650
687
  xapi: xapiRef.current,
@@ -656,7 +693,7 @@ function useLessonkitProviderRuntime(config) {
656
693
  lxpackBridge: lxpackBridgeModeRef.current,
657
694
  extraSinks: extraSinksRef.current
658
695
  });
659
- courseStartedEmittedToSinkRef.current = emitted;
696
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
660
697
  }
661
698
  })();
662
699
  }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
@@ -893,6 +930,10 @@ function isDevEnvironment3() {
893
930
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
894
931
  }
895
932
  function normalizeComponentId(id, path) {
933
+ if (path === "courseId") return (0, import_core9.assertValidId)(id, "courseId");
934
+ if (path === "lessonId") return (0, import_core9.assertValidId)(id, "lessonId");
935
+ if (path === "checkId") return (0, import_core9.assertValidId)(id, "checkId");
936
+ if (path === "blockId") return (0, import_core9.assertValidId)(id, "blockId");
896
937
  return (0, import_core9.assertValidId)(id, path);
897
938
  }
898
939
 
@@ -1386,7 +1427,13 @@ var BLOCK_CATALOG = [
1386
1427
  { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
1387
1428
  { name: "question", type: "string", required: true, description: "Question text shown above choices." },
1388
1429
  { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
1389
- { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
1430
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." },
1431
+ {
1432
+ name: "passingScore",
1433
+ type: "number",
1434
+ required: false,
1435
+ description: "Minimum score required to pass (defaults to maxScore when omitted)."
1436
+ }
1390
1437
  ],
1391
1438
  requiredIds: ["checkId"],
1392
1439
  parentConstraints: ["Lesson"],
package/dist/index.d.cts CHANGED
@@ -1,9 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import * as _lessonkit_core from '@lessonkit/core';
4
- import { CourseId, TelemetryUser, TrackingClient, LessonkitPlugin, ProgressState, LessonId, TelemetryEventName, PluginHost, CheckId, BlockId } from '@lessonkit/core';
4
+ import { CourseId, TelemetryUser, TrackingClient, LessonkitPlugin, ProgressState, LessonId, TelemetryEventName, TelemetryDataFor, PluginHost, BlockId, CheckId } from '@lessonkit/core';
5
5
  export { AssessmentScoreInput, AssessmentScoreResult, InteractionBlockRegistration, LessonkitPlugin, LessonkitPluginContext, LessonkitPluginKind, PluginHost, PluginRegistry, TelemetryPipelineSink, buildTelemetryEvent, createLessonkitRuntime, createPluginRegistry, createTelemetryPipeline, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin } from '@lessonkit/core';
6
+ import { AssessmentDescriptor } from '@lessonkit/lxpack';
6
7
  import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
8
+ import { LxpackBridgeMode } from '@lessonkit/lxpack/bridge';
7
9
  import { LessonkitThemeV1, ThemePresetName, PartialLessonkitThemeV1 } from '@lessonkit/themes';
8
10
  export { ThemePresetName } from '@lessonkit/themes';
9
11
 
@@ -31,7 +33,7 @@ type LessonkitConfig = {
31
33
  };
32
34
  lxpack?: {
33
35
  /** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
34
- bridge?: "auto" | "off";
36
+ bridge?: LxpackBridgeMode;
35
37
  };
36
38
  /** Framework plugins (analytics, LMS, assessment, interaction, AI). */
37
39
  plugins?: LessonkitPlugin[];
@@ -41,6 +43,10 @@ type LessonkitConfig = {
41
43
  sinks?: _lessonkit_core.TelemetryPipelineSink[];
42
44
  };
43
45
 
46
+ type LessonkitProviderProps = {
47
+ config: LessonkitConfig;
48
+ children: React.ReactNode;
49
+ };
44
50
  type LessonkitRuntime = {
45
51
  config: LessonkitConfig;
46
52
  tracking: TrackingClient;
@@ -54,65 +60,57 @@ type LessonkitRuntime = {
54
60
  setActiveLesson: (lessonId: LessonId) => void;
55
61
  completeLesson: (lessonId: LessonId) => void;
56
62
  completeCourse: () => void;
57
- track: (name: TelemetryEventName, data?: unknown, opts?: {
63
+ track: <N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, opts?: {
58
64
  lessonId?: LessonId;
59
65
  }) => void;
60
66
  plugins: PluginHost | null;
61
67
  };
62
- declare function LessonkitProvider(props: {
63
- config: LessonkitConfig;
64
- children: React.ReactNode;
65
- }): react_jsx_runtime.JSX.Element;
68
+ declare function LessonkitProvider(props: LessonkitProviderProps): react_jsx_runtime.JSX.Element;
66
69
 
67
70
  /** @internal Reset module warnings between tests. */
68
71
  declare function resetQuizWarningsForTests(): void;
69
- declare function Course(props: {
72
+ type CourseProps = {
70
73
  title: string;
71
74
  courseId: CourseId;
72
- config?: Omit<React.ComponentProps<typeof LessonkitProvider>["config"], "courseId">;
75
+ config?: Omit<LessonkitConfig, "courseId">;
73
76
  children: React.ReactNode;
74
- }): react_jsx_runtime.JSX.Element;
75
- declare function Lesson(props: {
77
+ };
78
+ type LessonProps = {
76
79
  title: string;
77
80
  lessonId: LessonId;
78
81
  /** When false, unmount does not emit lesson_completed (for routed multi-pane layouts). Default true. */
79
82
  autoCompleteOnUnmount?: boolean;
80
83
  children: React.ReactNode;
81
- }): react_jsx_runtime.JSX.Element;
82
- declare function Scenario(props: {
84
+ };
85
+ type ScenarioProps = {
83
86
  blockId?: BlockId;
84
87
  children: React.ReactNode;
85
- }): react_jsx_runtime.JSX.Element;
86
- declare function Reflection(props: {
88
+ };
89
+ type ReflectionProps = {
87
90
  blockId?: BlockId;
88
91
  prompt?: string;
89
92
  hint?: string;
90
93
  value?: string;
91
94
  onChange?: (value: string) => void;
92
95
  children?: React.ReactNode;
93
- }): react_jsx_runtime.JSX.Element;
94
- declare function KnowledgeCheck(props: {
95
- checkId: CheckId;
96
- question: string;
97
- choices: string[];
98
- answer: string;
99
- passingScore?: number;
100
- }): react_jsx_runtime.JSX.Element;
101
- declare function Quiz(props: {
102
- checkId: CheckId;
103
- question: string;
104
- choices: string[];
105
- answer: string;
106
- passingScore?: number;
107
- }): react_jsx_runtime.JSX.Element;
108
- declare function ProgressTracker(props: {
96
+ };
97
+ type QuizProps = AssessmentDescriptor;
98
+ type KnowledgeCheckProps = AssessmentDescriptor;
99
+ type ProgressTrackerProps = {
109
100
  totalLessons?: number;
110
- }): react_jsx_runtime.JSX.Element;
101
+ };
102
+ declare function Course(props: CourseProps): react_jsx_runtime.JSX.Element;
103
+ declare function Lesson(props: LessonProps): react_jsx_runtime.JSX.Element;
104
+ declare function Scenario(props: ScenarioProps): react_jsx_runtime.JSX.Element;
105
+ declare function Reflection(props: ReflectionProps): react_jsx_runtime.JSX.Element;
106
+ declare function KnowledgeCheck(props: KnowledgeCheckProps): react_jsx_runtime.JSX.Element;
107
+ declare function Quiz(props: QuizProps): react_jsx_runtime.JSX.Element;
108
+ declare function ProgressTracker(props: ProgressTrackerProps): react_jsx_runtime.JSX.Element;
111
109
 
112
110
  declare function useLessonkit(): LessonkitRuntime;
113
111
  declare function useProgress(): _lessonkit_core.ProgressState;
114
112
  declare function useTracking(): {
115
- track: (name: _lessonkit_core.TelemetryEventName, data?: unknown, opts?: {
113
+ track: <N extends _lessonkit_core.TelemetryEventName>(name: N, data?: _lessonkit_core.TelemetryDataFor<N>, opts?: {
116
114
  lessonId?: LessonId;
117
115
  }) => void;
118
116
  };
@@ -189,8 +187,214 @@ type BlockCatalogEntry = {
189
187
  manualTracking?: string;
190
188
  };
191
189
  };
192
- declare const BLOCK_CATALOG: BlockCatalogEntry[];
190
+ declare const BLOCK_CATALOG: ({
191
+ type: string;
192
+ category: "container";
193
+ description: string;
194
+ props: ({
195
+ name: string;
196
+ type: string;
197
+ required: true;
198
+ description: string;
199
+ } | {
200
+ name: string;
201
+ type: string;
202
+ required: false;
203
+ description: string;
204
+ })[];
205
+ requiredIds: string[];
206
+ a11y: {
207
+ element: string;
208
+ ariaLabel: string;
209
+ keyboard: string;
210
+ notes: string;
211
+ liveRegions?: undefined;
212
+ };
213
+ theming: {
214
+ surface: "global-inherit";
215
+ stylingNotes: string;
216
+ dataAttributes?: undefined;
217
+ };
218
+ telemetry: {
219
+ emits: string[];
220
+ manualTracking?: undefined;
221
+ requiresActiveLesson?: undefined;
222
+ };
223
+ parentConstraints?: undefined;
224
+ optionalIds?: undefined;
225
+ aliases?: undefined;
226
+ } | {
227
+ type: string;
228
+ category: "container";
229
+ description: string;
230
+ props: ({
231
+ name: string;
232
+ type: string;
233
+ required: true;
234
+ description: string;
235
+ } | {
236
+ name: string;
237
+ type: string;
238
+ required: false;
239
+ description: string;
240
+ })[];
241
+ requiredIds: string[];
242
+ parentConstraints: string[];
243
+ a11y: {
244
+ element: string;
245
+ ariaLabel: string;
246
+ keyboard: string;
247
+ notes: string;
248
+ liveRegions?: undefined;
249
+ };
250
+ theming: {
251
+ surface: "global-inherit";
252
+ stylingNotes: string;
253
+ dataAttributes?: undefined;
254
+ };
255
+ telemetry: {
256
+ emits: string[];
257
+ manualTracking?: undefined;
258
+ requiresActiveLesson?: undefined;
259
+ };
260
+ optionalIds?: undefined;
261
+ aliases?: undefined;
262
+ } | {
263
+ type: string;
264
+ category: "content";
265
+ description: string;
266
+ props: ({
267
+ name: string;
268
+ type: string;
269
+ required: false;
270
+ description: string;
271
+ } | {
272
+ name: string;
273
+ type: string;
274
+ required: true;
275
+ description: string;
276
+ })[];
277
+ requiredIds: never[];
278
+ optionalIds: string[];
279
+ parentConstraints: string[];
280
+ a11y: {
281
+ element: string;
282
+ ariaLabel: string;
283
+ keyboard: string;
284
+ notes: string;
285
+ liveRegions?: undefined;
286
+ };
287
+ theming: {
288
+ surface: "global-inherit";
289
+ dataAttributes: string[];
290
+ stylingNotes: string;
291
+ };
292
+ telemetry: {
293
+ emits: never[];
294
+ manualTracking: string;
295
+ requiresActiveLesson?: undefined;
296
+ };
297
+ aliases?: undefined;
298
+ } | {
299
+ type: string;
300
+ category: "content";
301
+ description: string;
302
+ props: {
303
+ name: string;
304
+ type: string;
305
+ required: false;
306
+ description: string;
307
+ }[];
308
+ requiredIds: never[];
309
+ optionalIds: string[];
310
+ parentConstraints: string[];
311
+ a11y: {
312
+ element: string;
313
+ ariaLabel: string;
314
+ keyboard: string;
315
+ notes: string;
316
+ liveRegions?: undefined;
317
+ };
318
+ theming: {
319
+ surface: "global-inherit";
320
+ dataAttributes: string[];
321
+ stylingNotes: string;
322
+ };
323
+ telemetry: {
324
+ emits: never[];
325
+ requiresActiveLesson: true;
326
+ manualTracking: string;
327
+ };
328
+ aliases?: undefined;
329
+ } | {
330
+ type: string;
331
+ aliases: string[];
332
+ category: "assessment";
333
+ description: string;
334
+ props: ({
335
+ name: string;
336
+ type: string;
337
+ required: true;
338
+ description: string;
339
+ } | {
340
+ name: string;
341
+ type: string;
342
+ required: false;
343
+ description: string;
344
+ })[];
345
+ requiredIds: string[];
346
+ parentConstraints: string[];
347
+ a11y: {
348
+ element: string;
349
+ ariaLabel: string;
350
+ keyboard: string;
351
+ liveRegions: string;
352
+ notes: string;
353
+ };
354
+ theming: {
355
+ surface: "global-inherit";
356
+ dataAttributes: string[];
357
+ stylingNotes: string;
358
+ };
359
+ telemetry: {
360
+ emits: string[];
361
+ requiresActiveLesson: true;
362
+ manualTracking?: undefined;
363
+ };
364
+ optionalIds?: undefined;
365
+ } | {
366
+ type: string;
367
+ category: "chrome";
368
+ description: string;
369
+ props: {
370
+ name: string;
371
+ type: string;
372
+ required: false;
373
+ description: string;
374
+ }[];
375
+ requiredIds: never[];
376
+ parentConstraints: string[];
377
+ a11y: {
378
+ element: string;
379
+ ariaLabel: string;
380
+ keyboard: string;
381
+ notes: string;
382
+ liveRegions?: undefined;
383
+ };
384
+ theming: {
385
+ surface: "global-inherit";
386
+ stylingNotes: string;
387
+ dataAttributes?: undefined;
388
+ };
389
+ telemetry: {
390
+ emits: never[];
391
+ manualTracking?: undefined;
392
+ requiresActiveLesson?: undefined;
393
+ };
394
+ optionalIds?: undefined;
395
+ aliases?: undefined;
396
+ })[];
193
397
  declare function buildBlockCatalog(): BlockCatalogEntry[];
194
398
  declare function getBlockCatalogEntry(type: string): BlockCatalogEntry | undefined;
195
399
 
196
- 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, resetQuizWarningsForTests, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
400
+ export { BLOCK_CATALOG, type BlockCatalogEntry, type BlockPropSpec, Course, type CourseProps, KnowledgeCheck, type KnowledgeCheckProps, Lesson, type LessonProps, type LessonkitConfig, LessonkitProvider, type LessonkitProviderProps, type LessonkitRuntime, ProgressTracker, type ProgressTrackerProps, Quiz, type QuizProps, Reflection, type ReflectionProps, Scenario, type ScenarioProps, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, blockCatalogVersion, buildBlockCatalog, getBlockCatalogEntry, resetQuizWarningsForTests, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import * as _lessonkit_core from '@lessonkit/core';
4
- import { CourseId, TelemetryUser, TrackingClient, LessonkitPlugin, ProgressState, LessonId, TelemetryEventName, PluginHost, CheckId, BlockId } from '@lessonkit/core';
4
+ import { CourseId, TelemetryUser, TrackingClient, LessonkitPlugin, ProgressState, LessonId, TelemetryEventName, TelemetryDataFor, PluginHost, BlockId, CheckId } from '@lessonkit/core';
5
5
  export { AssessmentScoreInput, AssessmentScoreResult, InteractionBlockRegistration, LessonkitPlugin, LessonkitPluginContext, LessonkitPluginKind, PluginHost, PluginRegistry, TelemetryPipelineSink, buildTelemetryEvent, createLessonkitRuntime, createPluginRegistry, createTelemetryPipeline, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin } from '@lessonkit/core';
6
+ import { AssessmentDescriptor } from '@lessonkit/lxpack';
6
7
  import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
8
+ import { LxpackBridgeMode } from '@lessonkit/lxpack/bridge';
7
9
  import { LessonkitThemeV1, ThemePresetName, PartialLessonkitThemeV1 } from '@lessonkit/themes';
8
10
  export { ThemePresetName } from '@lessonkit/themes';
9
11
 
@@ -31,7 +33,7 @@ type LessonkitConfig = {
31
33
  };
32
34
  lxpack?: {
33
35
  /** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
34
- bridge?: "auto" | "off";
36
+ bridge?: LxpackBridgeMode;
35
37
  };
36
38
  /** Framework plugins (analytics, LMS, assessment, interaction, AI). */
37
39
  plugins?: LessonkitPlugin[];
@@ -41,6 +43,10 @@ type LessonkitConfig = {
41
43
  sinks?: _lessonkit_core.TelemetryPipelineSink[];
42
44
  };
43
45
 
46
+ type LessonkitProviderProps = {
47
+ config: LessonkitConfig;
48
+ children: React.ReactNode;
49
+ };
44
50
  type LessonkitRuntime = {
45
51
  config: LessonkitConfig;
46
52
  tracking: TrackingClient;
@@ -54,65 +60,57 @@ type LessonkitRuntime = {
54
60
  setActiveLesson: (lessonId: LessonId) => void;
55
61
  completeLesson: (lessonId: LessonId) => void;
56
62
  completeCourse: () => void;
57
- track: (name: TelemetryEventName, data?: unknown, opts?: {
63
+ track: <N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, opts?: {
58
64
  lessonId?: LessonId;
59
65
  }) => void;
60
66
  plugins: PluginHost | null;
61
67
  };
62
- declare function LessonkitProvider(props: {
63
- config: LessonkitConfig;
64
- children: React.ReactNode;
65
- }): react_jsx_runtime.JSX.Element;
68
+ declare function LessonkitProvider(props: LessonkitProviderProps): react_jsx_runtime.JSX.Element;
66
69
 
67
70
  /** @internal Reset module warnings between tests. */
68
71
  declare function resetQuizWarningsForTests(): void;
69
- declare function Course(props: {
72
+ type CourseProps = {
70
73
  title: string;
71
74
  courseId: CourseId;
72
- config?: Omit<React.ComponentProps<typeof LessonkitProvider>["config"], "courseId">;
75
+ config?: Omit<LessonkitConfig, "courseId">;
73
76
  children: React.ReactNode;
74
- }): react_jsx_runtime.JSX.Element;
75
- declare function Lesson(props: {
77
+ };
78
+ type LessonProps = {
76
79
  title: string;
77
80
  lessonId: LessonId;
78
81
  /** When false, unmount does not emit lesson_completed (for routed multi-pane layouts). Default true. */
79
82
  autoCompleteOnUnmount?: boolean;
80
83
  children: React.ReactNode;
81
- }): react_jsx_runtime.JSX.Element;
82
- declare function Scenario(props: {
84
+ };
85
+ type ScenarioProps = {
83
86
  blockId?: BlockId;
84
87
  children: React.ReactNode;
85
- }): react_jsx_runtime.JSX.Element;
86
- declare function Reflection(props: {
88
+ };
89
+ type ReflectionProps = {
87
90
  blockId?: BlockId;
88
91
  prompt?: string;
89
92
  hint?: string;
90
93
  value?: string;
91
94
  onChange?: (value: string) => void;
92
95
  children?: React.ReactNode;
93
- }): react_jsx_runtime.JSX.Element;
94
- declare function KnowledgeCheck(props: {
95
- checkId: CheckId;
96
- question: string;
97
- choices: string[];
98
- answer: string;
99
- passingScore?: number;
100
- }): react_jsx_runtime.JSX.Element;
101
- declare function Quiz(props: {
102
- checkId: CheckId;
103
- question: string;
104
- choices: string[];
105
- answer: string;
106
- passingScore?: number;
107
- }): react_jsx_runtime.JSX.Element;
108
- declare function ProgressTracker(props: {
96
+ };
97
+ type QuizProps = AssessmentDescriptor;
98
+ type KnowledgeCheckProps = AssessmentDescriptor;
99
+ type ProgressTrackerProps = {
109
100
  totalLessons?: number;
110
- }): react_jsx_runtime.JSX.Element;
101
+ };
102
+ declare function Course(props: CourseProps): react_jsx_runtime.JSX.Element;
103
+ declare function Lesson(props: LessonProps): react_jsx_runtime.JSX.Element;
104
+ declare function Scenario(props: ScenarioProps): react_jsx_runtime.JSX.Element;
105
+ declare function Reflection(props: ReflectionProps): react_jsx_runtime.JSX.Element;
106
+ declare function KnowledgeCheck(props: KnowledgeCheckProps): react_jsx_runtime.JSX.Element;
107
+ declare function Quiz(props: QuizProps): react_jsx_runtime.JSX.Element;
108
+ declare function ProgressTracker(props: ProgressTrackerProps): react_jsx_runtime.JSX.Element;
111
109
 
112
110
  declare function useLessonkit(): LessonkitRuntime;
113
111
  declare function useProgress(): _lessonkit_core.ProgressState;
114
112
  declare function useTracking(): {
115
- track: (name: _lessonkit_core.TelemetryEventName, data?: unknown, opts?: {
113
+ track: <N extends _lessonkit_core.TelemetryEventName>(name: N, data?: _lessonkit_core.TelemetryDataFor<N>, opts?: {
116
114
  lessonId?: LessonId;
117
115
  }) => void;
118
116
  };
@@ -189,8 +187,214 @@ type BlockCatalogEntry = {
189
187
  manualTracking?: string;
190
188
  };
191
189
  };
192
- declare const BLOCK_CATALOG: BlockCatalogEntry[];
190
+ declare const BLOCK_CATALOG: ({
191
+ type: string;
192
+ category: "container";
193
+ description: string;
194
+ props: ({
195
+ name: string;
196
+ type: string;
197
+ required: true;
198
+ description: string;
199
+ } | {
200
+ name: string;
201
+ type: string;
202
+ required: false;
203
+ description: string;
204
+ })[];
205
+ requiredIds: string[];
206
+ a11y: {
207
+ element: string;
208
+ ariaLabel: string;
209
+ keyboard: string;
210
+ notes: string;
211
+ liveRegions?: undefined;
212
+ };
213
+ theming: {
214
+ surface: "global-inherit";
215
+ stylingNotes: string;
216
+ dataAttributes?: undefined;
217
+ };
218
+ telemetry: {
219
+ emits: string[];
220
+ manualTracking?: undefined;
221
+ requiresActiveLesson?: undefined;
222
+ };
223
+ parentConstraints?: undefined;
224
+ optionalIds?: undefined;
225
+ aliases?: undefined;
226
+ } | {
227
+ type: string;
228
+ category: "container";
229
+ description: string;
230
+ props: ({
231
+ name: string;
232
+ type: string;
233
+ required: true;
234
+ description: string;
235
+ } | {
236
+ name: string;
237
+ type: string;
238
+ required: false;
239
+ description: string;
240
+ })[];
241
+ requiredIds: string[];
242
+ parentConstraints: string[];
243
+ a11y: {
244
+ element: string;
245
+ ariaLabel: string;
246
+ keyboard: string;
247
+ notes: string;
248
+ liveRegions?: undefined;
249
+ };
250
+ theming: {
251
+ surface: "global-inherit";
252
+ stylingNotes: string;
253
+ dataAttributes?: undefined;
254
+ };
255
+ telemetry: {
256
+ emits: string[];
257
+ manualTracking?: undefined;
258
+ requiresActiveLesson?: undefined;
259
+ };
260
+ optionalIds?: undefined;
261
+ aliases?: undefined;
262
+ } | {
263
+ type: string;
264
+ category: "content";
265
+ description: string;
266
+ props: ({
267
+ name: string;
268
+ type: string;
269
+ required: false;
270
+ description: string;
271
+ } | {
272
+ name: string;
273
+ type: string;
274
+ required: true;
275
+ description: string;
276
+ })[];
277
+ requiredIds: never[];
278
+ optionalIds: string[];
279
+ parentConstraints: string[];
280
+ a11y: {
281
+ element: string;
282
+ ariaLabel: string;
283
+ keyboard: string;
284
+ notes: string;
285
+ liveRegions?: undefined;
286
+ };
287
+ theming: {
288
+ surface: "global-inherit";
289
+ dataAttributes: string[];
290
+ stylingNotes: string;
291
+ };
292
+ telemetry: {
293
+ emits: never[];
294
+ manualTracking: string;
295
+ requiresActiveLesson?: undefined;
296
+ };
297
+ aliases?: undefined;
298
+ } | {
299
+ type: string;
300
+ category: "content";
301
+ description: string;
302
+ props: {
303
+ name: string;
304
+ type: string;
305
+ required: false;
306
+ description: string;
307
+ }[];
308
+ requiredIds: never[];
309
+ optionalIds: string[];
310
+ parentConstraints: string[];
311
+ a11y: {
312
+ element: string;
313
+ ariaLabel: string;
314
+ keyboard: string;
315
+ notes: string;
316
+ liveRegions?: undefined;
317
+ };
318
+ theming: {
319
+ surface: "global-inherit";
320
+ dataAttributes: string[];
321
+ stylingNotes: string;
322
+ };
323
+ telemetry: {
324
+ emits: never[];
325
+ requiresActiveLesson: true;
326
+ manualTracking: string;
327
+ };
328
+ aliases?: undefined;
329
+ } | {
330
+ type: string;
331
+ aliases: string[];
332
+ category: "assessment";
333
+ description: string;
334
+ props: ({
335
+ name: string;
336
+ type: string;
337
+ required: true;
338
+ description: string;
339
+ } | {
340
+ name: string;
341
+ type: string;
342
+ required: false;
343
+ description: string;
344
+ })[];
345
+ requiredIds: string[];
346
+ parentConstraints: string[];
347
+ a11y: {
348
+ element: string;
349
+ ariaLabel: string;
350
+ keyboard: string;
351
+ liveRegions: string;
352
+ notes: string;
353
+ };
354
+ theming: {
355
+ surface: "global-inherit";
356
+ dataAttributes: string[];
357
+ stylingNotes: string;
358
+ };
359
+ telemetry: {
360
+ emits: string[];
361
+ requiresActiveLesson: true;
362
+ manualTracking?: undefined;
363
+ };
364
+ optionalIds?: undefined;
365
+ } | {
366
+ type: string;
367
+ category: "chrome";
368
+ description: string;
369
+ props: {
370
+ name: string;
371
+ type: string;
372
+ required: false;
373
+ description: string;
374
+ }[];
375
+ requiredIds: never[];
376
+ parentConstraints: string[];
377
+ a11y: {
378
+ element: string;
379
+ ariaLabel: string;
380
+ keyboard: string;
381
+ notes: string;
382
+ liveRegions?: undefined;
383
+ };
384
+ theming: {
385
+ surface: "global-inherit";
386
+ stylingNotes: string;
387
+ dataAttributes?: undefined;
388
+ };
389
+ telemetry: {
390
+ emits: never[];
391
+ manualTracking?: undefined;
392
+ requiresActiveLesson?: undefined;
393
+ };
394
+ optionalIds?: undefined;
395
+ aliases?: undefined;
396
+ })[];
193
397
  declare function buildBlockCatalog(): BlockCatalogEntry[];
194
398
  declare function getBlockCatalogEntry(type: string): BlockCatalogEntry | undefined;
195
399
 
196
- 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, resetQuizWarningsForTests, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
400
+ export { BLOCK_CATALOG, type BlockCatalogEntry, type BlockPropSpec, Course, type CourseProps, KnowledgeCheck, type KnowledgeCheckProps, Lesson, type LessonProps, type LessonkitConfig, LessonkitProvider, type LessonkitProviderProps, type LessonkitRuntime, ProgressTracker, type ProgressTrackerProps, Quiz, type QuizProps, Reflection, type ReflectionProps, Scenario, type ScenarioProps, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, blockCatalogVersion, buildBlockCatalog, getBlockCatalogEntry, resetQuizWarningsForTests, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "react";
17
17
  import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
18
18
  import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
19
- import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
19
+ import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
20
20
 
21
21
  // src/runtime/emitTelemetry.ts
22
22
  import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
@@ -135,9 +135,34 @@ import {
135
135
  markCourseStarted,
136
136
  hasCourseStartedEmittedToTracking,
137
137
  markCourseStartedEmittedToTracking,
138
+ hasCourseStartedPipelineDelivered,
139
+ markCourseStartedPipelineDelivered,
138
140
  migrateCourseStartedMark
139
141
  } from "@lessonkit/core";
140
142
 
143
+ // src/runtime/courseStartedPipeline.ts
144
+ import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
145
+ function emitCourseStartedNonTrackingPipeline(opts) {
146
+ let xapiStatementSent = false;
147
+ if (!opts.skipXapi && opts.xapi) {
148
+ const statement = telemetryEventToXAPIStatement2(opts.event);
149
+ if (statement) {
150
+ opts.xapi.send(statement);
151
+ xapiStatementSent = true;
152
+ }
153
+ }
154
+ forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
155
+ const emitCtx = {
156
+ courseId: opts.event.courseId,
157
+ sessionId: opts.event.sessionId,
158
+ attemptId: opts.event.attemptId
159
+ };
160
+ for (const sink of opts.extraSinks ?? []) {
161
+ sink.emit(opts.event, emitCtx);
162
+ }
163
+ return { xapiStatementSent };
164
+ }
165
+
141
166
  // src/runtime/plugins.ts
142
167
  import { createPluginRegistry } from "@lessonkit/core";
143
168
  function createReactPluginHost(plugins) {
@@ -189,8 +214,9 @@ var defaultStorage = createSessionStoragePort();
189
214
  function isTrackingActive(tracking) {
190
215
  return tracking?.enabled !== false;
191
216
  }
192
- var noopTrackingClient = { track: () => {
193
- } };
217
+ function isCourseStartedSinkSettled(result) {
218
+ return result === "emitted";
219
+ }
194
220
  function buildCourseStartedEvent(opts) {
195
221
  const pluginCtx = buildPluginContext({
196
222
  courseId: opts.courseId,
@@ -208,31 +234,27 @@ function buildCourseStartedEvent(opts) {
208
234
  return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
209
235
  }
210
236
  function emitCourseStartedPipelineOnly(opts) {
211
- const pluginCtx = buildPluginContext({
212
- courseId: opts.courseId,
213
- sessionId: opts.sessionId,
214
- attemptId: opts.attemptId,
215
- user: opts.user
216
- });
217
237
  try {
218
- emitTelemetryWithPlugins({
219
- pluginHost: null,
220
- tracking: noopTrackingClient,
221
- xapi: opts.xapi,
238
+ const { xapiStatementSent } = emitCourseStartedNonTrackingPipeline({
222
239
  event: opts.event,
223
- pluginCtx,
240
+ xapi: opts.xapi,
224
241
  lxpackBridge: opts.lxpackBridge,
225
- extraSinks: opts.extraSinks
242
+ extraSinks: opts.extraSinks,
243
+ skipXapi: opts.skipXapi
226
244
  });
227
245
  markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
228
- return true;
246
+ markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
247
+ if (xapiStatementSent) {
248
+ opts.onXapiStatementSent?.();
249
+ }
250
+ return "emitted";
229
251
  } catch {
230
- return false;
252
+ return "failed";
231
253
  }
232
254
  }
233
255
  function emitCourseStarted(opts) {
234
256
  const event = buildCourseStartedEvent(opts);
235
- if (event === null) return true;
257
+ if (event === null) return "filtered";
236
258
  const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
237
259
  opts.storage,
238
260
  opts.sessionId,
@@ -243,14 +265,19 @@ function emitCourseStarted(opts) {
243
265
  opts.tracking.track(event);
244
266
  markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
245
267
  } catch {
246
- return false;
268
+ return "failed";
247
269
  }
248
270
  }
249
- return emitCourseStartedPipelineOnly({ ...opts, event });
271
+ return emitCourseStartedPipelineOnly({
272
+ ...opts,
273
+ event,
274
+ skipXapi: opts.skipXapi,
275
+ onXapiStatementSent: opts.onXapiStatementSent
276
+ });
250
277
  }
251
278
  function emitCourseStartedToTrackingOnly(opts) {
252
279
  const event = buildCourseStartedEvent(opts);
253
- if (event === null) return true;
280
+ if (event === null) return "filtered";
254
281
  const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
255
282
  opts.storage,
256
283
  opts.sessionId,
@@ -261,28 +288,21 @@ function emitCourseStartedToTrackingOnly(opts) {
261
288
  opts.tracking.track(event);
262
289
  markCourseStartedEmittedToTracking(opts.storage, opts.sessionId, opts.courseId);
263
290
  } catch {
264
- return false;
291
+ return "failed";
265
292
  }
266
293
  }
267
- const pluginCtx = buildPluginContext({
268
- courseId: opts.courseId,
269
- sessionId: opts.sessionId,
270
- attemptId: opts.attemptId,
271
- user: opts.user
272
- });
273
294
  try {
274
- emitTelemetryWithPlugins({
275
- pluginHost: null,
276
- tracking: noopTrackingClient,
277
- xapi: null,
295
+ emitCourseStartedNonTrackingPipeline({
278
296
  event,
279
- pluginCtx,
297
+ xapi: null,
280
298
  lxpackBridge: opts.lxpackBridge,
281
- extraSinks: opts.extraSinks
299
+ extraSinks: opts.extraSinks,
300
+ skipXapi: true
282
301
  });
283
- return true;
302
+ markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
303
+ return "emitted";
284
304
  } catch {
285
- return false;
305
+ return "failed";
286
306
  }
287
307
  }
288
308
  function emitPendingCourseStarted(opts) {
@@ -297,13 +317,28 @@ function emitPendingCourseStarted(opts) {
297
317
  }
298
318
  if (trackingEmitted && !sessionStarted) {
299
319
  const event = buildCourseStartedEvent(opts);
300
- if (event === null) return true;
320
+ if (event === null) return "filtered";
301
321
  return emitCourseStartedPipelineOnly({ ...opts, event });
302
322
  }
303
323
  if (!trackingEmitted && !sessionStarted) {
304
324
  return emitCourseStarted(opts);
305
325
  }
306
- return true;
326
+ const pipelineDelivered = hasCourseStartedPipelineDelivered(
327
+ opts.storage,
328
+ opts.sessionId,
329
+ opts.courseId
330
+ );
331
+ if (sessionStarted && trackingEmitted && !pipelineDelivered) {
332
+ const event = buildCourseStartedEvent(opts);
333
+ if (event === null) return "filtered";
334
+ return emitCourseStartedPipelineOnly({
335
+ ...opts,
336
+ event,
337
+ skipXapi: opts.skipXapi,
338
+ onXapiStatementSent: opts.onXapiStatementSent
339
+ });
340
+ }
341
+ return "emitted";
307
342
  }
308
343
  function assertTrackingSinkConfig(tracking) {
309
344
  if (!tracking?.sink || !tracking?.batchSink) return;
@@ -429,21 +464,24 @@ function useLessonkitProviderRuntime(config) {
429
464
  const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
430
465
  if (needsBootstrap) {
431
466
  try {
432
- const statement = telemetryEventToXAPIStatement2(
433
- buildTelemetryEvent({
434
- name: "course_started",
435
- courseId: cid,
436
- sessionId,
437
- attemptId: attemptIdRef.current,
438
- user: userRef.current
439
- })
440
- );
441
- if (statement) {
442
- next.send(statement);
443
- if (!alreadyStarted) {
444
- markCourseStarted(defaultStorage, sessionId, cid);
467
+ const event = buildCourseStartedEvent({
468
+ pluginHost: pluginHostRef.current,
469
+ courseId: cid,
470
+ sessionId,
471
+ attemptId: attemptIdRef.current,
472
+ user: userRef.current,
473
+ lxpackBridge: lxpackBridgeModeRef.current
474
+ });
475
+ if (event === null) {
476
+ } else {
477
+ const statement = telemetryEventToXAPIStatement3(event);
478
+ if (statement) {
479
+ next.send(statement);
480
+ if (!alreadyStarted) {
481
+ markCourseStarted(defaultStorage, sessionId, cid);
482
+ }
483
+ xapiCourseStartedSentOnClientRef.current = true;
445
484
  }
446
- xapiCourseStartedSentOnClientRef.current = true;
447
485
  }
448
486
  } catch {
449
487
  }
@@ -517,7 +555,7 @@ function useLessonkitProviderRuntime(config) {
517
555
  if (!trackingActive) {
518
556
  courseStartedEmittedToSinkRef.current = false;
519
557
  } else if (!courseStartedEmittedToSinkRef.current) {
520
- const emitted = emitPendingCourseStarted({
558
+ const result = emitPendingCourseStarted({
521
559
  pluginHost: pluginHostRef.current,
522
560
  tracking: next,
523
561
  xapi: xapiRef.current,
@@ -527,12 +565,13 @@ function useLessonkitProviderRuntime(config) {
527
565
  attemptId: attemptIdRef.current,
528
566
  user: userRef.current,
529
567
  lxpackBridge: lxpackBridgeModeRef.current,
530
- extraSinks: extraSinksRef.current
568
+ extraSinks: extraSinksRef.current,
569
+ skipXapi: xapiCourseStartedSentOnClientRef.current,
570
+ onXapiStatementSent: () => {
571
+ xapiCourseStartedSentOnClientRef.current = true;
572
+ }
531
573
  });
532
- if (emitted) {
533
- markCourseStartedEmittedToTracking(defaultStorage, sessionId, cid);
534
- }
535
- courseStartedEmittedToSinkRef.current = emitted;
574
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
536
575
  } else if (trackingActive) {
537
576
  courseStartedEmittedToSinkRef.current = true;
538
577
  }
@@ -613,7 +652,7 @@ function useLessonkitProviderRuntime(config) {
613
652
  } catch {
614
653
  }
615
654
  if (!courseStartedEmittedToSinkRef.current) {
616
- const emitted = emitPendingCourseStarted({
655
+ const result = emitPendingCourseStarted({
617
656
  pluginHost: pluginHostRef.current,
618
657
  tracking: trackingRef.current,
619
658
  xapi: xapiRef.current,
@@ -625,7 +664,7 @@ function useLessonkitProviderRuntime(config) {
625
664
  lxpackBridge: lxpackBridgeModeRef.current,
626
665
  extraSinks: extraSinksRef.current
627
666
  });
628
- courseStartedEmittedToSinkRef.current = emitted;
667
+ courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
629
668
  }
630
669
  })();
631
670
  }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
@@ -862,6 +901,10 @@ function isDevEnvironment3() {
862
901
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
863
902
  }
864
903
  function normalizeComponentId(id, path) {
904
+ if (path === "courseId") return assertValidId2(id, "courseId");
905
+ if (path === "lessonId") return assertValidId2(id, "lessonId");
906
+ if (path === "checkId") return assertValidId2(id, "checkId");
907
+ if (path === "blockId") return assertValidId2(id, "blockId");
865
908
  return assertValidId2(id, path);
866
909
  }
867
910
 
@@ -1378,7 +1421,13 @@ var BLOCK_CATALOG = [
1378
1421
  { name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
1379
1422
  { name: "question", type: "string", required: true, description: "Question text shown above choices." },
1380
1423
  { name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
1381
- { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
1424
+ { name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." },
1425
+ {
1426
+ name: "passingScore",
1427
+ type: "number",
1428
+ required: false,
1429
+ description: "Minimum score required to pass (defaults to maxScore when omitted)."
1430
+ }
1382
1431
  ],
1383
1432
  requiredIds: ["checkId"],
1384
1433
  parentConstraints: ["Lesson"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -56,11 +56,11 @@
56
56
  "react-dom": ">=18"
57
57
  },
58
58
  "dependencies": {
59
- "@lessonkit/accessibility": "1.0.0",
60
- "@lessonkit/core": "1.0.0",
61
- "@lessonkit/lxpack": "1.0.0",
62
- "@lessonkit/themes": "1.0.0",
63
- "@lessonkit/xapi": "1.0.0"
59
+ "@lessonkit/accessibility": "1.0.1",
60
+ "@lessonkit/core": "1.0.1",
61
+ "@lessonkit/lxpack": "1.0.1",
62
+ "@lessonkit/themes": "1.0.1",
63
+ "@lessonkit/xapi": "1.0.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@storybook/addon-essentials": "8.6.18",