@lessonkit/react 0.3.0 → 0.4.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 +11 -2
- package/dist/index.cjs +190 -46
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +187 -40
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install @lessonkit/react react react-dom
|
|
|
17
17
|
```tsx
|
|
18
18
|
import { useMemo } from "react";
|
|
19
19
|
import type { TelemetryEvent } from "@lessonkit/core";
|
|
20
|
-
import { Course, Lesson, Quiz, Scenario, ProgressTracker } from "@lessonkit/react";
|
|
20
|
+
import { Course, Lesson, Quiz, Scenario, ProgressTracker, ThemeProvider } from "@lessonkit/react";
|
|
21
21
|
import type { XAPIStatement } from "@lessonkit/xapi";
|
|
22
22
|
|
|
23
23
|
export default function App() {
|
|
@@ -34,6 +34,7 @@ export default function App() {
|
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
+
<ThemeProvider mode="light">
|
|
37
38
|
<Course title="Cybersecurity Basics" courseId="cyber-basics" config={config}>
|
|
38
39
|
<ProgressTracker />
|
|
39
40
|
|
|
@@ -49,11 +50,12 @@ export default function App() {
|
|
|
49
50
|
/>
|
|
50
51
|
</Lesson>
|
|
51
52
|
</Course>
|
|
53
|
+
</ThemeProvider>
|
|
52
54
|
);
|
|
53
55
|
}
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
## API (0.
|
|
58
|
+
## API (0.4.0)
|
|
57
59
|
|
|
58
60
|
### Components
|
|
59
61
|
|
|
@@ -71,6 +73,12 @@ export default function App() {
|
|
|
71
73
|
- `useTracking`
|
|
72
74
|
- `useQuizState`
|
|
73
75
|
- `useCompletion`
|
|
76
|
+
- `useTheme`
|
|
77
|
+
|
|
78
|
+
### Theming
|
|
79
|
+
|
|
80
|
+
- `ThemeProvider` — injects `--lk-*` CSS variables (see [`docs/THEMING.md`](../../docs/THEMING.md))
|
|
81
|
+
- Props: `preset`, `mode` (`light` | `dark` | `system`), `theme` (partial override), `target` (`document` | `element`)
|
|
74
82
|
|
|
75
83
|
## Notes
|
|
76
84
|
|
|
@@ -84,4 +92,5 @@ export default function App() {
|
|
|
84
92
|
- If you omit `session.sessionId`, the provider reuses a tab-scoped id via `sessionStorage` so React
|
|
85
93
|
Strict Mode remounts do not split analytics sessions in development.
|
|
86
94
|
- Accessibility guidance lives in [`docs/ACCESSIBILITY.md`](../../docs/ACCESSIBILITY.md).
|
|
95
|
+
- Theming and token catalog: [`docs/THEMING.md`](../../docs/THEMING.md).
|
|
87
96
|
|
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,69 +54,91 @@ var import_accessibility = require("@lessonkit/accessibility");
|
|
|
42
54
|
|
|
43
55
|
// src/context.tsx
|
|
44
56
|
var import_react = require("react");
|
|
45
|
-
var
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
var import_core2 = require("@lessonkit/core");
|
|
58
|
+
var import_xapi2 = require("@lessonkit/xapi");
|
|
59
|
+
|
|
60
|
+
// src/runtime/ports.ts
|
|
61
|
+
function createNoopStorage() {
|
|
62
|
+
return {
|
|
63
|
+
getItem: () => null,
|
|
64
|
+
setItem: () => {
|
|
65
|
+
}
|
|
66
|
+
};
|
|
54
67
|
}
|
|
55
|
-
function
|
|
56
|
-
if (typeof sessionStorage === "undefined") return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
function createSessionStoragePort() {
|
|
69
|
+
if (typeof sessionStorage === "undefined") return createNoopStorage();
|
|
70
|
+
return {
|
|
71
|
+
getItem: (key) => {
|
|
72
|
+
try {
|
|
73
|
+
return sessionStorage.getItem(key);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
setItem: (key, value) => {
|
|
79
|
+
try {
|
|
80
|
+
sessionStorage.setItem(key, value);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
62
85
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
|
|
87
|
+
// src/runtime/xapi.ts
|
|
88
|
+
var import_xapi = require("@lessonkit/xapi");
|
|
89
|
+
function createXapiClientFromConfig(config, queue) {
|
|
90
|
+
if (config.xapi?.enabled === false) return null;
|
|
91
|
+
if (config.xapi?.client) return config.xapi.client;
|
|
92
|
+
const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
|
|
93
|
+
return (0, import_xapi.createXAPIClient)({ baseId, transport: config.xapi?.transport, queue });
|
|
69
94
|
}
|
|
70
|
-
|
|
95
|
+
|
|
96
|
+
// src/runtime/session.ts
|
|
97
|
+
var import_core = require("@lessonkit/core");
|
|
98
|
+
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
99
|
+
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
100
|
+
function resolveSessionId(storage, provided) {
|
|
71
101
|
if (provided) return provided;
|
|
72
|
-
const existing =
|
|
102
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
73
103
|
if (existing) return existing;
|
|
74
104
|
const id = (0, import_core.createSessionId)();
|
|
75
|
-
|
|
105
|
+
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
76
106
|
return id;
|
|
77
107
|
}
|
|
78
108
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
79
109
|
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
80
110
|
}
|
|
81
|
-
function hasCourseStarted(sessionId, courseId) {
|
|
111
|
+
function hasCourseStarted(storage, sessionId, courseId) {
|
|
82
112
|
if (!courseId) return false;
|
|
83
|
-
return
|
|
113
|
+
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
84
114
|
}
|
|
85
|
-
function markCourseStarted(sessionId, courseId) {
|
|
115
|
+
function markCourseStarted(storage, sessionId, courseId) {
|
|
86
116
|
if (!courseId) return;
|
|
87
|
-
|
|
117
|
+
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
88
118
|
}
|
|
119
|
+
|
|
120
|
+
// src/context.tsx
|
|
121
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
122
|
+
var LessonkitContext = (0, import_react.createContext)(null);
|
|
123
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
|
|
124
|
+
function disposeTrackingClient(client) {
|
|
125
|
+
client?.flush?.();
|
|
126
|
+
client?.dispose?.();
|
|
127
|
+
}
|
|
128
|
+
var defaultStorage = createSessionStoragePort();
|
|
89
129
|
function createTrackingClientFromConfig(config) {
|
|
90
130
|
if (config.tracking?.enabled === false) {
|
|
91
|
-
return (0,
|
|
131
|
+
return (0, import_core2.createTrackingClient)();
|
|
92
132
|
}
|
|
93
|
-
return (0,
|
|
133
|
+
return (0, import_core2.createTrackingClient)({
|
|
94
134
|
sink: config.tracking?.sink,
|
|
95
135
|
batchSink: config.tracking?.batchSink,
|
|
96
136
|
batch: config.tracking?.batch
|
|
97
137
|
});
|
|
98
138
|
}
|
|
99
|
-
function createXapiClientFromConfig(config, queue) {
|
|
100
|
-
if (config.xapi?.enabled === false) return null;
|
|
101
|
-
if (config.xapi?.client) return config.xapi.client;
|
|
102
|
-
const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
|
|
103
|
-
return (0, import_xapi.createXAPIClient)({ baseId, transport: config.xapi?.transport, queue });
|
|
104
|
-
}
|
|
105
139
|
function LessonkitProvider(props) {
|
|
106
140
|
const config = props.config ?? {};
|
|
107
|
-
const sessionIdRef = (0, import_react.useRef)(resolveSessionId(config.session?.sessionId));
|
|
141
|
+
const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
108
142
|
if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
|
|
109
143
|
const attemptIdRef = (0, import_react.useRef)(config.session?.attemptId);
|
|
110
144
|
const userRef = (0, import_react.useRef)(config.session?.user);
|
|
@@ -112,7 +146,7 @@ function LessonkitProvider(props) {
|
|
|
112
146
|
userRef.current = config.session?.user;
|
|
113
147
|
const courseIdRef = (0, import_react.useRef)(config.courseId);
|
|
114
148
|
courseIdRef.current = config.courseId;
|
|
115
|
-
const trackingRef = (0, import_react.useRef)((0,
|
|
149
|
+
const trackingRef = (0, import_react.useRef)((0, import_core2.createTrackingClient)());
|
|
116
150
|
const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
|
|
117
151
|
const courseStartedInProviderRef = (0, import_react.useRef)(false);
|
|
118
152
|
const trackingEnabled = config.tracking?.enabled;
|
|
@@ -121,23 +155,23 @@ function LessonkitProvider(props) {
|
|
|
121
155
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
122
156
|
const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
|
|
123
157
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
124
|
-
(
|
|
158
|
+
useIsoLayoutEffect(() => {
|
|
125
159
|
const prev = trackingRef.current;
|
|
126
160
|
const next = createTrackingClientFromConfig(config);
|
|
127
161
|
trackingRef.current = next;
|
|
128
162
|
setTracking(next);
|
|
129
163
|
const sessionId = sessionIdRef.current;
|
|
130
164
|
const cid = courseIdRef.current;
|
|
131
|
-
const shouldEmitCourseStarted = cid ? !hasCourseStarted(sessionId, cid) : !courseStartedInProviderRef.current;
|
|
165
|
+
const shouldEmitCourseStarted = cid ? !hasCourseStarted(defaultStorage, sessionId, cid) : !courseStartedInProviderRef.current;
|
|
132
166
|
if (shouldEmitCourseStarted) {
|
|
133
167
|
if (cid) {
|
|
134
|
-
markCourseStarted(sessionId, cid);
|
|
168
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
135
169
|
} else {
|
|
136
170
|
courseStartedInProviderRef.current = true;
|
|
137
171
|
}
|
|
138
172
|
next.track({
|
|
139
173
|
name: "course_started",
|
|
140
|
-
timestamp: (0,
|
|
174
|
+
timestamp: (0, import_core2.nowIso)(),
|
|
141
175
|
courseId: cid,
|
|
142
176
|
sessionId,
|
|
143
177
|
attemptId: attemptIdRef.current,
|
|
@@ -155,14 +189,14 @@ function LessonkitProvider(props) {
|
|
|
155
189
|
batchFlushIntervalMs,
|
|
156
190
|
batchMaxBatchSize
|
|
157
191
|
]);
|
|
158
|
-
const xapiQueueRef = (0, import_react.useRef)((0,
|
|
192
|
+
const xapiQueueRef = (0, import_react.useRef)((0, import_xapi2.createInMemoryXAPIQueue)());
|
|
159
193
|
const xapiRef = (0, import_react.useRef)(null);
|
|
160
194
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
161
195
|
const xapiEnabled = config.xapi?.enabled;
|
|
162
196
|
const xapiClient = config.xapi?.client;
|
|
163
197
|
const xapiTransport = config.xapi?.transport;
|
|
164
198
|
const courseId = config.courseId;
|
|
165
|
-
(
|
|
199
|
+
useIsoLayoutEffect(() => {
|
|
166
200
|
const prev = xapiRef.current;
|
|
167
201
|
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
168
202
|
xapiRef.current = next;
|
|
@@ -197,7 +231,7 @@ function LessonkitProvider(props) {
|
|
|
197
231
|
(name, data, opts) => {
|
|
198
232
|
trackingRef.current?.track({
|
|
199
233
|
name,
|
|
200
|
-
timestamp: (0,
|
|
234
|
+
timestamp: (0, import_core2.nowIso)(),
|
|
201
235
|
courseId: courseIdRef.current,
|
|
202
236
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
203
237
|
sessionId: sessionIdRef.current,
|
|
@@ -399,6 +433,114 @@ function sanitizeLessonId(id) {
|
|
|
399
433
|
const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
400
434
|
return s.length ? s : "id";
|
|
401
435
|
}
|
|
436
|
+
|
|
437
|
+
// src/theme/ThemeProvider.tsx
|
|
438
|
+
var import_react4 = __toESM(require("react"), 1);
|
|
439
|
+
var import_themes = require("@lessonkit/themes");
|
|
440
|
+
|
|
441
|
+
// src/theme/applyCssVariables.ts
|
|
442
|
+
function applyCssVariables(target, vars, previousKeys) {
|
|
443
|
+
for (const key of previousKeys) {
|
|
444
|
+
if (!(key in vars)) {
|
|
445
|
+
target.style.removeProperty(key);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const nextKeys = /* @__PURE__ */ new Set();
|
|
449
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
450
|
+
target.style.setProperty(key, value);
|
|
451
|
+
nextKeys.add(key);
|
|
452
|
+
}
|
|
453
|
+
return nextKeys;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/theme/ThemeProvider.tsx
|
|
457
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
458
|
+
var ThemeContext = (0, import_react4.createContext)(null);
|
|
459
|
+
var useIsoLayoutEffect2 = typeof window !== "undefined" ? import_react4.useLayoutEffect : import_react4.default.useEffect;
|
|
460
|
+
function getSystemMode() {
|
|
461
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
462
|
+
return "light";
|
|
463
|
+
}
|
|
464
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
465
|
+
}
|
|
466
|
+
function resolveModeBase(mode, resolvedMode) {
|
|
467
|
+
if (mode === "system") {
|
|
468
|
+
return resolvedMode === "dark" ? import_themes.darkTheme : import_themes.lightTheme;
|
|
469
|
+
}
|
|
470
|
+
if (mode === "dark") return import_themes.darkTheme;
|
|
471
|
+
return import_themes.lightTheme;
|
|
472
|
+
}
|
|
473
|
+
function ThemeProvider(props) {
|
|
474
|
+
const preset = props.preset ?? "default";
|
|
475
|
+
const mode = props.mode ?? "light";
|
|
476
|
+
const targetKind = props.target ?? "document";
|
|
477
|
+
const [resolvedMode, setResolvedMode] = (0, import_react4.useState)(
|
|
478
|
+
() => mode === "system" ? getSystemMode() : mode
|
|
479
|
+
);
|
|
480
|
+
useIsoLayoutEffect2(() => {
|
|
481
|
+
if (mode !== "system") {
|
|
482
|
+
setResolvedMode(mode);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
setResolvedMode(getSystemMode());
|
|
486
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
487
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
488
|
+
const onChange = () => setResolvedMode(mq.matches ? "dark" : "light");
|
|
489
|
+
mq.addEventListener("change", onChange);
|
|
490
|
+
return () => mq.removeEventListener("change", onChange);
|
|
491
|
+
}, [mode]);
|
|
492
|
+
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
493
|
+
const effectiveTheme = (0, import_react4.useMemo)(() => {
|
|
494
|
+
const modeBase = resolveModeBase(mode, dataTheme);
|
|
495
|
+
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));
|
|
496
|
+
return (0, import_themes.mergeThemes)(base, props.theme ?? {});
|
|
497
|
+
}, [preset, mode, dataTheme, props.theme]);
|
|
498
|
+
const hostRef = (0, import_react4.useRef)(null);
|
|
499
|
+
const appliedKeysRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
|
|
500
|
+
useIsoLayoutEffect2(() => {
|
|
501
|
+
if (targetKind === "document" && typeof document !== "undefined") {
|
|
502
|
+
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
503
|
+
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
504
|
+
}
|
|
505
|
+
}, [targetKind, dataTheme]);
|
|
506
|
+
const inject = (0, import_react4.useCallback)(() => {
|
|
507
|
+
const vars = (0, import_themes.themeToCssVariables)(effectiveTheme);
|
|
508
|
+
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
509
|
+
if (!el) return;
|
|
510
|
+
appliedKeysRef.current = applyCssVariables(el, vars, appliedKeysRef.current);
|
|
511
|
+
}, [effectiveTheme, targetKind]);
|
|
512
|
+
useIsoLayoutEffect2(() => {
|
|
513
|
+
inject();
|
|
514
|
+
return () => {
|
|
515
|
+
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
516
|
+
if (!el) return;
|
|
517
|
+
for (const key of appliedKeysRef.current) {
|
|
518
|
+
el.style.removeProperty(key);
|
|
519
|
+
}
|
|
520
|
+
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
521
|
+
};
|
|
522
|
+
}, [inject, targetKind]);
|
|
523
|
+
const value = (0, import_react4.useMemo)(
|
|
524
|
+
() => ({
|
|
525
|
+
theme: effectiveTheme,
|
|
526
|
+
preset,
|
|
527
|
+
mode,
|
|
528
|
+
resolvedMode: dataTheme
|
|
529
|
+
}),
|
|
530
|
+
[effectiveTheme, preset, mode, dataTheme]
|
|
531
|
+
);
|
|
532
|
+
if (targetKind === "document") {
|
|
533
|
+
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 }) });
|
|
534
|
+
}
|
|
535
|
+
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 }) });
|
|
536
|
+
}
|
|
537
|
+
function useTheme() {
|
|
538
|
+
const ctx = (0, import_react4.useContext)(ThemeContext);
|
|
539
|
+
if (!ctx) {
|
|
540
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
541
|
+
}
|
|
542
|
+
return ctx;
|
|
543
|
+
}
|
|
402
544
|
// Annotate the CommonJS export names for ESM import in node:
|
|
403
545
|
0 && (module.exports = {
|
|
404
546
|
Course,
|
|
@@ -409,9 +551,11 @@ function sanitizeLessonId(id) {
|
|
|
409
551
|
Quiz,
|
|
410
552
|
Reflection,
|
|
411
553
|
Scenario,
|
|
554
|
+
ThemeProvider,
|
|
412
555
|
useCompletion,
|
|
413
556
|
useLessonkit,
|
|
414
557
|
useProgress,
|
|
415
558
|
useQuizState,
|
|
559
|
+
useTheme,
|
|
416
560
|
useTracking
|
|
417
561
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -3,6 +3,8 @@ import React from 'react';
|
|
|
3
3
|
import * as _lessonkit_core from '@lessonkit/core';
|
|
4
4
|
import { CourseId, TelemetryUser, TelemetryEvent, TrackingClient, LessonId } from '@lessonkit/core';
|
|
5
5
|
import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
|
|
6
|
+
import { LessonkitThemeV1, ThemePresetName, PartialLessonkitThemeV1 } from '@lessonkit/themes';
|
|
7
|
+
export { ThemePresetName } from '@lessonkit/themes';
|
|
6
8
|
|
|
7
9
|
type LessonkitConfig = {
|
|
8
10
|
courseId?: CourseId;
|
|
@@ -107,4 +109,24 @@ declare function useQuizState(): {
|
|
|
107
109
|
}) => void;
|
|
108
110
|
};
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
type ThemeMode = "light" | "dark" | "system";
|
|
113
|
+
type ThemeResolvedMode = "light" | "dark";
|
|
114
|
+
type ThemeProviderProps = {
|
|
115
|
+
children: React.ReactNode;
|
|
116
|
+
/** Partial theme merged on top of the resolved preset (last writer wins). */
|
|
117
|
+
theme?: PartialLessonkitThemeV1;
|
|
118
|
+
preset?: ThemePresetName;
|
|
119
|
+
mode?: ThemeMode;
|
|
120
|
+
/** `document` injects on `:root`; `element` scopes to the provider wrapper. */
|
|
121
|
+
target?: "document" | "element";
|
|
122
|
+
};
|
|
123
|
+
type ThemeContextValue = {
|
|
124
|
+
theme: LessonkitThemeV1;
|
|
125
|
+
preset: ThemePresetName;
|
|
126
|
+
mode: ThemeMode;
|
|
127
|
+
resolvedMode: ThemeResolvedMode;
|
|
128
|
+
};
|
|
129
|
+
declare function ThemeProvider(props: ThemeProviderProps): react_jsx_runtime.JSX.Element;
|
|
130
|
+
declare function useTheme(): ThemeContextValue;
|
|
131
|
+
|
|
132
|
+
export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import React from 'react';
|
|
|
3
3
|
import * as _lessonkit_core from '@lessonkit/core';
|
|
4
4
|
import { CourseId, TelemetryUser, TelemetryEvent, TrackingClient, LessonId } from '@lessonkit/core';
|
|
5
5
|
import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
|
|
6
|
+
import { LessonkitThemeV1, ThemePresetName, PartialLessonkitThemeV1 } from '@lessonkit/themes';
|
|
7
|
+
export { ThemePresetName } from '@lessonkit/themes';
|
|
6
8
|
|
|
7
9
|
type LessonkitConfig = {
|
|
8
10
|
courseId?: CourseId;
|
|
@@ -107,4 +109,24 @@ declare function useQuizState(): {
|
|
|
107
109
|
}) => void;
|
|
108
110
|
};
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
type ThemeMode = "light" | "dark" | "system";
|
|
113
|
+
type ThemeResolvedMode = "light" | "dark";
|
|
114
|
+
type ThemeProviderProps = {
|
|
115
|
+
children: React.ReactNode;
|
|
116
|
+
/** Partial theme merged on top of the resolved preset (last writer wins). */
|
|
117
|
+
theme?: PartialLessonkitThemeV1;
|
|
118
|
+
preset?: ThemePresetName;
|
|
119
|
+
mode?: ThemeMode;
|
|
120
|
+
/** `document` injects on `:root`; `element` scopes to the provider wrapper. */
|
|
121
|
+
target?: "document" | "element";
|
|
122
|
+
};
|
|
123
|
+
type ThemeContextValue = {
|
|
124
|
+
theme: LessonkitThemeV1;
|
|
125
|
+
preset: ThemePresetName;
|
|
126
|
+
mode: ThemeMode;
|
|
127
|
+
resolvedMode: ThemeResolvedMode;
|
|
128
|
+
};
|
|
129
|
+
declare function ThemeProvider(props: ThemeProviderProps): react_jsx_runtime.JSX.Element;
|
|
130
|
+
declare function useTheme(): ThemeContextValue;
|
|
131
|
+
|
|
132
|
+
export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, type ThemeContextValue, type ThemeMode, ThemeProvider, type ThemeProviderProps, type ThemeResolvedMode, useCompletion, useLessonkit, useProgress, useQuizState, useTheme, useTracking };
|
package/dist/index.js
CHANGED
|
@@ -12,50 +12,78 @@ import {
|
|
|
12
12
|
useRef,
|
|
13
13
|
useState
|
|
14
14
|
} from "react";
|
|
15
|
-
import {
|
|
16
|
-
import { createInMemoryXAPIQueue
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
import { createTrackingClient, nowIso } from "@lessonkit/core";
|
|
16
|
+
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
17
|
+
|
|
18
|
+
// src/runtime/ports.ts
|
|
19
|
+
function createNoopStorage() {
|
|
20
|
+
return {
|
|
21
|
+
getItem: () => null,
|
|
22
|
+
setItem: () => {
|
|
23
|
+
}
|
|
24
|
+
};
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
-
if (typeof sessionStorage === "undefined") return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
function createSessionStoragePort() {
|
|
27
|
+
if (typeof sessionStorage === "undefined") return createNoopStorage();
|
|
28
|
+
return {
|
|
29
|
+
getItem: (key) => {
|
|
30
|
+
try {
|
|
31
|
+
return sessionStorage.getItem(key);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
setItem: (key, value) => {
|
|
37
|
+
try {
|
|
38
|
+
sessionStorage.setItem(key, value);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
32
43
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
|
|
45
|
+
// src/runtime/xapi.ts
|
|
46
|
+
import { createXAPIClient } from "@lessonkit/xapi";
|
|
47
|
+
function createXapiClientFromConfig(config, queue) {
|
|
48
|
+
if (config.xapi?.enabled === false) return null;
|
|
49
|
+
if (config.xapi?.client) return config.xapi.client;
|
|
50
|
+
const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
|
|
51
|
+
return createXAPIClient({ baseId, transport: config.xapi?.transport, queue });
|
|
39
52
|
}
|
|
40
|
-
|
|
53
|
+
|
|
54
|
+
// src/runtime/session.ts
|
|
55
|
+
import { createSessionId } from "@lessonkit/core";
|
|
56
|
+
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
57
|
+
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
58
|
+
function resolveSessionId(storage, provided) {
|
|
41
59
|
if (provided) return provided;
|
|
42
|
-
const existing =
|
|
60
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
43
61
|
if (existing) return existing;
|
|
44
62
|
const id = createSessionId();
|
|
45
|
-
|
|
63
|
+
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
46
64
|
return id;
|
|
47
65
|
}
|
|
48
66
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
49
67
|
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
50
68
|
}
|
|
51
|
-
function hasCourseStarted(sessionId, courseId) {
|
|
69
|
+
function hasCourseStarted(storage, sessionId, courseId) {
|
|
52
70
|
if (!courseId) return false;
|
|
53
|
-
return
|
|
71
|
+
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
54
72
|
}
|
|
55
|
-
function markCourseStarted(sessionId, courseId) {
|
|
73
|
+
function markCourseStarted(storage, sessionId, courseId) {
|
|
56
74
|
if (!courseId) return;
|
|
57
|
-
|
|
75
|
+
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
58
76
|
}
|
|
77
|
+
|
|
78
|
+
// src/context.tsx
|
|
79
|
+
import { jsx } from "react/jsx-runtime";
|
|
80
|
+
var LessonkitContext = createContext(null);
|
|
81
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
82
|
+
function disposeTrackingClient(client) {
|
|
83
|
+
client?.flush?.();
|
|
84
|
+
client?.dispose?.();
|
|
85
|
+
}
|
|
86
|
+
var defaultStorage = createSessionStoragePort();
|
|
59
87
|
function createTrackingClientFromConfig(config) {
|
|
60
88
|
if (config.tracking?.enabled === false) {
|
|
61
89
|
return createTrackingClient();
|
|
@@ -66,15 +94,9 @@ function createTrackingClientFromConfig(config) {
|
|
|
66
94
|
batch: config.tracking?.batch
|
|
67
95
|
});
|
|
68
96
|
}
|
|
69
|
-
function createXapiClientFromConfig(config, queue) {
|
|
70
|
-
if (config.xapi?.enabled === false) return null;
|
|
71
|
-
if (config.xapi?.client) return config.xapi.client;
|
|
72
|
-
const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
|
|
73
|
-
return createXAPIClient({ baseId, transport: config.xapi?.transport, queue });
|
|
74
|
-
}
|
|
75
97
|
function LessonkitProvider(props) {
|
|
76
98
|
const config = props.config ?? {};
|
|
77
|
-
const sessionIdRef = useRef(resolveSessionId(config.session?.sessionId));
|
|
99
|
+
const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
78
100
|
if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
|
|
79
101
|
const attemptIdRef = useRef(config.session?.attemptId);
|
|
80
102
|
const userRef = useRef(config.session?.user);
|
|
@@ -91,17 +113,17 @@ function LessonkitProvider(props) {
|
|
|
91
113
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
92
114
|
const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
|
|
93
115
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
94
|
-
|
|
116
|
+
useIsoLayoutEffect(() => {
|
|
95
117
|
const prev = trackingRef.current;
|
|
96
118
|
const next = createTrackingClientFromConfig(config);
|
|
97
119
|
trackingRef.current = next;
|
|
98
120
|
setTracking(next);
|
|
99
121
|
const sessionId = sessionIdRef.current;
|
|
100
122
|
const cid = courseIdRef.current;
|
|
101
|
-
const shouldEmitCourseStarted = cid ? !hasCourseStarted(sessionId, cid) : !courseStartedInProviderRef.current;
|
|
123
|
+
const shouldEmitCourseStarted = cid ? !hasCourseStarted(defaultStorage, sessionId, cid) : !courseStartedInProviderRef.current;
|
|
102
124
|
if (shouldEmitCourseStarted) {
|
|
103
125
|
if (cid) {
|
|
104
|
-
markCourseStarted(sessionId, cid);
|
|
126
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
105
127
|
} else {
|
|
106
128
|
courseStartedInProviderRef.current = true;
|
|
107
129
|
}
|
|
@@ -132,7 +154,7 @@ function LessonkitProvider(props) {
|
|
|
132
154
|
const xapiClient = config.xapi?.client;
|
|
133
155
|
const xapiTransport = config.xapi?.transport;
|
|
134
156
|
const courseId = config.courseId;
|
|
135
|
-
|
|
157
|
+
useIsoLayoutEffect(() => {
|
|
136
158
|
const prev = xapiRef.current;
|
|
137
159
|
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
138
160
|
xapiRef.current = next;
|
|
@@ -369,6 +391,129 @@ function sanitizeLessonId(id) {
|
|
|
369
391
|
const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
370
392
|
return s.length ? s : "id";
|
|
371
393
|
}
|
|
394
|
+
|
|
395
|
+
// src/theme/ThemeProvider.tsx
|
|
396
|
+
import React3, {
|
|
397
|
+
createContext as createContext2,
|
|
398
|
+
useCallback as useCallback2,
|
|
399
|
+
useContext as useContext2,
|
|
400
|
+
useLayoutEffect as useLayoutEffect2,
|
|
401
|
+
useMemo as useMemo4,
|
|
402
|
+
useRef as useRef3,
|
|
403
|
+
useState as useState3
|
|
404
|
+
} from "react";
|
|
405
|
+
import {
|
|
406
|
+
brandThemeOverrides,
|
|
407
|
+
darkTheme,
|
|
408
|
+
getPresetTheme,
|
|
409
|
+
lightTheme,
|
|
410
|
+
mergeThemes,
|
|
411
|
+
themeToCssVariables
|
|
412
|
+
} from "@lessonkit/themes";
|
|
413
|
+
|
|
414
|
+
// src/theme/applyCssVariables.ts
|
|
415
|
+
function applyCssVariables(target, vars, previousKeys) {
|
|
416
|
+
for (const key of previousKeys) {
|
|
417
|
+
if (!(key in vars)) {
|
|
418
|
+
target.style.removeProperty(key);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const nextKeys = /* @__PURE__ */ new Set();
|
|
422
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
423
|
+
target.style.setProperty(key, value);
|
|
424
|
+
nextKeys.add(key);
|
|
425
|
+
}
|
|
426
|
+
return nextKeys;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/theme/ThemeProvider.tsx
|
|
430
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
431
|
+
var ThemeContext = createContext2(null);
|
|
432
|
+
var useIsoLayoutEffect2 = typeof window !== "undefined" ? useLayoutEffect2 : React3.useEffect;
|
|
433
|
+
function getSystemMode() {
|
|
434
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
435
|
+
return "light";
|
|
436
|
+
}
|
|
437
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
438
|
+
}
|
|
439
|
+
function resolveModeBase(mode, resolvedMode) {
|
|
440
|
+
if (mode === "system") {
|
|
441
|
+
return resolvedMode === "dark" ? darkTheme : lightTheme;
|
|
442
|
+
}
|
|
443
|
+
if (mode === "dark") return darkTheme;
|
|
444
|
+
return lightTheme;
|
|
445
|
+
}
|
|
446
|
+
function ThemeProvider(props) {
|
|
447
|
+
const preset = props.preset ?? "default";
|
|
448
|
+
const mode = props.mode ?? "light";
|
|
449
|
+
const targetKind = props.target ?? "document";
|
|
450
|
+
const [resolvedMode, setResolvedMode] = useState3(
|
|
451
|
+
() => mode === "system" ? getSystemMode() : mode
|
|
452
|
+
);
|
|
453
|
+
useIsoLayoutEffect2(() => {
|
|
454
|
+
if (mode !== "system") {
|
|
455
|
+
setResolvedMode(mode);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
setResolvedMode(getSystemMode());
|
|
459
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
460
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
461
|
+
const onChange = () => setResolvedMode(mq.matches ? "dark" : "light");
|
|
462
|
+
mq.addEventListener("change", onChange);
|
|
463
|
+
return () => mq.removeEventListener("change", onChange);
|
|
464
|
+
}, [mode]);
|
|
465
|
+
const dataTheme = mode === "system" ? resolvedMode : mode === "dark" ? "dark" : "light";
|
|
466
|
+
const effectiveTheme = useMemo4(() => {
|
|
467
|
+
const modeBase = resolveModeBase(mode, dataTheme);
|
|
468
|
+
const base = preset === "default" ? modeBase : preset === "brand" ? mergeThemes(modeBase, brandThemeOverrides) : mergeThemes(modeBase, getPresetTheme(preset));
|
|
469
|
+
return mergeThemes(base, props.theme ?? {});
|
|
470
|
+
}, [preset, mode, dataTheme, props.theme]);
|
|
471
|
+
const hostRef = useRef3(null);
|
|
472
|
+
const appliedKeysRef = useRef3(/* @__PURE__ */ new Set());
|
|
473
|
+
useIsoLayoutEffect2(() => {
|
|
474
|
+
if (targetKind === "document" && typeof document !== "undefined") {
|
|
475
|
+
document.documentElement.setAttribute("data-lk-theme", dataTheme);
|
|
476
|
+
return () => document.documentElement.removeAttribute("data-lk-theme");
|
|
477
|
+
}
|
|
478
|
+
}, [targetKind, dataTheme]);
|
|
479
|
+
const inject = useCallback2(() => {
|
|
480
|
+
const vars = themeToCssVariables(effectiveTheme);
|
|
481
|
+
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
482
|
+
if (!el) return;
|
|
483
|
+
appliedKeysRef.current = applyCssVariables(el, vars, appliedKeysRef.current);
|
|
484
|
+
}, [effectiveTheme, targetKind]);
|
|
485
|
+
useIsoLayoutEffect2(() => {
|
|
486
|
+
inject();
|
|
487
|
+
return () => {
|
|
488
|
+
const el = targetKind === "document" && typeof document !== "undefined" ? document.documentElement : hostRef.current;
|
|
489
|
+
if (!el) return;
|
|
490
|
+
for (const key of appliedKeysRef.current) {
|
|
491
|
+
el.style.removeProperty(key);
|
|
492
|
+
}
|
|
493
|
+
appliedKeysRef.current = /* @__PURE__ */ new Set();
|
|
494
|
+
};
|
|
495
|
+
}, [inject, targetKind]);
|
|
496
|
+
const value = useMemo4(
|
|
497
|
+
() => ({
|
|
498
|
+
theme: effectiveTheme,
|
|
499
|
+
preset,
|
|
500
|
+
mode,
|
|
501
|
+
resolvedMode: dataTheme
|
|
502
|
+
}),
|
|
503
|
+
[effectiveTheme, preset, mode, dataTheme]
|
|
504
|
+
);
|
|
505
|
+
if (targetKind === "document") {
|
|
506
|
+
return /* @__PURE__ */ jsx3(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx3("div", { "data-lk-theme": dataTheme, style: { display: "contents" }, children: props.children }) });
|
|
507
|
+
}
|
|
508
|
+
return /* @__PURE__ */ jsx3(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx3("div", { ref: hostRef, "data-lk-theme": dataTheme, children: props.children }) });
|
|
509
|
+
}
|
|
510
|
+
function useTheme() {
|
|
511
|
+
const ctx = useContext2(ThemeContext);
|
|
512
|
+
if (!ctx) {
|
|
513
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
514
|
+
}
|
|
515
|
+
return ctx;
|
|
516
|
+
}
|
|
372
517
|
export {
|
|
373
518
|
Course,
|
|
374
519
|
KnowledgeCheck,
|
|
@@ -378,9 +523,11 @@ export {
|
|
|
378
523
|
Quiz,
|
|
379
524
|
Reflection,
|
|
380
525
|
Scenario,
|
|
526
|
+
ThemeProvider,
|
|
381
527
|
useCompletion,
|
|
382
528
|
useLessonkit,
|
|
383
529
|
useProgress,
|
|
384
530
|
useQuizState,
|
|
531
|
+
useTheme,
|
|
385
532
|
useTracking
|
|
386
533
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "React components and hooks for building learning experiences with LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"dist"
|
|
38
38
|
],
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility",
|
|
41
|
-
"dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility",
|
|
40
|
+
"build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
|
|
41
|
+
"dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
|
|
42
42
|
"prepublishOnly": "npm run build",
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json",
|
|
44
44
|
"test": "vitest run --passWithNoTests",
|
|
@@ -50,9 +50,10 @@
|
|
|
50
50
|
"react-dom": ">=18"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lessonkit/accessibility": "0.
|
|
54
|
-
"@lessonkit/core": "0.
|
|
55
|
-
"@lessonkit/
|
|
53
|
+
"@lessonkit/accessibility": "0.4.0",
|
|
54
|
+
"@lessonkit/core": "0.4.0",
|
|
55
|
+
"@lessonkit/themes": "0.4.0",
|
|
56
|
+
"@lessonkit/xapi": "0.4.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@testing-library/react": "^16.3.0",
|