@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 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.3.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 import_xapi = require("@lessonkit/xapi");
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, import_xapi.createInMemoryXAPIQueue)());
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
- export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, useCompletion, useLessonkit, useProgress, useQuizState, useTracking };
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
- export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, useCompletion, useLessonkit, useProgress, useQuizState, useTracking };
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, createXAPIClient } from "@lessonkit/xapi";
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.1",
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.3.1",
54
- "@lessonkit/core": "0.3.1",
55
- "@lessonkit/xapi": "0.3.1"
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",