@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/README.md +24 -10
- package/dist/index.cjs +369 -85
- package/dist/index.d.cts +46 -16
- package/dist/index.d.ts +46 -16
- package/dist/index.js +367 -80
- package/package.json +7 -6
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
|
|
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
|
|
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,
|
|
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,
|
|
255
|
+
return (0, import_core3.createTrackingClient)();
|
|
111
256
|
}
|
|
112
|
-
return (0,
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
405
|
+
const result = progressRef.current.completeCourse();
|
|
406
|
+
if (!result.didComplete) return;
|
|
407
|
+
syncProgress();
|
|
264
408
|
track("course_completed");
|
|
265
|
-
|
|
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
|
|
342
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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)(
|
|
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({
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
});
|