@lessonkit/react 0.3.1 → 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 +133 -8
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +135 -7
- 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);
|
|
@@ -43,7 +55,7 @@ var import_accessibility = require("@lessonkit/accessibility");
|
|
|
43
55
|
// src/context.tsx
|
|
44
56
|
var import_react = require("react");
|
|
45
57
|
var import_core2 = require("@lessonkit/core");
|
|
46
|
-
var
|
|
58
|
+
var import_xapi2 = require("@lessonkit/xapi");
|
|
47
59
|
|
|
48
60
|
// src/runtime/ports.ts
|
|
49
61
|
function createNoopStorage() {
|
|
@@ -72,6 +84,15 @@ function createSessionStoragePort() {
|
|
|
72
84
|
};
|
|
73
85
|
}
|
|
74
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 });
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
// src/runtime/session.ts
|
|
76
97
|
var import_core = require("@lessonkit/core");
|
|
77
98
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
@@ -115,12 +136,6 @@ function createTrackingClientFromConfig(config) {
|
|
|
115
136
|
batch: config.tracking?.batch
|
|
116
137
|
});
|
|
117
138
|
}
|
|
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
139
|
function LessonkitProvider(props) {
|
|
125
140
|
const config = props.config ?? {};
|
|
126
141
|
const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
@@ -174,7 +189,7 @@ function LessonkitProvider(props) {
|
|
|
174
189
|
batchFlushIntervalMs,
|
|
175
190
|
batchMaxBatchSize
|
|
176
191
|
]);
|
|
177
|
-
const xapiQueueRef = (0, import_react.useRef)((0,
|
|
192
|
+
const xapiQueueRef = (0, import_react.useRef)((0, import_xapi2.createInMemoryXAPIQueue)());
|
|
178
193
|
const xapiRef = (0, import_react.useRef)(null);
|
|
179
194
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
180
195
|
const xapiEnabled = config.xapi?.enabled;
|
|
@@ -418,6 +433,114 @@ function sanitizeLessonId(id) {
|
|
|
418
433
|
const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
419
434
|
return s.length ? s : "id";
|
|
420
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
|
+
}
|
|
421
544
|
// Annotate the CommonJS export names for ESM import in node:
|
|
422
545
|
0 && (module.exports = {
|
|
423
546
|
Course,
|
|
@@ -428,9 +551,11 @@ function sanitizeLessonId(id) {
|
|
|
428
551
|
Quiz,
|
|
429
552
|
Reflection,
|
|
430
553
|
Scenario,
|
|
554
|
+
ThemeProvider,
|
|
431
555
|
useCompletion,
|
|
432
556
|
useLessonkit,
|
|
433
557
|
useProgress,
|
|
434
558
|
useQuizState,
|
|
559
|
+
useTheme,
|
|
435
560
|
useTracking
|
|
436
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
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
useState
|
|
14
14
|
} from "react";
|
|
15
15
|
import { createTrackingClient, nowIso } from "@lessonkit/core";
|
|
16
|
-
import { createInMemoryXAPIQueue
|
|
16
|
+
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
17
17
|
|
|
18
18
|
// src/runtime/ports.ts
|
|
19
19
|
function createNoopStorage() {
|
|
@@ -42,6 +42,15 @@ function createSessionStoragePort() {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
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 });
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
// src/runtime/session.ts
|
|
46
55
|
import { createSessionId } from "@lessonkit/core";
|
|
47
56
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
@@ -85,12 +94,6 @@ function createTrackingClientFromConfig(config) {
|
|
|
85
94
|
batch: config.tracking?.batch
|
|
86
95
|
});
|
|
87
96
|
}
|
|
88
|
-
function createXapiClientFromConfig(config, queue) {
|
|
89
|
-
if (config.xapi?.enabled === false) return null;
|
|
90
|
-
if (config.xapi?.client) return config.xapi.client;
|
|
91
|
-
const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
|
|
92
|
-
return createXAPIClient({ baseId, transport: config.xapi?.transport, queue });
|
|
93
|
-
}
|
|
94
97
|
function LessonkitProvider(props) {
|
|
95
98
|
const config = props.config ?? {};
|
|
96
99
|
const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
@@ -388,6 +391,129 @@ function sanitizeLessonId(id) {
|
|
|
388
391
|
const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
389
392
|
return s.length ? s : "id";
|
|
390
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
|
+
}
|
|
391
517
|
export {
|
|
392
518
|
Course,
|
|
393
519
|
KnowledgeCheck,
|
|
@@ -397,9 +523,11 @@ export {
|
|
|
397
523
|
Quiz,
|
|
398
524
|
Reflection,
|
|
399
525
|
Scenario,
|
|
526
|
+
ThemeProvider,
|
|
400
527
|
useCompletion,
|
|
401
528
|
useLessonkit,
|
|
402
529
|
useProgress,
|
|
403
530
|
useQuizState,
|
|
531
|
+
useTheme,
|
|
404
532
|
useTracking
|
|
405
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",
|