@lessonkit/react 0.3.1 → 0.5.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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.tsx
@@ -28,10 +38,12 @@ __export(index_exports, {
28
38
  Quiz: () => Quiz,
29
39
  Reflection: () => Reflection,
30
40
  Scenario: () => Scenario,
41
+ ThemeProvider: () => ThemeProvider,
31
42
  useCompletion: () => useCompletion,
32
43
  useLessonkit: () => useLessonkit,
33
44
  useProgress: () => useProgress,
34
45
  useQuizState: () => useQuizState,
46
+ useTheme: () => useTheme,
35
47
  useTracking: () => useTracking
36
48
  });
37
49
  module.exports = __toCommonJS(index_exports);
@@ -42,8 +54,94 @@ var import_accessibility = require("@lessonkit/accessibility");
42
54
 
43
55
  // src/context.tsx
44
56
  var import_react = require("react");
45
- var import_core2 = require("@lessonkit/core");
57
+ var import_core3 = require("@lessonkit/core");
58
+ var import_xapi3 = require("@lessonkit/xapi");
59
+
60
+ // src/runtime/emitTelemetry.ts
61
+ var import_core = require("@lessonkit/core");
46
62
  var import_xapi = require("@lessonkit/xapi");
63
+ var warnedMissingCourseId = false;
64
+ function isDevEnvironment() {
65
+ const g = globalThis;
66
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
67
+ }
68
+ function emitTelemetry(tracking, xapi, event) {
69
+ if (!event.courseId) {
70
+ if (isDevEnvironment() && !warnedMissingCourseId) {
71
+ warnedMissingCourseId = true;
72
+ console.warn("[lessonkit] telemetry event missing courseId");
73
+ }
74
+ return;
75
+ }
76
+ tracking.track(event);
77
+ try {
78
+ const statement = (0, import_xapi.telemetryEventToXAPIStatement)(event);
79
+ if (statement) xapi?.send(statement);
80
+ } catch (err) {
81
+ if (isDevEnvironment()) {
82
+ console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
83
+ }
84
+ }
85
+ }
86
+ function buildTrackEvent(opts) {
87
+ const base = {
88
+ timestamp: (0, import_core.nowIso)(),
89
+ courseId: opts.courseId,
90
+ sessionId: opts.sessionId,
91
+ attemptId: opts.attemptId,
92
+ user: opts.user
93
+ };
94
+ switch (opts.name) {
95
+ case "course_started":
96
+ return { name: "course_started", ...base };
97
+ case "course_completed":
98
+ return { name: "course_completed", ...base };
99
+ case "lesson_started": {
100
+ const data = opts.data;
101
+ const lessonId = opts.lessonId ?? data?.lessonId;
102
+ if (!lessonId) throw new Error("lesson_started requires lessonId");
103
+ return {
104
+ name: "lesson_started",
105
+ ...base,
106
+ lessonId,
107
+ data: { lessonId, ...data }
108
+ };
109
+ }
110
+ case "lesson_completed":
111
+ case "lesson_time_on_task": {
112
+ const data = opts.data;
113
+ const lessonId = opts.lessonId ?? data?.lessonId;
114
+ if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
115
+ return {
116
+ name: opts.name,
117
+ ...base,
118
+ lessonId,
119
+ data: { lessonId, ...data }
120
+ };
121
+ }
122
+ case "quiz_answered": {
123
+ const data = opts.data;
124
+ const lessonId = opts.lessonId;
125
+ if (!lessonId) throw new Error("quiz_answered requires active lessonId");
126
+ return { name: "quiz_answered", ...base, lessonId, data };
127
+ }
128
+ case "quiz_completed": {
129
+ const data = opts.data;
130
+ const lessonId = opts.lessonId;
131
+ if (!lessonId) throw new Error("quiz_completed requires active lessonId");
132
+ return { name: "quiz_completed", ...base, lessonId, data };
133
+ }
134
+ case "interaction":
135
+ return {
136
+ name: "interaction",
137
+ ...base,
138
+ lessonId: opts.lessonId,
139
+ data: opts.data
140
+ };
141
+ default:
142
+ return { name: opts.name, ...base };
143
+ }
144
+ }
47
145
 
48
146
  // src/runtime/ports.ts
49
147
  function createNoopStorage() {
@@ -72,15 +170,62 @@ function createSessionStoragePort() {
72
170
  };
73
171
  }
74
172
 
