@lessonkit/react 0.9.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -42,9 +42,15 @@ __export(index_exports, {
42
42
  ThemeProvider: () => ThemeProvider,
43
43
  blockCatalogVersion: () => blockCatalogVersion,
44
44
  buildBlockCatalog: () => buildBlockCatalog,
45
- createPluginHost: () => import_core7.createPluginHost,
46
- defineLessonkitPlugin: () => import_core7.defineLessonkitPlugin,
45
+ buildTelemetryEvent: () => import_core10.buildTelemetryEvent,
46
+ createLessonkitRuntime: () => import_core10.createLessonkitRuntime,
47
+ createPluginRegistry: () => import_core10.createPluginRegistry,
48
+ createTelemetryPipeline: () => import_core10.createTelemetryPipeline,
49
+ defineAssessmentPlugin: () => import_core10.defineAssessmentPlugin,
50
+ defineLifecyclePlugin: () => import_core10.defineLifecyclePlugin,
51
+ defineTelemetryPlugin: () => import_core10.defineTelemetryPlugin,
47
52
  getBlockCatalogEntry: () => getBlockCatalogEntry,
53
+ resetQuizWarningsForTests: () => resetQuizWarningsForTests,
48
54
  useCompletion: () => useCompletion,
49
55
  useLessonkit: () => useLessonkit,
50
56
  useProgress: () => useProgress,
@@ -55,240 +61,95 @@ __export(index_exports, {
55
61
  module.exports = __toCommonJS(index_exports);
56
62
 
57
63
  // src/components.tsx
58
- var import_react3 = require("react");
64
+ var import_react5 = require("react");
59
65
  var import_accessibility = require("@lessonkit/accessibility");
60
66
 
61
67
  // src/context.tsx
68
+ var import_react2 = require("react");
69
+
70
+ // src/provider/useLessonkitProviderRuntime.ts
62
71
  var import_react = require("react");
63
- var import_core5 = require("@lessonkit/core");
72
+ var import_core8 = require("@lessonkit/core");
64
73
  var import_xapi3 = require("@lessonkit/xapi");
65
74
  var import_xapi4 = require("@lessonkit/xapi");
66
75
 
67
76
  // src/runtime/emitTelemetry.ts
77
+ var import_core2 = require("@lessonkit/core");
78
+
79
+ // src/runtime/telemetryPipeline.ts
68
80
  var import_core = require("@lessonkit/core");
69
81
  var import_xapi = require("@lessonkit/xapi");
70
82
 
71
83
  // src/runtime/lxpackBridge.ts
72
84
  var import_bridge = require("@lessonkit/lxpack/bridge");
73
- function getBridge() {
74
- const fromSdk = (0, import_bridge.getLxpackBridge)();
75
- if (fromSdk) return fromSdk;
76
- if (typeof window === "undefined") return null;
77
- const parent = window.parent;
78
- if (!parent || parent === window) return null;
79
- return parent.lxpack ?? null;
85
+ function forwardTelemetryToLxpack(event, mode = "auto") {
86
+ (0, import_bridge.forwardTelemetryToBridge)(event, mode);
80
87
  }
81
- function applyBridgeAction(bridge, action) {
82
- if (!action) return;
83
- switch (action.kind) {
84
- case "completeLesson":
85
- bridge.completeLesson?.(action.lessonId);
86
- return;
87
- case "completeCourse":
88
- bridge.completeCourse?.();
89
- return;
90
- case "submitAssessment": {
91
- const scaled = (0, import_bridge.normalizeScore)({
92
- score: action.score,
93
- maxScore: action.maxScore
94
- });
95
- if (scaled === null) return;
96
- bridge.submitAssessment?.({
97
- id: action.id,
98
- score: scaled,
99
- passingScore: (0, import_bridge.normalizePassingThreshold)({
100
- passingScore: action.passingScore,
101
- maxScore: action.maxScore
102
- }),
103
- maxScore: action.maxScore
104
- });
105
- return;
106
- }
107
- case "track":
108
- bridge.track?.(action.event);
109
- return;
110
- default:
111
- return;
112
- }
88
+
89
+ // src/runtime/telemetryPipeline.ts
90
+ function isDevEnvironment() {
91
+ const g = globalThis;
92
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
113
93
  }
114
- function forwardTelemetryToLxpack(event, mode = "auto") {
115
- if (mode === "off") return;
116
- const bridge = getBridge();
117
- if (!bridge) return;
118
- const lessonkitEvent = (0, import_bridge.telemetryEventToLessonkit)(event);
119
- if (!lessonkitEvent) return;
120
- const action = (0, import_bridge.mapLessonkitTelemetryToBridgeAction)(lessonkitEvent);
121
- applyBridgeAction(bridge, action);
94
+ function createLegacyPipeline(opts, extraSinks = []) {
95
+ return (0, import_core.createTelemetryPipeline)([
96
+ (0, import_core.createTrackingPipelineSink)("tracking", (event) => opts.tracking.track(event)),
97
+ {
98
+ id: "xapi",
99
+ emit(event) {
100
+ try {
101
+ const statement = (0, import_xapi.telemetryEventToXAPIStatement)(event);
102
+ if (statement) opts.xapi?.send(statement);
103
+ } catch (err) {
104
+ if (isDevEnvironment()) {
105
+ console.warn(
106
+ "[lessonkit] xAPI mapping skipped:",
107
+ err instanceof Error ? err.message : err
108
+ );
109
+ }
110
+ }
111
+ }
112
+ },
113
+ {
114
+ id: "lxpack-bridge",
115
+ emit(event) {
116
+ forwardTelemetryToLxpack(event, opts.lxpackBridge);
117
+ }
118
+ },
119
+ ...extraSinks
120
+ ]);
121
+ }
122
+ function emitThroughPipeline(event, opts, extraSinks) {
123
+ createLegacyPipeline(opts, extraSinks).emit(event);
122
124
  }
123
125
 
124
126
  // src/runtime/emitTelemetry.ts
125
127
  var warnedMissingCourseId = false;
126
- var warnedMissingQuizLesson = false;
127
- function isDevEnvironment() {
128
+ function isDevEnvironment2() {
128
129
  const g = globalThis;
129
130
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
130
131
  }
131
132
  function emitTelemetry(tracking, xapi, event, opts) {
132
133
  if (!event.courseId) {
133
- if (isDevEnvironment() && !warnedMissingCourseId) {
134
+ if (isDevEnvironment2() && !warnedMissingCourseId) {
134
135
  warnedMissingCourseId = true;
135
136
  console.warn("[lessonkit] telemetry event missing courseId");
136
137
  }
137
138
  return;
138
139
  }
139
- tracking.track(event);
140
- try {
141
- const statement = (0, import_xapi.telemetryEventToXAPIStatement)(event);
142
- if (statement) xapi?.send(statement);
143
- } catch (err) {
144
- if (isDevEnvironment()) {
145
- console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
146
- }
147
- }
148
- forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
149
- }
150
- function buildTrackEvent(opts) {
151
- const base = {
152
- timestamp: (0, import_core.nowIso)(),
153
- courseId: opts.courseId,
154
- sessionId: opts.sessionId,
155
- attemptId: opts.attemptId,
156
- user: opts.user
140
+ const legacy = {
141
+ tracking,
142
+ xapi,
143
+ lxpackBridge: opts?.lxpackBridge ?? "auto"
157
144
  };
158
- switch (opts.name) {
159
- case "course_started":
160
- return { name: "course_started", ...base };
161
- case "course_completed":
162
- return { name: "course_completed", ...base };
163
- case "lesson_started": {
164
- const data = opts.data;
165
- const lessonId = opts.lessonId ?? data?.lessonId;
166
- if (!lessonId) throw new Error("lesson_started requires lessonId");
167
- return {
168
- name: "lesson_started",
169
- ...base,
170
- lessonId,
171
- data: { ...data, lessonId }
172
- };
173
- }
174
- case "lesson_completed":
175
- case "lesson_time_on_task": {
176
- const data = opts.data;
177
- const lessonId = opts.lessonId ?? data?.lessonId;
178
- if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
179
- return {
180
- name: opts.name,
181
- ...base,
182
- lessonId,
183
- data: { ...data, lessonId }
184
- };
185
- }
186
- case "quiz_answered": {
187
- const data = opts.data;
188
- const lessonId = opts.lessonId;
189
- if (!lessonId) throw new Error("quiz_answered requires active lessonId");
190
- return { name: "quiz_answered", ...base, lessonId, data };
191
- }
192
- case "quiz_completed": {
193
- const data = opts.data;
194
- const lessonId = opts.lessonId;
195
- if (!lessonId) throw new Error("quiz_completed requires active lessonId");
196
- return { name: "quiz_completed", ...base, lessonId, data };
197
- }
198
- case "interaction":
199
- return {
200
- name: "interaction",
201
- ...base,
202
- lessonId: opts.lessonId,
203
- data: opts.data
204
- };
205
- default:
206
- return { name: opts.name, ...base };
207
- }
208
- }
209
- function tryBuildTrackEvent(opts) {
210
- const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
211
- if (isQuiz && !opts.lessonId) {
212
- if (isDevEnvironment() && !warnedMissingQuizLesson) {
213
- warnedMissingQuizLesson = true;
214
- console.warn(
215
- `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
216
- );
217
- }
218
- return null;
219
- }
220
- return buildTrackEvent(opts);
145
+ emitThroughPipeline(event, legacy, opts?.extraSinks);
221
146
  }
222
147
 
223
148
  // src/runtime/ports.ts
224
- function createNoopStorage() {
225
- return {
226
- getItem: () => null,
227
- setItem: () => {
228
- }
229
- };
230
- }
231
- function createSessionStoragePort() {
232
- if (typeof sessionStorage === "undefined") return createNoopStorage();
233
- return {
234
- getItem: (key) => {
235
- try {
236
- return sessionStorage.getItem(key);
237
- } catch {
238
- return null;
239
- }
240
- },
241
- setItem: (key, value) => {
242
- try {
243
- sessionStorage.setItem(key, value);
244
- } catch {
245
- }
246
- },
247
- removeItem: (key) => {
248
- try {
249
- sessionStorage.removeItem(key);
250
- } catch {
251
- }
252
- }
253
- };
254
- }
149
+ var import_core3 = require("@lessonkit/core");
255
150
 
256
151
  // src/runtime/progress.ts
257
- function createProgressController() {
258
- let activeLessonId;
259
- let completedLessonIds = /* @__PURE__ */ new Set();
260
- let courseCompleted = false;
261
- const lessonStartTimes = /* @__PURE__ */ new Map();
262
- return {
263
- getState: () => ({
264
- activeLessonId,
265
- completedLessonIds: new Set(completedLessonIds),
266
- courseCompleted
267
- }),
268
- setActiveLesson: (lessonId, startedAtMs) => {
269
- const previousLessonId = activeLessonId;
270
- activeLessonId = lessonId;
271
- lessonStartTimes.set(lessonId, startedAtMs);
272
- return { previousLessonId };
273
- },
274
- completeLesson: (lessonId, completedAtMs) => {
275
- if (completedLessonIds.has(lessonId)) return { didComplete: false };
276
- completedLessonIds = new Set(completedLessonIds).add(lessonId);
277
- if (activeLessonId === lessonId) {
278
- activeLessonId = void 0;
279
- }
280
- const startedAt = lessonStartTimes.get(lessonId);
281
- lessonStartTimes.delete(lessonId);
282
- const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
283
- return { durationMs, didComplete: true };
284
- },
285
- completeCourse: () => {
286
- if (courseCompleted) return { didComplete: false };
287
- courseCompleted = true;
288
- return { didComplete: true };
289
- }
290
- };
291
- }
152
+ var import_core4 = require("@lessonkit/core");
292
153
 
293
154
  // src/runtime/xapi.ts
294
155
  var import_xapi2 = require("@lessonkit/xapi");
@@ -306,64 +167,37 @@ function createXapiClientFromConfig(config, queue) {
306
167
  }
307
168
 
308
169
  // src/runtime/session.ts
309
- var import_core2 = require("@lessonkit/core");
310
- var SESSION_STORAGE_KEY = "lessonkit:sessionId";
311
- function getTabSessionId(storage) {
312
- return storage.getItem(SESSION_STORAGE_KEY);
313
- }
314
- var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
315
- function resolveSessionId(storage, provided) {
316
- if (provided) return provided;
317
- const existing = storage.getItem(SESSION_STORAGE_KEY);
318
- if (existing) return existing;
319
- const id = (0, import_core2.createSessionId)();
320
- storage.setItem(SESSION_STORAGE_KEY, id);
321
- return id;
322
- }
323
- function courseStartedStorageKey(sessionId, courseId) {
324
- return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
325
- }
326
- function hasCourseStarted(storage, sessionId, courseId) {
327
- if (!courseId) return false;
328
- return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
329
- }
330
- function markCourseStarted(storage, sessionId, courseId) {
331
- if (!courseId) return;
332
- storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
333
- }
334
- function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
335
- if (!courseId || fromSessionId === toSessionId) return;
336
- if (hasCourseStarted(storage, fromSessionId, courseId)) {
337
- markCourseStarted(storage, toSessionId, courseId);
338
- storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
339
- }
340
- }
170
+ var import_core5 = require("@lessonkit/core");
341
171
 
342
172
  // src/runtime/plugins.ts
343
- var import_core3 = require("@lessonkit/core");
173
+ var import_core6 = require("@lessonkit/core");
344
174
  function createReactPluginHost(plugins) {
345
175
  if (!plugins?.length) return null;
346
- return (0, import_core3.createPluginHost)(plugins);
176
+ return (0, import_core6.createPluginRegistry)(plugins);
347
177
  }
348
178
  function buildPluginContext(opts) {
349
179
  return {
350
180
  courseId: opts.courseId,
351
181
  sessionId: opts.sessionId,
352
- attemptId: opts.attemptId
182
+ attemptId: opts.attemptId,
183
+ user: opts.user
353
184
  };
354
185
  }
355
186
  function emitTelemetryWithPlugins(opts) {
356
187
  const next = opts.pluginHost ? opts.pluginHost.runTelemetry(opts.event, opts.pluginCtx) : opts.event;
357
188
  if (next === null) return;
358
- emitTelemetry(opts.tracking, opts.xapi, next, { lxpackBridge: opts.lxpackBridge ?? "auto" });
189
+ emitTelemetry(opts.tracking, opts.xapi, next, {
190
+ lxpackBridge: opts.lxpackBridge ?? "auto",
191
+ extraSinks: opts.extraSinks
192
+ });
359
193
  }
360
194
 
361
195
  // src/runtime/telemetry.ts
362
- var import_core4 = require("@lessonkit/core");
196
+ var import_core7 = require("@lessonkit/core");
363
197
  function createTrackingClientFromConfig(config) {
364
- if (config.tracking?.enabled === false) return (0, import_core4.createTrackingClient)();
198
+ if (config.tracking?.enabled === false) return (0, import_core7.createTrackingClient)();
365
199
  if (config.tracking?.createClient) return config.tracking.createClient();
366
- return (0, import_core4.createTrackingClient)({
200
+ return (0, import_core7.createTrackingClient)({
367
201
  sink: config.tracking?.sink,
368
202
  batchSink: config.tracking?.batchSink,
369
203
  batch: config.tracking?.batch
@@ -380,71 +214,207 @@ async function disposeTrackingClient(client) {
380
214
  }
381
215
  }
382
216
 
383
- // src/context.tsx
384
- var import_jsx_runtime = require("react/jsx-runtime");
385
- var LessonkitContext = (0, import_react.createContext)(null);
217
+ // src/provider/useLessonkitProviderRuntime.ts
386
218
  var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
387
- var defaultStorage = createSessionStoragePort();
219
+ var defaultStorage = (0, import_core3.createSessionStoragePort)();
388
220
  function isTrackingActive(tracking) {
389
221
  return tracking?.enabled !== false;
390
222
  }
391
- function emitCourseStarted(opts) {
223
+ var noopTrackingClient = { track: () => {
224
+ } };
225
+ function buildCourseStartedEvent(opts) {
226
+ const pluginCtx = buildPluginContext({
227
+ courseId: opts.courseId,
228
+ sessionId: opts.sessionId,
229
+ attemptId: opts.attemptId,
230
+ user: opts.user
231
+ });
232
+ const built = (0, import_core2.buildTelemetryEvent)({
233
+ name: "course_started",
234
+ courseId: opts.courseId,
235
+ sessionId: opts.sessionId,
236
+ attemptId: opts.attemptId,
237
+ user: opts.user
238
+ });
239
+ return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
240
+ }
241
+ function emitCourseStartedPipelineOnly(opts) {
392
242
  const pluginCtx = buildPluginContext({
393
243
  courseId: opts.courseId,
394
244
  sessionId: opts.sessionId,
395
- attemptId: opts.attemptId
245
+ attemptId: opts.attemptId,
246
+ user: opts.user
396
247
  });
397
248
  try {
398
249
  emitTelemetryWithPlugins({
399
- pluginHost: opts.pluginHost,
400
- tracking: opts.tracking,
250
+ pluginHost: null,
251
+ tracking: noopTrackingClient,
401
252
  xapi: opts.xapi,
402
- event: buildTrackEvent({
403
- name: "course_started",
404
- courseId: opts.courseId,
405
- sessionId: opts.sessionId,
406
- attemptId: opts.attemptId,
407
- user: opts.user
408
- }),
253
+ event: opts.event,
409
254
  pluginCtx,
410
- lxpackBridge: opts.lxpackBridge
255
+ lxpackBridge: opts.lxpackBridge,
256
+ extraSinks: opts.extraSinks
411
257
  });
412
- markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
258
+ (0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
413
259
  return true;
414
260
  } catch {
415
261
  return false;
416
262
  }
417
263
  }
418
- function LessonkitProvider(props) {
419
- const config = props.config;
420
- const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
421
- const prevConfiguredSessionIdRef = (0, import_react.useRef)(config.session?.sessionId);
422
- if (config.session?.sessionId) {
423
- sessionIdRef.current = config.session.sessionId;
264
+ function emitCourseStarted(opts) {
265
+ const event = buildCourseStartedEvent(opts);
266
+ if (event === null) return true;
267
+ const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
268
+ opts.storage,
269
+ opts.sessionId,
270
+ opts.courseId
271
+ );
272
+ if (!trackingAlreadyEmitted) {
273
+ try {
274
+ opts.tracking.track(event);
275
+ (0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+ return emitCourseStartedPipelineOnly({ ...opts, event });
281
+ }
282
+ function emitCourseStartedToTrackingOnly(opts) {
283
+ const event = buildCourseStartedEvent(opts);
284
+ if (event === null) return true;
285
+ const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
286
+ opts.storage,
287
+ opts.sessionId,
288
+ opts.courseId
289
+ );
290
+ if (!trackingAlreadyEmitted) {
291
+ try {
292
+ opts.tracking.track(event);
293
+ (0, import_core5.markCourseStartedEmittedToTracking)(opts.storage, opts.sessionId, opts.courseId);
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+ const pluginCtx = buildPluginContext({
299
+ courseId: opts.courseId,
300
+ sessionId: opts.sessionId,
301
+ attemptId: opts.attemptId,
302
+ user: opts.user
303
+ });
304
+ try {
305
+ emitTelemetryWithPlugins({
306
+ pluginHost: null,
307
+ tracking: noopTrackingClient,
308
+ xapi: null,
309
+ event,
310
+ pluginCtx,
311
+ lxpackBridge: opts.lxpackBridge,
312
+ extraSinks: opts.extraSinks
313
+ });
314
+ return true;
315
+ } catch {
316
+ return false;
317
+ }
318
+ }
319
+ function emitPendingCourseStarted(opts) {
320
+ const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
321
+ opts.storage,
322
+ opts.sessionId,
323
+ opts.courseId
324
+ );
325
+ const sessionStarted = (0, import_core5.hasCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
326
+ if (sessionStarted && !trackingEmitted) {
327
+ return emitCourseStartedToTrackingOnly(opts);
328
+ }
329
+ if (trackingEmitted && !sessionStarted) {
330
+ const event = buildCourseStartedEvent(opts);
331
+ if (event === null) return true;
332
+ return emitCourseStartedPipelineOnly({ ...opts, event });
333
+ }
334
+ if (!trackingEmitted && !sessionStarted) {
335
+ return emitCourseStarted(opts);
336
+ }
337
+ return true;
338
+ }
339
+ function assertTrackingSinkConfig(tracking) {
340
+ if (!tracking?.sink || !tracking?.batchSink) return;
341
+ throw new Error(
342
+ "[lessonkit] tracking.sink and tracking.batchSink cannot both be set; use batchSink alone for batched delivery"
343
+ );
344
+ }
345
+ function useLessonkitProviderRuntime(config) {
346
+ const normalizedCourseId = (0, import_react.useMemo)(
347
+ () => (0, import_core8.assertValidId)(config.courseId, "courseId"),
348
+ [config.courseId]
349
+ );
350
+ const normalizedConfig = (0, import_react.useMemo)(
351
+ () => ({ ...config, courseId: normalizedCourseId }),
352
+ [config, normalizedCourseId]
353
+ );
354
+ const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
355
+ const extraSinksRef = (0, import_react.useRef)(normalizedConfig.sinks);
356
+ extraSinksRef.current = normalizedConfig.sinks;
357
+ const headlessRef = (0, import_react.useRef)(null);
358
+ const sessionIdRef = (0, import_react.useRef)((0, import_core5.resolveSessionId)(defaultStorage, normalizedConfig.session?.sessionId));
359
+ const prevConfiguredSessionIdRef = (0, import_react.useRef)(normalizedConfig.session?.sessionId);
360
+ if (normalizedConfig.session?.sessionId) {
361
+ sessionIdRef.current = normalizedConfig.session.sessionId;
424
362
  } else if (prevConfiguredSessionIdRef.current) {
425
- sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
363
+ sessionIdRef.current = (0, import_core5.resolveSessionId)(defaultStorage, void 0);
426
364
  }
427
- const attemptIdRef = (0, import_react.useRef)(config.session?.attemptId);
428
- const userRef = (0, import_react.useRef)(config.session?.user);
429
- attemptIdRef.current = config.session?.attemptId;
430
- userRef.current = config.session?.user;
431
- const courseIdRef = (0, import_react.useRef)(config.courseId);
432
- courseIdRef.current = config.courseId;
433
- const lxpackBridgeModeRef = (0, import_react.useRef)(config.lxpack?.bridge ?? "auto");
434
- lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
435
- const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(config.plugins), [config.plugins]);
365
+ const attemptIdRef = (0, import_react.useRef)(normalizedConfig.session?.attemptId);
366
+ const userRef = (0, import_react.useRef)(normalizedConfig.session?.user);
367
+ attemptIdRef.current = normalizedConfig.session?.attemptId;
368
+ userRef.current = normalizedConfig.session?.user;
369
+ const courseIdRef = (0, import_react.useRef)(normalizedCourseId);
370
+ courseIdRef.current = normalizedCourseId;
371
+ const lxpackBridgeModeRef = (0, import_react.useRef)(normalizedConfig.lxpack?.bridge ?? "auto");
372
+ lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
373
+ const pluginHost = (0, import_react.useMemo)(() => createReactPluginHost(normalizedConfig.plugins), [normalizedConfig.plugins]);
436
374
  const pluginHostRef = (0, import_react.useRef)(pluginHost);
437
375
  pluginHostRef.current = pluginHost;
438
- const progressRef = (0, import_react.useRef)(createProgressController());
376
+ const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
439
377
  const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
440
- const prevCourseIdForProgressRef = (0, import_react.useRef)(config.courseId);
378
+ const prevCourseIdForProgressRef = (0, import_react.useRef)(normalizedCourseId);
441
379
  const pendingCourseIdResetRef = (0, import_react.useRef)(false);
442
- if (prevCourseIdForProgressRef.current !== config.courseId) {
443
- prevCourseIdForProgressRef.current = config.courseId;
444
- progressRef.current = createProgressController();
380
+ const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
381
+ const xapiCourseStartedSentOnClientRef = (0, import_react.useRef)(false);
382
+ if (prevUseV2RuntimeRef.current !== useV2Runtime) {
383
+ prevUseV2RuntimeRef.current = useV2Runtime;
384
+ if (useV2Runtime) {
385
+ headlessRef.current = (0, import_core8.createLessonkitRuntime)({
386
+ courseId: normalizedCourseId,
387
+ runtimeVersion: "v2",
388
+ session: normalizedConfig.session
389
+ });
390
+ progressRef.current = headlessRef.current.progress;
391
+ } else {
392
+ headlessRef.current = null;
393
+ progressRef.current = (0, import_core4.createProgressController)();
394
+ }
395
+ pendingCourseIdResetRef.current = true;
396
+ courseStartedEmittedToSinkRef.current = false;
397
+ } else if (useV2Runtime && !headlessRef.current) {
398
+ headlessRef.current = (0, import_core8.createLessonkitRuntime)({
399
+ courseId: normalizedCourseId,
400
+ runtimeVersion: "v2",
401
+ session: normalizedConfig.session
402
+ });
403
+ }
404
+ if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
405
+ prevCourseIdForProgressRef.current = normalizedCourseId;
406
+ if (useV2Runtime && headlessRef.current) {
407
+ headlessRef.current.resetForCourseChange(normalizedCourseId);
408
+ progressRef.current = headlessRef.current.progress;
409
+ } else {
410
+ progressRef.current = (0, import_core4.createProgressController)();
411
+ }
445
412
  pendingCourseIdResetRef.current = true;
446
413
  courseStartedEmittedToSinkRef.current = false;
447
414
  }
415
+ if (useV2Runtime && headlessRef.current) {
416
+ progressRef.current = headlessRef.current.progress;
417
+ }
448
418
  const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
449
419
  const syncProgress = (0, import_react.useCallback)(() => {
450
420
  setProgress(progressRef.current.getState());
@@ -454,16 +424,16 @@ function LessonkitProvider(props) {
454
424
  const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
455
425
  const xapiRef = (0, import_react.useRef)(null);
456
426
  const [xapi, setXapi] = (0, import_react.useState)(null);
457
- const prevXapiCourseIdRef = (0, import_react.useRef)(config.courseId);
458
- const xapiEnabled = config.xapi?.enabled;
459
- const xapiClient = config.xapi?.client;
460
- const xapiTransport = config.xapi?.transport;
461
- const courseId = config.courseId;
462
- const trackingEnabled = config.tracking?.enabled;
427
+ const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
428
+ const xapiEnabled = normalizedConfig.xapi?.enabled;
429
+ const xapiClient = normalizedConfig.xapi?.client;
430
+ const xapiTransport = normalizedConfig.xapi?.transport;
431
+ const courseId = normalizedCourseId;
432
+ const trackingEnabled = normalizedConfig.tracking?.enabled;
463
433
  useIsoLayoutEffect(() => {
464
434
  const courseChanged = prevXapiCourseIdRef.current !== courseId;
465
435
  if (courseChanged) {
466
- if (config.xapi?.client) {
436
+ if (normalizedConfig.xapi?.client) {
467
437
  const g = globalThis;
468
438
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
469
439
  console.warn(
@@ -474,20 +444,24 @@ function LessonkitProvider(props) {
474
444
  }
475
445
  xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
476
446
  prevXapiCourseIdRef.current = courseId;
447
+ xapiCourseStartedSentOnClientRef.current = false;
477
448
  }
478
449
  const prev = xapiRef.current;
479
- const next = createXapiClientFromConfig(config, xapiQueueRef.current);
450
+ const next = createXapiClientFromConfig(normalizedConfig, xapiQueueRef.current);
480
451
  xapiRef.current = next;
481
452
  setXapi(next);
482
- if (next && !prev) {
453
+ if (next) {
483
454
  const sessionId = sessionIdRef.current;
484
455
  const cid = courseIdRef.current;
485
- const trackingActive = isTrackingActive(config.tracking);
486
- const alreadyStarted = hasCourseStarted(defaultStorage, sessionId, cid);
487
- if (!trackingActive || alreadyStarted) {
456
+ const trackingActive = isTrackingActive(normalizedConfig.tracking);
457
+ const alreadyStarted = (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid);
458
+ const clientChanged = !prev || prev !== next;
459
+ const skipBootstrap = trackingActive && !alreadyStarted;
460
+ const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
461
+ if (needsBootstrap) {
488
462
  try {
489
463
  const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
490
- buildTrackEvent({
464
+ (0, import_core2.buildTelemetryEvent)({
491
465
  name: "course_started",
492
466
  courseId: cid,
493
467
  sessionId,
@@ -497,7 +471,10 @@ function LessonkitProvider(props) {
497
471
  );
498
472
  if (statement) {
499
473
  next.send(statement);
500
- markCourseStarted(defaultStorage, sessionId, cid);
474
+ if (!alreadyStarted) {
475
+ (0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
476
+ }
477
+ xapiCourseStartedSentOnClientRef.current = true;
501
478
  }
502
479
  } catch {
503
480
  }
@@ -522,46 +499,56 @@ function LessonkitProvider(props) {
522
499
  void prev?.flush();
523
500
  };
524
501
  }, [xapiEnabled, xapiClient, xapiTransport, courseId, trackingEnabled]);
525
- const trackingRef = (0, import_react.useRef)((0, import_core5.createTrackingClient)());
502
+ const trackingRef = (0, import_react.useRef)((0, import_core8.createTrackingClient)());
526
503
  const trackingClientForUnmountRef = (0, import_react.useRef)(trackingRef.current);
527
504
  const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
528
- const trackingSink = config.tracking?.sink;
529
- const trackingBatchSink = config.tracking?.batchSink;
530
- const batchEnabled = config.tracking?.batch?.enabled;
531
- const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
532
- const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
505
+ const trackingSink = normalizedConfig.tracking?.sink;
506
+ const trackingBatchSink = normalizedConfig.tracking?.batchSink;
507
+ const batchEnabled = normalizedConfig.tracking?.batch?.enabled;
508
+ const batchFlushIntervalMs = normalizedConfig.tracking?.batch?.flushIntervalMs;
509
+ const batchMaxBatchSize = normalizedConfig.tracking?.batch?.maxBatchSize;
533
510
  const buildCurrentPluginCtx = (0, import_react.useCallback)(
534
511
  () => buildPluginContext({
535
512
  courseId: courseIdRef.current,
536
513
  sessionId: sessionIdRef.current,
537
- attemptId: attemptIdRef.current
514
+ attemptId: attemptIdRef.current,
515
+ user: userRef.current
538
516
  }),
539
517
  []
540
518
  );
541
519
  useIsoLayoutEffect(() => {
542
520
  const prev = trackingRef.current;
543
- const baseSink = config.tracking?.sink;
521
+ const baseSink = normalizedConfig.tracking?.sink;
522
+ const userBatchSink = normalizedConfig.tracking?.batchSink;
523
+ assertTrackingSinkConfig(normalizedConfig.tracking);
544
524
  const sink = pluginHostRef.current && baseSink ? pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx) ?? baseSink : baseSink;
545
- const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
546
- const delivered = pluginHostRef.current.deliverTelemetryBatch(
547
- events,
548
- buildCurrentPluginCtx()
549
- );
550
- return config.tracking.batchSink(delivered);
551
- } : config.tracking?.batchSink;
525
+ const batchSink = pluginHostRef.current && userBatchSink ? async (events) => {
526
+ const host = pluginHostRef.current;
527
+ const ctx = buildCurrentPluginCtx();
528
+ const delivered = host.deliverTelemetryBatch(events, ctx);
529
+ const perEventForBatch = [];
530
+ const collector = (event) => {
531
+ perEventForBatch.push(event);
532
+ };
533
+ const composedPerEvent = host.composeTrackingSink(collector, buildCurrentPluginCtx) ?? collector;
534
+ for (const event of delivered) {
535
+ await Promise.resolve(composedPerEvent(event));
536
+ }
537
+ return userBatchSink(perEventForBatch);
538
+ } : userBatchSink;
552
539
  const next = createTrackingClientFromConfig({
553
- tracking: { ...config.tracking, sink, batchSink }
540
+ tracking: { ...normalizedConfig.tracking, sink, batchSink }
554
541
  });
555
542
  trackingRef.current = next;
556
543
  trackingClientForUnmountRef.current = next;
557
544
  setTracking(next);
558
545
  const sessionId = sessionIdRef.current;
559
546
  const cid = courseIdRef.current;
560
- const trackingActive = isTrackingActive(config.tracking);
547
+ const trackingActive = isTrackingActive(normalizedConfig.tracking);
561
548
  if (!trackingActive) {
562
549
  courseStartedEmittedToSinkRef.current = false;
563
- } else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
564
- const emitted = emitCourseStarted({
550
+ } else if (!courseStartedEmittedToSinkRef.current) {
551
+ const emitted = emitPendingCourseStarted({
565
552
  pluginHost: pluginHostRef.current,
566
553
  tracking: next,
567
554
  xapi: xapiRef.current,
@@ -570,8 +557,12 @@ function LessonkitProvider(props) {
570
557
  courseId: cid,
571
558
  attemptId: attemptIdRef.current,
572
559
  user: userRef.current,
573
- lxpackBridge: lxpackBridgeModeRef.current
560
+ lxpackBridge: lxpackBridgeModeRef.current,
561
+ extraSinks: extraSinksRef.current
574
562
  });
563
+ if (emitted) {
564
+ (0, import_core5.markCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid);
565
+ }
575
566
  courseStartedEmittedToSinkRef.current = emitted;
576
567
  } else if (trackingActive) {
577
568
  courseStartedEmittedToSinkRef.current = true;
@@ -588,8 +579,8 @@ function LessonkitProvider(props) {
588
579
  batchEnabled,
589
580
  batchFlushIntervalMs,
590
581
  batchMaxBatchSize,
591
- config.plugins,
592
- config.courseId,
582
+ normalizedConfig.plugins,
583
+ normalizedCourseId,
593
584
  buildCurrentPluginCtx
594
585
  ]);
595
586
  const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
@@ -601,14 +592,32 @@ function LessonkitProvider(props) {
601
592
  pluginCtx: buildPluginContext({
602
593
  courseId: courseIdRef.current,
603
594
  sessionId: sessionIdRef.current,
604
- attemptId: attemptIdRef.current
595
+ attemptId: attemptIdRef.current,
596
+ user: userRef.current
605
597
  }),
606
- lxpackBridge: lxpackBridgeModeRef.current
598
+ lxpackBridge: lxpackBridgeModeRef.current,
599
+ extraSinks: extraSinksRef.current
607
600
  });
608
601
  }, []);
602
+ const emitLifecycleEvent = (0, import_react.useCallback)(
603
+ (name, data, lessonId) => {
604
+ const event = (0, import_core2.tryBuildTelemetryEvent)({
605
+ name,
606
+ courseId: courseIdRef.current,
607
+ lessonId: lessonId ?? activeLessonIdRef.current,
608
+ sessionId: sessionIdRef.current,
609
+ attemptId: attemptIdRef.current,
610
+ user: userRef.current,
611
+ data
612
+ });
613
+ if (!event) return;
614
+ emitWithBridge(trackingRef.current, event);
615
+ },
616
+ [emitWithBridge]
617
+ );
609
618
  const track = (0, import_react.useCallback)(
610
619
  (name, data, opts) => {
611
- const event = tryBuildTrackEvent({
620
+ const event = (0, import_core2.tryBuildTelemetryEvent)({
612
621
  name,
613
622
  courseId: courseIdRef.current,
614
623
  lessonId: opts?.lessonId ?? activeLessonIdRef.current,
@@ -626,7 +635,7 @@ function LessonkitProvider(props) {
626
635
  if (!pendingCourseIdResetRef.current) return;
627
636
  pendingCourseIdResetRef.current = false;
628
637
  syncProgress();
629
- if (!isTrackingActive(config.tracking)) return;
638
+ if (!isTrackingActive(normalizedConfig.tracking)) return;
630
639
  const sessionId = sessionIdRef.current;
631
640
  const cid = courseIdRef.current;
632
641
  void (async () => {
@@ -634,8 +643,8 @@ function LessonkitProvider(props) {
634
643
  await trackingRef.current?.flush?.();
635
644
  } catch {
636
645
  }
637
- if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
638
- const emitted = emitCourseStarted({
646
+ if (!courseStartedEmittedToSinkRef.current) {
647
+ const emitted = emitPendingCourseStarted({
639
648
  pluginHost: pluginHostRef.current,
640
649
  tracking: trackingRef.current,
641
650
  xapi: xapiRef.current,
@@ -644,12 +653,13 @@ function LessonkitProvider(props) {
644
653
  courseId: cid,
645
654
  attemptId: attemptIdRef.current,
646
655
  user: userRef.current,
647
- lxpackBridge: lxpackBridgeModeRef.current
656
+ lxpackBridge: lxpackBridgeModeRef.current,
657
+ extraSinks: extraSinksRef.current
648
658
  });
649
659
  courseStartedEmittedToSinkRef.current = emitted;
650
660
  }
651
661
  })();
652
- }, [config.courseId, config.tracking?.enabled, syncProgress]);
662
+ }, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
653
663
  const emitLessonCompleted = (0, import_react.useCallback)(
654
664
  (lessonId, durationMs) => {
655
665
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
@@ -661,13 +671,19 @@ function LessonkitProvider(props) {
661
671
  );
662
672
  const completeLesson = (0, import_react.useCallback)(
663
673
  (lessonId) => {
674
+ if (useV2Runtime && headlessRef.current) {
675
+ headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
676
+ syncProgress();
677
+ void Promise.resolve(trackingRef.current?.flush?.());
678
+ return;
679
+ }
664
680
  const result = progressRef.current.completeLesson(lessonId, Date.now());
665
681
  if (!result.didComplete) return;
666
682
  syncProgress();
667
683
  emitLessonCompleted(lessonId, result.durationMs);
668
684
  void Promise.resolve(trackingRef.current?.flush?.());
669
685
  },
670
- [syncProgress, emitLessonCompleted]
686
+ [syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
671
687
  );
672
688
  (0, import_react.useEffect)(() => {
673
689
  return () => {
@@ -691,8 +707,19 @@ function LessonkitProvider(props) {
691
707
  }, []);
692
708
  const setActiveLesson = (0, import_react.useCallback)(
693
709
  (lessonId) => {
710
+ if (useV2Runtime && headlessRef.current) {
711
+ headlessRef.current.setActiveLesson(lessonId, emitLifecycleEvent);
712
+ syncProgress();
713
+ void Promise.resolve(trackingRef.current?.flush?.());
714
+ return;
715
+ }
694
716
  const current = progressRef.current.getState();
695
717
  if (current.activeLessonId === lessonId) return;
718
+ if (current.completedLessonIds.has(lessonId)) {
719
+ progressRef.current.setActiveLesson(lessonId, Date.now());
720
+ syncProgress();
721
+ return;
722
+ }
696
723
  const previous = current.activeLessonId;
697
724
  if (previous && previous !== lessonId) {
698
725
  const completed = progressRef.current.completeLesson(previous, Date.now());
@@ -705,9 +732,15 @@ function LessonkitProvider(props) {
705
732
  syncProgress();
706
733
  track("lesson_started", { lessonId }, { lessonId });
707
734
  },
708
- [track, syncProgress, emitLessonCompleted]
735
+ [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]
709
736
  );
710
737
  const completeCourse = (0, import_react.useCallback)(() => {
738
+ if (useV2Runtime && headlessRef.current) {
739
+ headlessRef.current.completeCourse(emitLifecycleEvent);
740
+ syncProgress();
741
+ void trackingRef.current?.flush?.();
742
+ return;
743
+ }
711
744
  const current = progressRef.current.getState();
712
745
  if (current.activeLessonId) {
713
746
  const lessonResult = progressRef.current.completeLesson(current.activeLessonId, Date.now());
@@ -720,24 +753,37 @@ function LessonkitProvider(props) {
720
753
  syncProgress();
721
754
  track("course_completed");
722
755
  void trackingRef.current?.flush?.();
723
- }, [track, syncProgress, emitLessonCompleted]);
724
- const sessionUser = config.session?.user;
725
- const sessionAttemptId = config.session?.attemptId;
726
- const sessionConfiguredId = config.session?.sessionId;
756
+ }, [track, syncProgress, emitLessonCompleted, useV2Runtime, emitLifecycleEvent]);
757
+ const sessionUser = normalizedConfig.session?.user;
758
+ const sessionUserKey = (0, import_react.useMemo)(
759
+ () => sessionUser ? JSON.stringify(sessionUser) : "",
760
+ [sessionUser]
761
+ );
762
+ const sessionAttemptId = normalizedConfig.session?.attemptId;
763
+ const sessionConfiguredId = normalizedConfig.session?.sessionId;
764
+ (0, import_react.useEffect)(() => {
765
+ if (useV2Runtime && headlessRef.current) {
766
+ headlessRef.current.updateConfig({
767
+ courseId: normalizedCourseId,
768
+ session: normalizedConfig.session
769
+ });
770
+ }
771
+ }, [useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey, normalizedConfig.session]);
727
772
  (0, import_react.useEffect)(() => {
728
773
  if (!pluginHost) return;
729
774
  const ctx = buildPluginContext({
730
775
  courseId: courseIdRef.current,
731
776
  sessionId: sessionIdRef.current,
732
- attemptId: attemptIdRef.current
777
+ attemptId: attemptIdRef.current,
778
+ user: userRef.current
733
779
  });
734
780
  pluginHost.setupAll(ctx);
735
781
  return () => {
736
782
  pluginHost.disposeAll();
737
783
  };
738
- }, [pluginHost, config.courseId, sessionAttemptId, sessionConfiguredId]);
784
+ }, [pluginHost, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
739
785
  (0, import_react.useEffect)(() => {
740
- const nextConfigured = config.session?.sessionId;
786
+ const nextConfigured = normalizedConfig.session?.sessionId;
741
787
  const prevConfigured = prevConfiguredSessionIdRef.current;
742
788
  if (nextConfigured === prevConfigured) return;
743
789
  prevConfiguredSessionIdRef.current = nextConfigured;
@@ -745,23 +791,23 @@ function LessonkitProvider(props) {
745
791
  if (nextConfigured) {
746
792
  const fromIds = /* @__PURE__ */ new Set();
747
793
  if (prevConfigured) fromIds.add(prevConfigured);
748
- const tabId = getTabSessionId(defaultStorage);
794
+ const tabId = (0, import_core5.getTabSessionId)(defaultStorage);
749
795
  if (tabId) fromIds.add(tabId);
750
796
  for (const fromId of fromIds) {
751
797
  if (fromId !== nextConfigured) {
752
- migrateCourseStartedMark(defaultStorage, fromId, nextConfigured, cid);
798
+ (0, import_core5.migrateCourseStartedMark)(defaultStorage, fromId, nextConfigured, cid);
753
799
  }
754
800
  }
755
801
  sessionIdRef.current = nextConfigured;
756
802
  } else if (prevConfigured) {
757
- const nextAuto = resolveSessionId(defaultStorage, void 0);
758
- migrateCourseStartedMark(defaultStorage, prevConfigured, nextAuto, cid);
803
+ const nextAuto = (0, import_core5.resolveSessionId)(defaultStorage, void 0);
804
+ (0, import_core5.migrateCourseStartedMark)(defaultStorage, prevConfigured, nextAuto, cid);
759
805
  sessionIdRef.current = nextAuto;
760
806
  }
761
- }, [sessionConfiguredId, config.courseId]);
807
+ }, [sessionConfiguredId, normalizedCourseId]);
762
808
  const runtime = (0, import_react.useMemo)(
763
809
  () => ({
764
- config,
810
+ config: normalizedConfig,
765
811
  tracking,
766
812
  xapi,
767
813
  session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
@@ -773,7 +819,7 @@ function LessonkitProvider(props) {
773
819
  plugins: pluginHost
774
820
  }),
775
821
  [
776
- config,
822
+ normalizedConfig,
777
823
  tracking,
778
824
  xapi,
779
825
  progress,
@@ -787,13 +833,21 @@ function LessonkitProvider(props) {
787
833
  sessionConfiguredId
788
834
  ]
789
835
  );
836
+ return runtime;
837
+ }
838
+
839
+ // src/context.tsx
840
+ var import_jsx_runtime = require("react/jsx-runtime");
841
+ var LessonkitContext = (0, import_react2.createContext)(null);
842
+ function LessonkitProvider(props) {
843
+ const runtime = useLessonkitProviderRuntime(props.config);
790
844
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LessonkitContext.Provider, { value: runtime, children: props.children });
791
845
  }
792
846
 
793
847
  // src/hooks.ts
794
- var import_react2 = require("react");
848
+ var import_react3 = require("react");
795
849
  function useLessonkit() {
796
- const ctx = (0, import_react2.useContext)(LessonkitContext);
850
+ const ctx = (0, import_react3.useContext)(LessonkitContext);
797
851
  if (!ctx) throw new Error("LessonKit: missing LessonkitProvider");
798
852
  return ctx;
799
853
  }
@@ -803,52 +857,80 @@ function useProgress() {
803
857
  }
804
858
  function useTracking() {
805
859
  const { track } = useLessonkit();
806
- return (0, import_react2.useMemo)(() => ({ track }), [track]);
860
+ return (0, import_react3.useMemo)(() => ({ track }), [track]);
807
861
  }
808
862
  function useCompletion() {
809
863
  const { completeLesson, completeCourse } = useLessonkit();
810
- return (0, import_react2.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
864
+ return (0, import_react3.useMemo)(() => ({ completeLesson, completeCourse }), [completeLesson, completeCourse]);
811
865
  }
812
- function useQuizState() {
866
+ function useQuizState(enclosingLessonId) {
813
867
  const { track } = useLessonkit();
814
- return (0, import_react2.useMemo)(
868
+ const trackOpts = enclosingLessonId ? { lessonId: enclosingLessonId } : void 0;
869
+ return (0, import_react3.useMemo)(
815
870
  () => ({
816
871
  answer: (opts) => {
817
- track("quiz_answered", opts);
872
+ track("quiz_answered", opts, trackOpts);
818
873
  },
819
874
  complete: (opts) => {
820
- track("quiz_completed", opts);
875
+ track("quiz_completed", opts, trackOpts);
821
876
  }
822
877
  }),
823
- [track]
878
+ [track, enclosingLessonId]
824
879
  );
825
880
  }
826
881
 
882
+ // src/lessonContext.tsx
883
+ var import_react4 = require("react");
884
+ var LessonContext = (0, import_react4.createContext)(void 0);
885
+ function useEnclosingLessonId() {
886
+ return (0, import_react4.useContext)(LessonContext);
887
+ }
888
+
827
889
  // src/runtime/validateComponentId.ts
828
- var import_core6 = require("@lessonkit/core");
829
- var warnedPaths = /* @__PURE__ */ new Set();
830
- function isDevEnvironment2() {
890
+ var import_core9 = require("@lessonkit/core");
891
+ function isDevEnvironment3() {
831
892
  const g = globalThis;
832
893
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
833
894
  }
834
- function warnInvalidComponentId(id, path) {
835
- if (!isDevEnvironment2()) return;
836
- const key = `${path}:${String(id)}`;
837
- if (warnedPaths.has(key)) return;
838
- const result = (0, import_core6.validateId)(id, path);
839
- if (result.ok) return;
840
- warnedPaths.add(key);
841
- const detail = result.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
842
- console.warn(`[lessonkit] invalid ${path} \u2014 ${detail}`);
895
+ function normalizeComponentId(id, path) {
896
+ return (0, import_core9.assertValidId)(id, path);
897
+ }
898
+
899
+ // src/runtime/lessonMountRegistry.ts
900
+ var mountCounts = /* @__PURE__ */ new Map();
901
+ var warnedConcurrentLessons = false;
902
+ function registerLessonMount(lessonId) {
903
+ if (isDevEnvironment3() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
904
+ warnedConcurrentLessons = true;
905
+ console.warn(
906
+ "[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
907
+ );
908
+ }
909
+ mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
910
+ return () => {
911
+ const next = (mountCounts.get(lessonId) ?? 1) - 1;
912
+ if (next <= 0) {
913
+ mountCounts.delete(lessonId);
914
+ } else {
915
+ mountCounts.set(lessonId, next);
916
+ }
917
+ };
918
+ }
919
+ function getLessonMountCount(lessonId) {
920
+ return mountCounts.get(lessonId) ?? 0;
843
921
  }
844
922
 
845
923
  // src/components.tsx
846
924
  var import_jsx_runtime2 = require("react/jsx-runtime");
925
+ var warnedQuizOutsideLesson = false;
926
+ function resetQuizWarningsForTests() {
927
+ warnedQuizOutsideLesson = false;
928
+ }
847
929
  function Course(props) {
848
- warnInvalidComponentId(props.courseId, "courseId");
849
- const providerConfig = (0, import_react3.useMemo)(
850
- () => ({ ...props.config, courseId: props.courseId }),
851
- [props.config, props.courseId]
930
+ const courseId = (0, import_react5.useMemo)(() => normalizeComponentId(props.courseId, "courseId"), [props.courseId]);
931
+ const providerConfig = (0, import_react5.useMemo)(
932
+ () => ({ ...props.config, courseId }),
933
+ [props.config, courseId]
852
934
  );
853
935
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": props.title, children: [
854
936
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { children: props.title }),
@@ -856,41 +938,64 @@ function Course(props) {
856
938
  ] }) });
857
939
  }
858
940
  function Lesson(props) {
859
- warnInvalidComponentId(props.lessonId, "lessonId");
941
+ const lessonId = (0, import_react5.useMemo)(() => normalizeComponentId(props.lessonId, "lessonId"), [props.lessonId]);
942
+ const autoComplete = props.autoCompleteOnUnmount !== false;
860
943
  const { setActiveLesson, config } = useLessonkit();
861
944
  const { completeLesson } = useCompletion();
862
- const id = props.lessonId;
863
- const lessonMountGenerationRef = (0, import_react3.useRef)(0);
864
- (0, import_react3.useEffect)(() => {
945
+ const lessonMountGenerationRef = (0, import_react5.useRef)(0);
946
+ (0, import_react5.useEffect)(() => {
947
+ const unregister = registerLessonMount(lessonId);
865
948
  const generation = ++lessonMountGenerationRef.current;
866
- setActiveLesson(id);
949
+ setActiveLesson(lessonId);
867
950
  return () => {
868
- const lessonId = id;
951
+ unregister();
952
+ if (getLessonMountCount(lessonId) > 0) {
953
+ return;
954
+ }
955
+ if (!autoComplete) return;
869
956
  queueMicrotask(() => {
870
957
  if (lessonMountGenerationRef.current !== generation) return;
871
958
  completeLesson(lessonId);
872
959
  });
873
960
  };
874
- }, [id, config.courseId, setActiveLesson, completeLesson]);
875
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
961
+ }, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
962
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonContext.Provider, { value: lessonId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
876
963
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: props.title }),
877
964
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
878
- ] });
965
+ ] }) });
879
966
  }
880
967
  function Scenario(props) {
881
- if (props.blockId !== void 0) warnInvalidComponentId(props.blockId, "blockId");
882
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": props.blockId, children: props.children });
968
+ const blockId = (0, import_react5.useMemo)(
969
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
970
+ [props.blockId]
971
+ );
972
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": blockId, children: props.children });
883
973
  }
884
974
  function Reflection(props) {
885
- if (props.blockId !== void 0) warnInvalidComponentId(props.blockId, "blockId");
886
- const promptId = (0, import_react3.useId)();
887
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": props.blockId, children: [
975
+ const blockId = (0, import_react5.useMemo)(
976
+ () => props.blockId !== void 0 ? normalizeComponentId(props.blockId, "blockId") : void 0,
977
+ [props.blockId]
978
+ );
979
+ const promptId = (0, import_react5.useId)();
980
+ const hintId = (0, import_react5.useId)();
981
+ const [internalValue, setInternalValue] = (0, import_react5.useState)("");
982
+ const isControlled = props.value !== void 0;
983
+ const value = isControlled ? props.value : internalValue;
984
+ const handleChange = (event) => {
985
+ if (!isControlled) setInternalValue(event.target.value);
986
+ props.onChange?.(event.target.value);
987
+ };
988
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": blockId, children: [
888
989
  props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: promptId, children: props.prompt }) : null,
990
+ props.hint ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: hintId, style: import_accessibility.visuallyHiddenStyle, children: props.hint }) : null,
889
991
  props.children,
890
992
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
891
993
  "textarea",
892
994
  {
995
+ value,
996
+ onChange: handleChange,
893
997
  "aria-labelledby": props.prompt ? promptId : void 0,
998
+ "aria-describedby": props.hint ? hintId : void 0,
894
999
  "aria-label": props.prompt ? void 0 : "Reflection response"
895
1000
  }
896
1001
  )
@@ -909,18 +1014,35 @@ function KnowledgeCheck(props) {
909
1014
  );
910
1015
  }
911
1016
  function Quiz(props) {
912
- warnInvalidComponentId(props.checkId, "checkId");
913
- const quiz = useQuizState();
914
- const { plugins, config, progress, session } = useLessonkit();
915
- const [selected, setSelected] = (0, import_react3.useState)(null);
916
- const [selectionCorrect, setSelectionCorrect] = (0, import_react3.useState)(null);
917
- const completedRef = (0, import_react3.useRef)(false);
918
- const questionId = (0, import_react3.useId)();
919
- (0, import_react3.useEffect)(() => {
1017
+ const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
1018
+ const enclosingLessonId = useEnclosingLessonId();
1019
+ const missingLesson = enclosingLessonId === void 0;
1020
+ (0, import_react5.useEffect)(() => {
1021
+ if (!missingLesson || isDevEnvironment3()) return;
1022
+ if (!warnedQuizOutsideLesson) {
1023
+ warnedQuizOutsideLesson = true;
1024
+ console.error(
1025
+ "[lessonkit] <Quiz> must be wrapped in <Lesson>; quiz telemetry will not be emitted."
1026
+ );
1027
+ }
1028
+ }, [missingLesson]);
1029
+ if (missingLesson && isDevEnvironment3()) {
1030
+ throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
1031
+ }
1032
+ const quiz = useQuizState(enclosingLessonId);
1033
+ const { plugins, config, session } = useLessonkit();
1034
+ const [selected, setSelected] = (0, import_react5.useState)(null);
1035
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react5.useState)(null);
1036
+ const [quizPassed, setQuizPassed] = (0, import_react5.useState)(false);
1037
+ const completedRef = (0, import_react5.useRef)(false);
1038
+ const questionId = (0, import_react5.useId)();
1039
+ const choicesKey = props.choices.join("\0");
1040
+ (0, import_react5.useEffect)(() => {
920
1041
  completedRef.current = false;
1042
+ setQuizPassed(false);
921
1043
  setSelected(null);
922
1044
  setSelectionCorrect(null);
923
- }, [props.checkId, props.answer, props.question, config.courseId]);
1045
+ }, [checkId, props.answer, props.question, config.courseId, enclosingLessonId, choicesKey]);
924
1046
  const isChoiceCorrect = (choice, custom) => {
925
1047
  if (!custom) return choice === props.answer;
926
1048
  if (custom.passed !== void 0) return custom.passed;
@@ -929,7 +1051,11 @@ function Quiz(props) {
929
1051
  }
930
1052
  return choice === props.answer;
931
1053
  };
932
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
1054
+ if (missingLesson) {
1055
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
1056
+ }
1057
+ const passed = quizPassed;
1058
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
933
1059
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
934
1060
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
935
1061
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
@@ -941,17 +1067,21 @@ function Quiz(props) {
941
1067
  name: questionId,
942
1068
  value: c,
943
1069
  checked: selected === c,
1070
+ disabled: passed,
1071
+ "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
944
1072
  onChange: () => {
1073
+ if (passed) return;
945
1074
  setSelected(c);
946
1075
  const pluginCtx = buildPluginContext({
947
1076
  courseId: config.courseId,
948
1077
  sessionId: session.sessionId,
949
- attemptId: session.attemptId
1078
+ attemptId: session.attemptId,
1079
+ user: session.user
950
1080
  });
951
1081
  const custom = plugins?.scoreAssessment(
952
1082
  {
953
- checkId: props.checkId,
954
- lessonId: progress.activeLessonId,
1083
+ checkId,
1084
+ lessonId: enclosingLessonId,
955
1085
  response: c
956
1086
  },
957
1087
  pluginCtx
@@ -959,18 +1089,20 @@ function Quiz(props) {
959
1089
  const correct = isChoiceCorrect(c, custom);
960
1090
  setSelectionCorrect(correct);
961
1091
  quiz.answer({
962
- checkId: props.checkId,
1092
+ checkId,
963
1093
  question: props.question,
964
1094
  choice: c,
965
1095
  correct
966
1096
  });
967
1097
  if (correct && !completedRef.current) {
968
1098
  completedRef.current = true;
1099
+ setQuizPassed(true);
1100
+ const maxScore = custom?.maxScore ?? 1;
969
1101
  quiz.complete({
970
- checkId: props.checkId,
1102
+ checkId,
971
1103
  score: custom?.score ?? 1,
972
- maxScore: custom?.maxScore ?? 1,
973
- passingScore: props.passingScore ?? 1
1104
+ maxScore,
1105
+ passingScore: props.passingScore ?? maxScore
974
1106
  });
975
1107
  }
976
1108
  }
@@ -982,20 +1114,40 @@ function Quiz(props) {
982
1114
  selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
983
1115
  ] });
984
1116
  }
985
- function ProgressTracker() {
1117
+ function ProgressTracker(props) {
986
1118
  const { progress } = useLessonkit();
987
1119
  const completed = progress.completedLessonIds.size;
988
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
1120
+ if (props.totalLessons != null) {
1121
+ const total = props.totalLessons;
1122
+ const displayed = Math.min(completed, total);
1123
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1124
+ "div",
1125
+ {
1126
+ role: "progressbar",
1127
+ "aria-valuemin": 0,
1128
+ "aria-valuemax": total,
1129
+ "aria-valuenow": displayed,
1130
+ "aria-label": "Lessons completed",
1131
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
1132
+ "Lessons completed: ",
1133
+ displayed,
1134
+ " of ",
1135
+ total
1136
+ ] })
1137
+ }
1138
+ ) });
1139
+ }
1140
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("aside", { "aria-label": "Progress", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
989
1141
  "Lessons completed: ",
990
1142
  completed
991
1143
  ] }) });
992
1144
  }
993
1145
 
994
1146
  // src/index.tsx
995
- var import_core7 = require("@lessonkit/core");
1147
+ var import_core10 = require("@lessonkit/core");
996
1148
 
997
1149
  // src/theme/ThemeProvider.tsx
998
- var import_react4 = __toESM(require("react"), 1);
1150
+ var import_react6 = __toESM(require("react"), 1);
999
1151
  var import_themes = require("@lessonkit/themes");
1000
1152
 
1001
1153
  // src/theme/applyCssVariables.ts
@@ -1015,8 +1167,8 @@ function applyCssVariables(target, vars, previousKeys) {
1015
1167
 
1016
1168
  // src/theme/ThemeProvider.tsx
1017
1169
  var import_jsx_runtime3 = require("react/jsx-runtime");
1018
- var ThemeContext = (0, import_react4.createContext)(null);
1019
- var useIsoLayoutEffect2 = typeof window !== "undefined" ? import_react4.useLayoutEffect : import_react4.default.useEffect;
1170
+ var ThemeContext = (0, import_react6.createContext)(null);
1171
+ var useIsoLayoutEffect2 = typeof window !== "undefined" ? import_react6.useLayoutEffect : import_react6.default.useEffect;
1020
1172
  function getSystemMode() {
1021
1173
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1022
1174
  return "light";
@@ -1034,7 +1186,7 @@ function ThemeProvider(props) {
1034
1186
  const preset = props.preset ?? "default";
1035
1187
  const mode = props.mode ?? "light";
1036
1188
  const targetKind = props.target ?? "document";
1037
- const [resolvedMode, setResolvedMode] = (0, import_react4.useState)(
1189
+ const [resolvedMode, setResolvedMode] = (0, import_react6.useState)(
1038
1190
  () => mode === "system" ? getSystemMode() : mode
1039
1191
  );
1040
1192
  useIsoLayoutEffect2(() => {
@@ -1050,20 +1202,20 @@ function ThemeProvider(props) {
1050
1202
  return () => mq.removeEventListener("change", onChange);
1051
1203
  }, [mode]);
1052
1204
  const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
1053
- const effectiveTheme = (0, import_react4.useMemo)(() => {
1205
+ const effectiveTheme = (0, import_react6.useMemo)(() => {
1054
1206
  const modeBase = resolveModeBase(mode, dataTheme);
1055
1207
  const base = preset === "default" ? modeBase : preset === "brand" ? (0, import_themes.mergeThemes)(modeBase, import_themes.brandThemeOverrides) : (0, import_themes.mergeThemes)(modeBase, (0, import_themes.getPresetTheme)(preset));
1056
1208
  return (0, import_themes.mergeThemes)(base, props.theme ?? {});
1057
1209
  }, [preset, mode, dataTheme, props.theme]);
1058
- const hostRef = (0, import_react4.useRef)(null);
1059
- const appliedKeysRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
1210
+ const hostRef = (0, import_react6.useRef)(null);
1211
+ const appliedKeysRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
1060
1212
  useIsoLayoutEffect2(() => {
1061
1213
  if (targetKind === "document" && typeof document !== "undefined") {
1062
1214
  document.documentElement.setAttribute("data-lk-theme", dataTheme);
1063
1215
  return () => document.documentElement.removeAttribute("data-lk-theme");
1064
1216
  }
1065
1217
  }, [targetKind, dataTheme]);
1066
- const inject = (0, import_react4.useCallback)(() => {
1218
+ const inject = (0, import_react6.useCallback)(() => {
1067
1219
  const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
1068
1220
  const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
1069
1221
  if (!el) return;
@@ -1080,7 +1232,7 @@ function ThemeProvider(props) {
1080
1232
  appliedKeysRef.current = /* @__PURE__ */ new Set();
1081
1233
  };
1082
1234
  }, [inject, targetKind]);
1083
- const value = (0, import_react4.useMemo)(
1235
+ const value = (0, import_react6.useMemo)(
1084
1236
  () => ({
1085
1237
  theme: effectiveTheme,
1086
1238
  preset,
@@ -1095,7 +1247,7 @@ function ThemeProvider(props) {
1095
1247
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
1096
1248
  }
1097
1249
  function useTheme() {
1098
- const ctx = (0, import_react4.useContext)(ThemeContext);
1250
+ const ctx = (0, import_react6.useContext)(ThemeContext);
1099
1251
  if (!ctx) {
1100
1252
  throw new Error("useTheme must be used within a ThemeProvider");
1101
1253
  }
@@ -1142,6 +1294,12 @@ var BLOCK_CATALOG = [
1142
1294
  props: [
1143
1295
  { name: "title", type: "string", required: true, description: "Lesson title shown in the h2." },
1144
1296
  { name: "lessonId", type: "LessonId", required: true, description: "Stable lesson identifier for telemetry and packaging." },
1297
+ {
1298
+ name: "autoCompleteOnUnmount",
1299
+ type: "boolean",
1300
+ required: false,
1301
+ description: "When false, unmount does not emit lesson_completed (default true)."
1302
+ },
1145
1303
  { name: "children", type: "ReactNode", required: true, description: "Scenario, Quiz, Reflection, and other blocks." }
1146
1304
  ],
1147
1305
  requiredIds: ["lessonId"],
@@ -1194,6 +1352,9 @@ var BLOCK_CATALOG = [
1194
1352
  props: [
1195
1353
  { name: "blockId", type: "BlockId", required: false, description: "Optional stable block id for interaction telemetry URNs." },
1196
1354
  { name: "prompt", type: "string", required: false, description: "Reflection question or instruction." },
1355
+ { name: "hint", type: "string", required: false, description: "Optional hint linked via aria-describedby." },
1356
+ { name: "value", type: "string", required: false, description: "Controlled textarea value." },
1357
+ { name: "onChange", type: "(value: string) => void", required: false, description: "Called when the learner edits the textarea." },
1197
1358
  { name: "children", type: "ReactNode", required: false, description: "Optional content above the textarea." }
1198
1359
  ],
1199
1360
  requiredIds: [],
@@ -1212,6 +1373,7 @@ var BLOCK_CATALOG = [
1212
1373
  },
1213
1374
  telemetry: {
1214
1375
  emits: [],
1376
+ requiresActiveLesson: true,
1215
1377
  manualTracking: "useTracking().track('interaction', { kind, blockId, payload }) on submit or blur"
1216
1378
  }
1217
1379
  },
@@ -1249,7 +1411,14 @@ var BLOCK_CATALOG = [
1249
1411
  type: "ProgressTracker",
1250
1412
  category: "chrome",
1251
1413
  description: "Displays count of completed lessons from runtime progress state.",
1252
- props: [],
1414
+ props: [
1415
+ {
1416
+ name: "totalLessons",
1417
+ type: "number",
1418
+ required: false,
1419
+ description: "When set, renders role=progressbar with aria-valuenow/max."
1420
+ }
1421
+ ],
1253
1422
  requiredIds: [],
1254
1423
  parentConstraints: ["Course"],
1255
1424
  a11y: {
@@ -1302,9 +1471,15 @@ function getBlockCatalogEntry(type) {
1302
1471
  ThemeProvider,
1303
1472
  blockCatalogVersion,
1304
1473
  buildBlockCatalog,
1305
- createPluginHost,
1306
- defineLessonkitPlugin,
1474
+ buildTelemetryEvent,
1475
+ createLessonkitRuntime,
1476
+ createPluginRegistry,
1477
+ createTelemetryPipeline,
1478
+ defineAssessmentPlugin,
1479
+ defineLifecyclePlugin,
1480
+ defineTelemetryPlugin,
1307
1481
  getBlockCatalogEntry,
1482
+ resetQuizWarningsForTests,
1308
1483
  useCompletion,
1309
1484
  useLessonkit,
1310
1485
  useProgress,