173
+ // src/runtime/progress.ts
174
+ function createProgressController() {
175
+ let activeLessonId;
176
+ let completedLessonIds = /* @__PURE__ */ new Set();
177
+ let courseCompleted = false;
178
+ const lessonStartTimes = /* @__PURE__ */ new Map();
179
+ return {
180
+ getState: () => ({
181
+ activeLessonId,
182
+ completedLessonIds: new Set(completedLessonIds),
183
+ courseCompleted
184
+ }),
185
+ setActiveLesson: (lessonId, startedAtMs) => {
186
+ const previousLessonId = activeLessonId;
187
+ activeLessonId = lessonId;
188
+ lessonStartTimes.set(lessonId, startedAtMs);
189
+ return { previousLessonId };
190
+ },
191
+ completeLesson: (lessonId, completedAtMs) => {
192
+ if (completedLessonIds.has(lessonId)) return { didComplete: false };
193
+ completedLessonIds = new Set(completedLessonIds).add(lessonId);
194
+ const startedAt = lessonStartTimes.get(lessonId);
195
+ lessonStartTimes.delete(lessonId);
196
+ const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
197
+ return { durationMs, didComplete: true };
198
+ },
199
+ completeCourse: () => {
200
+ if (courseCompleted) return { didComplete: false };
201
+ courseCompleted = true;
202
+ return { didComplete: true };
203
+ }
204
+ };
205
+ }
206
+
207
+ // src/runtime/xapi.ts
208
+ var import_xapi2 = require("@lessonkit/xapi");
209
+ function createXapiClientFromConfig(config, queue) {
210
+ if (config.xapi?.enabled === false) return null;
211
+ if (config.xapi?.client) return config.xapi.client;
212
+ if (!config.courseId) return null;
213
+ return (0, import_xapi2.createXAPIClient)({
214
+ courseId: config.courseId,
215
+ transport: config.xapi?.transport,
216
+ queue
217
+ });
218
+ }
219
+
75
220
  // src/runtime/session.ts
76
- var import_core = require("@lessonkit/core");
221
+ var import_core2 = require("@lessonkit/core");
77
222
  var SESSION_STORAGE_KEY = "lessonkit:sessionId";
78
223
  var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
79
224
  function resolveSessionId(storage, provided) {
80
225
  if (provided) return provided;
81
226
  const existing = storage.getItem(SESSION_STORAGE_KEY);
82
227
  if (existing) return existing;
83
- const id = (0, import_core.createSessionId)();
228
+ const id = (0, import_core2.createSessionId)();
84
229
  storage.setItem(SESSION_STORAGE_KEY, id);
85
230
  return id;
86
231
  }
@@ -107,22 +252,16 @@ function disposeTrackingClient(client) {
107
252
  var defaultStorage = createSessionStoragePort();
108
253
  function createTrackingClientFromConfig(config) {
109
254
  if (config.tracking?.enabled === false) {
110
- return (0, import_core2.createTrackingClient)();
255
+ return (0, import_core3.createTrackingClient)();
111
256
  }
112
- return (0, import_core2.createTrackingClient)({
257
+ return (0, import_core3.createTrackingClient)({
113
258
  sink: config.tracking?.sink,
114
259
  batchSink: config.tracking?.batchSink,
115
260
  batch: config.tracking?.batch
116
261
  });
117
262
  }
118
- function createXapiClientFromConfig(config, queue) {
119
- if (config.xapi?.enabled === false) return null;
120
- if (config.xapi?.client) return config.xapi.client;
121
- const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
122
- return (0, import_xapi.createXAPIClient)({ baseId, transport: config.xapi?.transport, queue });
123
- }
124
263
  function LessonkitProvider(props) {
125
- const config = props.config ?? {};
264
+ const config = props.config;
126
265
  const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
127
266
  if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
128
267
  const attemptIdRef = (0, import_react.useRef)(config.session?.attemptId);
@@ -131,9 +270,15 @@ function LessonkitProvider(props) {
131
270
  userRef.current = config.session?.user;
132
271
  const courseIdRef = (0, import_react.useRef)(config.courseId);
133
272
  courseIdRef.current = config.courseId;
134
- const trackingRef = (0, import_react.useRef)((0, import_core2.createTrackingClient)());
273
+ const progressRef = (0, import_react.useRef)(createProgressController());
274
+ const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
275
+ const syncProgress = (0, import_react.useCallback)(() => {
276
+ setProgress(progressRef.current.getState());
277
+ }, []);
278
+ const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
279
+ activeLessonIdRef.current = progress.activeLessonId;
280
+ const trackingRef = (0, import_react.useRef)((0, import_core3.createTrackingClient)());
135
281
  const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
136
- const courseStartedInProviderRef = (0, import_react.useRef)(false);
137
282
  const trackingEnabled = config.tracking?.enabled;
138
283
  const trackingSink = config.tracking?.sink;
139
284
  const trackingBatchSink = config.tracking?.batchSink;
@@ -147,21 +292,19 @@ function LessonkitProvider(props) {
147
292
  setTracking(next);
148
293
  const sessionId = sessionIdRef.current;
149
294
  const cid = courseIdRef.current;
150
- const shouldEmitCourseStarted = cid ? !hasCourseStarted(defaultStorage, sessionId, cid) : !courseStartedInProviderRef.current;
151
- if (shouldEmitCourseStarted) {
152
- if (cid) {
153
- markCourseStarted(defaultStorage, sessionId, cid);
154
- } else {
155
- courseStartedInProviderRef.current = true;
156
- }
157
- next.track({
158
- name: "course_started",
159
- timestamp: (0, import_core2.nowIso)(),
160
- courseId: cid,
161
- sessionId,
162
- attemptId: attemptIdRef.current,
163
- user: userRef.current
164
- });
295
+ if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
296
+ markCourseStarted(defaultStorage, sessionId, cid);
297
+ emitTelemetry(
298
+ next,
299
+ xapiRef.current,
300
+ buildTrackEvent({
301
+ name: "course_started",
302
+ courseId: cid,
303
+ sessionId,
304
+ attemptId: attemptIdRef.current,
305
+ user: userRef.current
306
+ })
307
+ );
165
308
  }
166
309
  return () => {
167
310
  disposeTrackingClient(prev);
@@ -174,7 +317,7 @@ function LessonkitProvider(props) {
174
317
  batchFlushIntervalMs,
175
318
  batchMaxBatchSize
176
319
  ]);
177
- const xapiQueueRef = (0, import_react.useRef)((0, import_xapi.createInMemoryXAPIQueue)());
320
+ const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
178
321
  const xapiRef = (0, import_react.useRef)(null);
179
322
  const [xapi, setXapi] = (0, import_react.useState)(null);
180
323
  const xapiEnabled = config.xapi?.enabled;
@@ -202,21 +345,10 @@ function LessonkitProvider(props) {
202
345
  void prev?.flush();
203
346
  };
204
347
  }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
205
- const [completedLessonIds, setCompletedLessonIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
206
- const completedLessonIdsRef = (0, import_react.useRef)(completedLessonIds);
207
- completedLessonIdsRef.current = completedLessonIds;
208
- const [activeLessonId, setActiveLessonId] = (0, import_react.useState)(void 0);
209
- const [courseCompleted, setCourseCompleted] = (0, import_react.useState)(false);
210
- const courseCompletedRef = (0, import_react.useRef)(false);
211
- courseCompletedRef.current = courseCompleted;
212
- const activeLessonIdRef = (0, import_react.useRef)(void 0);
213
- activeLessonIdRef.current = activeLessonId;
214
- const lessonStartTimesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
215
348
  const track = (0, import_react.useCallback)(
216
349
  (name, data, opts) => {
217
- trackingRef.current?.track({
350
+ const event = buildTrackEvent({
218
351
  name,
219
- timestamp: (0, import_core2.nowIso)(),
220
352
  courseId: courseIdRef.current,
221
353
  lessonId: opts?.lessonId ?? activeLessonIdRef.current,
222
354
  sessionId: sessionIdRef.current,
@@ -224,6 +356,7 @@ function LessonkitProvider(props) {
224
356
  user: userRef.current,
225
357
  data
226
358
  });
359
+ emitTelemetry(trackingRef.current, xapiRef.current, event);
227
360
  },
228
361
  []
229
362
  );
@@ -233,45 +366,47 @@ function LessonkitProvider(props) {
233
366
  void xapiRef.current?.flush();
234
367
  };
235
368
  }, []);
236
- const setActiveLesson = (0, import_react.useCallback)((lessonId) => {
237
- if (activeLessonIdRef.current === lessonId) return;
238
- activeLessonIdRef.current = lessonId;
239
- setActiveLessonId(lessonId);
240
- lessonStartTimesRef.current.set(lessonId, Date.now());
241
- track("lesson_started", { lessonId }, { lessonId });
242
- xapiRef.current?.startedLesson({ lessonId });
243
- }, [track]);
244
- const completeLesson = (0, import_react.useCallback)(
245
- (lessonId) => {
246
- if (completedLessonIdsRef.current.has(lessonId)) return;
247
- completedLessonIdsRef.current = new Set(completedLessonIdsRef.current).add(lessonId);
248
- setCompletedLessonIds(completedLessonIdsRef.current);
249
- const startedAt = lessonStartTimesRef.current.get(lessonId);
250
- lessonStartTimesRef.current.delete(lessonId);
251
- const durationMs = typeof startedAt === "number" ? Math.max(0, Date.now() - startedAt) : void 0;
369
+ const emitLessonCompleted = (0, import_react.useCallback)(
370
+ (lessonId, durationMs) => {
252
371
  track("lesson_completed", { lessonId, durationMs }, { lessonId });
253
372
  if (durationMs !== void 0) {
254
373
  track("lesson_time_on_task", { lessonId, durationMs }, { lessonId });
255
374
  }
256
- xapiRef.current?.completeLesson({ lessonId, durationMs });
257
375
  },
258
376
  [track]
259
377
  );
378
+ const completeLesson = (0, import_react.useCallback)(
379
+ (lessonId) => {
380
+ const result = progressRef.current.completeLesson(lessonId, Date.now());
381
+ if (!result.didComplete) return;
382
+ syncProgress();
383
+ emitLessonCompleted(lessonId, result.durationMs);
384
+ },
385
+ [syncProgress, emitLessonCompleted]
386
+ );
387
+ const setActiveLesson = (0, import_react.useCallback)(
388
+ (lessonId) => {
389
+ const current = progressRef.current.getState();
390
+ if (current.activeLessonId === lessonId) return;
391
+ const previous = current.activeLessonId;
392
+ if (previous && previous !== lessonId) {
393
+ const completed = progressRef.current.completeLesson(previous, Date.now());
394
+ if (completed.didComplete) {
395
+ emitLessonCompleted(previous, completed.durationMs);
396
+ }
397
+ }
398
+ progressRef.current.setActiveLesson(lessonId, Date.now());
399
+ syncProgress();
400
+ track("lesson_started", { lessonId }, { lessonId });
401
+ },
402
+ [track, syncProgress, emitLessonCompleted]
403
+ );
260
404
  const completeCourse = (0, import_react.useCallback)(() => {
261
- if (courseCompletedRef.current) return;
262
- courseCompletedRef.current = true;
263
- setCourseCompleted(true);
405
+ const result = progressRef.current.completeCourse();
406
+ if (!result.didComplete) return;
407
+ syncProgress();
264
408
  track("course_completed");
265
- xapiRef.current?.completeCourse();
266
- }, [track]);
267
- const progress = (0, import_react.useMemo)(
268
- () => ({
269
- activeLessonId,
270
- completedLessonIds: new Set(completedLessonIds),
271
- courseCompleted
272
- }),
273
- [activeLessonId, completedLessonIds, courseCompleted]
274
- );
409
+ }, [track, syncProgress]);
275
410
  const runtime = (0, import_react.useMemo)(
276
411
  () => ({
277
412
  config,
@@ -323,9 +458,28 @@ function useQuizState() {
323
458
  );
324
459
  }
325
460
 
461
+ // src/runtime/validateComponentId.ts
462
+ var import_core4 = require("@lessonkit/core");
463
+ var warnedPaths = /* @__PURE__ */ new Set();
464
+ function isDevEnvironment2() {
465
+ const g = globalThis;
466
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
467
+ }
468
+ function warnInvalidComponentId(id, path) {
469
+ if (!isDevEnvironment2()) return;
470
+ const key = `${path}:${String(id)}`;
471
+ if (warnedPaths.has(key)) return;
472
+ const result = (0, import_core4.validateId)(id, path);
473
+ if (result.ok) return;
474
+ warnedPaths.add(key);
475
+ const detail = result.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
476
+ console.warn(`[lessonkit] invalid ${path} \u2014 ${detail}`);
477
+ }
478
+
326
479
  // src/components.tsx
327
480
  var import_jsx_runtime2 = require("react/jsx-runtime");
328
481
  function Course(props) {
482
+ warnInvalidComponentId(props.courseId, "courseId");
329
483
  const providerConfig = (0, import_react3.useMemo)(
330
484
  () => ({ ...props.config, courseId: props.courseId }),
331
485
  [props.config, props.courseId]
@@ -336,15 +490,23 @@ function Course(props) {
336
490
  ] }) });
337
491
  }
338
492
  function Lesson(props) {
493
+ warnInvalidComponentId(props.lessonId, "lessonId");
339
494
  const { setActiveLesson } = useLessonkit();
340
495
  const { completeLesson } = useCompletion();
341
- const reactId = (0, import_react3.useId)();
342
- const generatedId = (0, import_react3.useMemo)(() => `lesson-${sanitizeLessonId(reactId)}`, [reactId]);
343
- const id = props.lessonId ?? generatedId;
496
+ const id = props.lessonId;
497
+ const pendingCompleteRef = (0, import_react3.useRef)(null);
344
498
  (0, import_react3.useEffect)(() => {
499
+ if (pendingCompleteRef.current !== null) {
500
+ clearTimeout(pendingCompleteRef.current);
501
+ pendingCompleteRef.current = null;
502
+ }
345
503
  setActiveLesson(id);
346
504
  return () => {
347
- completeLesson(id);
505
+ const lessonId = id;
506
+ pendingCompleteRef.current = setTimeout(() => {
507
+ pendingCompleteRef.current = null;
508
+ completeLesson(lessonId);
509
+ }, 0);
348
510
  };
349
511
  }, [id, setActiveLesson, completeLesson]);
350
512
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { "aria-label": props.title, children: [
@@ -353,11 +515,13 @@ function Lesson(props) {
353
515
  ] });
354
516
  }
355
517
  function Scenario(props) {
356
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", children: props.children });
518
+ if (props.blockId !== void 0) warnInvalidComponentId(props.blockId, "blockId");
519
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "aria-label": "Scenario", "data-lk-block-id": props.blockId, children: props.children });
357
520
  }
358
521
  function Reflection(props) {
522
+ if (props.blockId !== void 0) warnInvalidComponentId(props.blockId, "blockId");
359
523
  const promptId = (0, import_react3.useId)();
360
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", children: [
524
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", "data-lk-block-id": props.blockId, children: [
361
525
  props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: promptId, children: props.prompt }) : null,
362
526
  props.children,
363
527
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -370,14 +534,23 @@ function Reflection(props) {
370
534
  ] });
371
535
  }
372
536
  function KnowledgeCheck(props) {
373
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Quiz, { question: props.question, choices: props.choices, answer: props.answer });
537
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
538
+ Quiz,
539
+ {
540
+ checkId: props.checkId,
541
+ question: props.question,
542
+ choices: props.choices,
543
+ answer: props.answer
544
+ }
545
+ );
374
546
  }
375
547
  function Quiz(props) {
548
+ warnInvalidComponentId(props.checkId, "checkId");
376
549
  const quiz = useQuizState();
377
550
  const [selected, setSelected] = (0, import_react3.useState)(null);
378
551
  const completedRef = (0, import_react3.useRef)(false);
379
552
  const questionId = (0, import_react3.useId)();
380
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", children: [
553
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
381
554
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
382
555
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
383
556
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
@@ -392,10 +565,15 @@ function Quiz(props) {
392
565
  onChange: () => {
393
566
  setSelected(c);
394
567
  const correct = c === props.answer;
395
- quiz.answer({ question: props.question, choice: c, correct });
568
+ quiz.answer({
569
+ checkId: props.checkId,
570
+ question: props.question,
571
+ choice: c,
572
+ correct
573
+ });
396
574
  if (correct && !completedRef.current) {
397
575
  completedRef.current = true;
398
- quiz.complete({ score: 1, maxScore: 1 });
576
+ quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1 });
399
577
  }
400
578
  }
401
579
  }
@@ -414,9 +592,113 @@ function ProgressTracker() {
414
592
  completed
415
593
  ] }) });
416
594
  }
417
- function sanitizeLessonId(id) {
418
- const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
419
- return s.length ? s : "id";
595
+
596
+ // src/theme/ThemeProvider.tsx
597
+ var import_react4 = __toESM(require("react"), 1);
598
+ var import_themes = require("@lessonkit/themes");
599
+
600
+ // src/theme/applyCssVariables.ts
601
+ function applyCssVariables(target, vars, previousKeys) {
602
+ for (const key of previousKeys) {
603
+ if (!(key in vars)) {
604
+ target.style.removeProperty(key);
605
+ }
606
+ }
607
+ const nextKeys = /* @__PURE__ */ new Set();
608
+ for (const [key, value] of Object.entries(vars)) {
609
+ target.style.setProperty(key, value);
610
+ nextKeys.add(key);
611
+ }
612
+ return nextKeys;
613
+ }
614
+
615
+ // src/theme/ThemeProvider.tsx
616
+ var import_jsx_runtime3 = require("react/jsx-runtime");
617
+ var ThemeContext = (0, import_react4.createContext)(null);
618
+ var useIsoLayoutEffect2 = typeof window !== "undefined" ? import_react4.useLayoutEffect : import_react4.default.useEffect;
619
+ function getSystemMode() {
620
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
621
+ return "light";
622
+ }
623
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
624
+ }
625
+ function resolveModeBase(mode, resolvedMode) {
626
+ if (mode === "system") {
627
+ return resolvedMode === "dark" ? import_themes.darkTheme : import_themes.lightTheme;
628
+ }
629
+ if (mode === "dark") return import_themes.darkTheme;
630
+ return import_themes.lightTheme;
631
+ }
632
+ function ThemeProvider(props) {
633
+ const preset = props.preset ?? "default";
634
+ const mode = props.mode ?? "light";
635
+ const targetKind = props.target ?? "document";
636
+ const [resolvedMode, setResolvedMode] = (0, import_react4.useState)(
637
+ () => mode === "system" ? getSystemMode() : mode
638
+ );
639
+ useIsoLayoutEffect2(() => {
640
+ if (mode !== "system") {
641
+ setResolvedMode(mode);
642
+ return;
643
+ }
644
+ setResolvedMode(getSystemMode());
645
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
646
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
647
+ const onChange = () => setResolvedMode(mq.matches ? "dark" : "light");
648
+ mq.addEventListener("change", onChange);
649
+ return () => mq.removeEventListener("change", onChange);
650
+ }, [mode]);
651
+ const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
652
+ const effectiveTheme = (0, import_react4.useMemo)(() => {
653
+ const modeBase = resolveModeBase(mode, dataTheme);
654
+ 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));
655
+ return (0, import_themes.mergeThemes)(base, props.theme ?? {});
656
+ }, [preset, mode, dataTheme, props.theme]);
657
+ const hostRef = (0, import_react4.useRef)(null);
658
+ const appliedKeysRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
659
+ useIsoLayoutEffect2(() => {
660
+ if (targetKind === "document" && typeof document !== "undefined") {
661
+ document.documentElement.setAttribute("data-lk-theme", dataTheme);
662
+ return () => document.documentElement.removeAttribute("data-lk-theme");
663
+ }
664
+ }, [targetKind, dataTheme]);
665
+ const inject = (0, import_react4.useCallback)(() => {
666
+ const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
667
+ const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
668
+ if (!el) return;
669
+ appliedKeysRef.current = applyCssVariables(el, vars, appliedKeysRef.current);
670
+ }, [effectiveTheme, targetKind]);
671
+ useIsoLayoutEffect2(() => {
672
+ inject();
673
+ return () => {
674
+ const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
675
+ if (!el) return;
676
+ for (const key of appliedKeysRef.current) {
677
+ el.style.removeProperty(key);
678
+ }
679
+ appliedKeysRef.current = /* @__PURE__ */ new Set();
680
+ };
681
+ }, [inject, targetKind]);
682
+ const value = (0, import_react4.useMemo)(
683
+ () => ({
684
+ theme: effectiveTheme,
685
+ preset,
686
+ mode,
687
+ resolvedMode: dataTheme
688
+ }),
689
+ [effectiveTheme, preset, mode, dataTheme]
690
+ );
691
+ if (targetKind === "document") {
692
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThemeContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
693
+ }
694
+ 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 }) });
695
+ }
696
+ function useTheme() {
697
+ const ctx = (0, import_react4.useContext)(ThemeContext);
698
+ if (!ctx) {
699
+ throw new Error("useTheme must be used within a ThemeProvider");
700
+ }
701
+ return ctx;
420
702
  }
421
703
  // Annotate the CommonJS export names for ESM import in node:
422
704
  0 && (module.exports = {
@@ -428,9 +710,11 @@ function sanitizeLessonId(id) {
428
710
  Quiz,
429
711
  Reflection,
430
712
  Scenario,
713
+ ThemeProvider,
431
714
  useCompletion,
432
715
  useLessonkit,
433
716
  useProgress,
434
717
  useQuizState,
718
+ useTheme,
435
719
  useTracking
436
720
  });