@lessonkit/react 0.1.0 → 0.2.1

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
@@ -1,5 +1,9 @@
1
1
  # `@lessonkit/react`
2
2
 
3
+ [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml)
4
+ [![npm](https://img.shields.io/npm/v/@lessonkit/react.svg)](https://www.npmjs.com/package/@lessonkit/react)
5
+ [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](../../LICENSE)
6
+
3
7
  React components and hooks for building learning experiences in LessonKit.
4
8
 
5
9
  ## Install
@@ -11,11 +15,26 @@ npm install @lessonkit/react react react-dom
11
15
  ## Quick example
12
16
 
13
17
  ```tsx
18
+ import { useMemo } from "react";
19
+ import type { TelemetryEvent } from "@lessonkit/core";
14
20
  import { Course, Lesson, Quiz, Scenario, ProgressTracker } from "@lessonkit/react";
21
+ import type { XAPIStatement } from "@lessonkit/xapi";
15
22
 
16
23
  export default function App() {
24
+ const config = useMemo(
25
+ () => ({
26
+ tracking: {
27
+ sink: (event: TelemetryEvent) => console.log(event),
28
+ },
29
+ xapi: {
30
+ transport: (statement: XAPIStatement) => console.log(statement),
31
+ },
32
+ }),
33
+ [],
34
+ );
35
+
17
36
  return (
18
- <Course title="Cybersecurity Basics" courseId="cyber-basics">
37
+ <Course title="Cybersecurity Basics" courseId="cyber-basics" config={config}>
19
38
  <ProgressTracker />
20
39
 
21
40
  <Lesson title="Phishing Awareness" lessonId="phishing-101">
@@ -34,7 +53,7 @@ export default function App() {
34
53
  }
35
54
  ```
36
55
 
37
- ## API (0.1.x)
56
+ ## API (0.2.1)
38
57
 
39
58
  ### Components
40
59
 
@@ -57,4 +76,11 @@ export default function App() {
57
76
 
58
77
  - `@lessonkit/react` ships **framework primitives**, not content. You bring your own layout/content
59
78
  and compose interactions as React components.
79
+ - `Course` accepts a `config` prop that is passed through to `LessonkitProvider` (tracking sink,
80
+ optional `xapi.transport` or custom `xapi.client`, session metadata). Hoist `config` with `useMemo`
81
+ so tracking/xAPI clients are not recreated every render.
82
+ - When a `<Lesson>` unmounts (for example, wizard navigation), it automatically calls `completeLesson`
83
+ for that lesson. Use stable `lessonId` values so completion and time-on-task telemetry stay consistent.
84
+ - If you omit `session.sessionId`, the provider reuses a tab-scoped id via `sessionStorage` so React
85
+ Strict Mode remounts do not split analytics sessions in development.
60
86
 
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  Course: () => Course,
24
24
  KnowledgeCheck: () => KnowledgeCheck,
25
25
  Lesson: () => Lesson,
26
+ LessonkitProvider: () => LessonkitProvider,
26
27
  ProgressTracker: () => ProgressTracker,
27
28
  Quiz: () => Quiz,
28
29
  Reflection: () => Reflection,
@@ -37,6 +38,7 @@ module.exports = __toCommonJS(index_exports);
37
38
 
38
39
  // src/components.tsx
39
40
  var import_react3 = require("react");
41
+ var import_accessibility = require("@lessonkit/accessibility");
40
42
 
41
43
  // src/context.tsx
42
44
  var import_react = require("react");
@@ -44,77 +46,200 @@ var import_core = require("@lessonkit/core");
44
46
  var import_xapi = require("@lessonkit/xapi");
45
47
  var import_jsx_runtime = require("react/jsx-runtime");
46
48
  var LessonkitContext = (0, import_react.createContext)(null);
49
+ var SESSION_STORAGE_KEY = "lessonkit:sessionId";
50
+ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
51
+ function disposeTrackingClient(client) {
52
+ client?.flush?.();
53
+ client?.dispose?.();
54
+ }
55
+ function resolveSessionId(provided) {
56
+ if (provided) return provided;
57
+ if (typeof sessionStorage !== "undefined") {
58
+ const existing = sessionStorage.getItem(SESSION_STORAGE_KEY);
59
+ if (existing) return existing;
60
+ const id = (0, import_core.createSessionId)();
61
+ sessionStorage.setItem(SESSION_STORAGE_KEY, id);
62
+ return id;
63
+ }
64
+ return (0, import_core.createSessionId)();
65
+ }
66
+ function courseStartedStorageKey(sessionId, courseId) {
67
+ return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
68
+ }
69
+ function hasCourseStarted(sessionId, courseId) {
70
+ if (typeof sessionStorage === "undefined") return false;
71
+ return sessionStorage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
72
+ }
73
+ function markCourseStarted(sessionId, courseId) {
74
+ if (typeof sessionStorage === "undefined") return;
75
+ sessionStorage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
76
+ }
77
+ function createTrackingClientFromConfig(config) {
78
+ if (config.tracking?.enabled === false) {
79
+ return (0, import_core.createTrackingClient)();
80
+ }
81
+ return (0, import_core.createTrackingClient)({
82
+ sink: config.tracking?.sink,
83
+ batchSink: config.tracking?.batchSink,
84
+ batch: config.tracking?.batch
85
+ });
86
+ }
87
+ function createXapiClientFromConfig(config, queue) {
88
+ if (config.xapi?.enabled === false) return null;
89
+ if (config.xapi?.client) return config.xapi.client;
90
+ const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
91
+ return (0, import_xapi.createXAPIClient)({ baseId, transport: config.xapi?.transport, queue });
92
+ }
47
93
  function LessonkitProvider(props) {
48
94
  const config = props.config ?? {};
49
- const tracking = (0, import_react.useMemo)(() => {
50
- if (config.tracking?.enabled === false) return (0, import_core.createTrackingClient)();
51
- return (0, import_core.createTrackingClient)({ sink: config.tracking?.sink });
52
- }, [config.tracking?.enabled, config.tracking?.sink]);
53
- const xapi = (0, import_react.useMemo)(() => {
54
- if (config.xapi?.enabled === false) return null;
55
- return config.xapi?.client ?? (0, import_xapi.createXAPIClient)();
56
- }, [config.xapi?.enabled, config.xapi?.client]);
95
+ const sessionIdRef = (0, import_react.useRef)(resolveSessionId(config.session?.sessionId));
96
+ if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
97
+ const attemptIdRef = (0, import_react.useRef)(config.session?.attemptId);
98
+ const userRef = (0, import_react.useRef)(config.session?.user);
99
+ attemptIdRef.current = config.session?.attemptId;
100
+ userRef.current = config.session?.user;
101
+ const courseIdRef = (0, import_react.useRef)(config.courseId);
102
+ courseIdRef.current = config.courseId;
103
+ const trackingRef = (0, import_react.useRef)((0, import_core.createTrackingClient)());
104
+ const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
105
+ const trackingEnabled = config.tracking?.enabled;
106
+ const trackingSink = config.tracking?.sink;
107
+ const trackingBatchSink = config.tracking?.batchSink;
108
+ const batchEnabled = config.tracking?.batch?.enabled;
109
+ const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
110
+ const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
111
+ (0, import_react.useLayoutEffect)(() => {
112
+ const prev = trackingRef.current;
113
+ const next = createTrackingClientFromConfig(config);
114
+ trackingRef.current = next;
115
+ setTracking(next);
116
+ const sessionId = sessionIdRef.current;
117
+ const cid = courseIdRef.current;
118
+ if (!hasCourseStarted(sessionId, cid)) {
119
+ markCourseStarted(sessionId, cid);
120
+ next.track({
121
+ name: "course_started",
122
+ timestamp: (0, import_core.nowIso)(),
123
+ courseId: cid,
124
+ sessionId,
125
+ attemptId: attemptIdRef.current,
126
+ user: userRef.current
127
+ });
128
+ }
129
+ return () => {
130
+ disposeTrackingClient(prev);
131
+ };
132
+ }, [
133
+ trackingEnabled,
134
+ trackingSink,
135
+ trackingBatchSink,
136
+ batchEnabled,
137
+ batchFlushIntervalMs,
138
+ batchMaxBatchSize
139
+ ]);
140
+ const xapiQueueRef = (0, import_react.useRef)((0, import_xapi.createInMemoryXAPIQueue)());
141
+ const xapiRef = (0, import_react.useRef)(null);
142
+ const [xapi, setXapi] = (0, import_react.useState)(null);
143
+ const xapiEnabled = config.xapi?.enabled;
144
+ const xapiClient = config.xapi?.client;
145
+ const xapiTransport = config.xapi?.transport;
146
+ const courseId = config.courseId;
147
+ (0, import_react.useLayoutEffect)(() => {
148
+ const prev = xapiRef.current;
149
+ const next = createXapiClientFromConfig(config, xapiQueueRef.current);
150
+ xapiRef.current = next;
151
+ setXapi(next);
152
+ void (async () => {
153
+ if (prev) await prev.flush();
154
+ await next?.flush();
155
+ })();
156
+ return () => {
157
+ void prev?.flush();
158
+ };
159
+ }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
57
160
  const [completedLessonIds, setCompletedLessonIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
161
+ const completedLessonIdsRef = (0, import_react.useRef)(completedLessonIds);
162
+ completedLessonIdsRef.current = completedLessonIds;
58
163
  const [activeLessonId, setActiveLessonId] = (0, import_react.useState)(void 0);
59
164
  const [courseCompleted, setCourseCompleted] = (0, import_react.useState)(false);
60
- const courseIdRef = (0, import_react.useRef)(config.courseId);
61
- courseIdRef.current = config.courseId;
165
+ const courseCompletedRef = (0, import_react.useRef)(false);
166
+ courseCompletedRef.current = courseCompleted;
167
+ const activeLessonIdRef = (0, import_react.useRef)(void 0);
168
+ activeLessonIdRef.current = activeLessonId;
169
+ const lessonStartTimesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
62
170
  const track = (0, import_react.useCallback)(
63
- (name, data) => {
64
- tracking.track({
171
+ (name, data, opts) => {
172
+ trackingRef.current?.track({
65
173
  name,
66
174
  timestamp: (0, import_core.nowIso)(),
67
175
  courseId: courseIdRef.current,
68
- lessonId: activeLessonId,
176
+ lessonId: opts?.lessonId ?? activeLessonIdRef.current,
177
+ sessionId: sessionIdRef.current,
178
+ attemptId: attemptIdRef.current,
179
+ user: userRef.current,
69
180
  data
70
181
  });
71
182
  },
72
- [tracking, activeLessonId]
73
- );
74
- const setActiveLesson = (0, import_react.useCallback)(
75
- (lessonId) => {
76
- setActiveLessonId(lessonId);
77
- track("lesson_started", { lessonId });
78
- xapi?.startedLesson({ lessonId });
79
- },
80
- [track, xapi]
183
+ []
81
184
  );
185
+ (0, import_react.useEffect)(() => {
186
+ return () => {
187
+ trackingRef.current?.flush?.();
188
+ void xapiRef.current?.flush();
189
+ };
190
+ }, []);
191
+ const setActiveLesson = (0, import_react.useCallback)((lessonId) => {
192
+ if (activeLessonIdRef.current === lessonId) return;
193
+ activeLessonIdRef.current = lessonId;
194
+ setActiveLessonId(lessonId);
195
+ lessonStartTimesRef.current.set(lessonId, Date.now());
196
+ track("lesson_started", { lessonId }, { lessonId });
197
+ xapiRef.current?.startedLesson({ lessonId });
198
+ }, [track]);
82
199
  const completeLesson = (0, import_react.useCallback)(
83
200
  (lessonId) => {
84
- setCompletedLessonIds((prev) => new Set(prev).add(lessonId));
85
- track("lesson_completed", { lessonId });
86
- xapi?.completeLesson({ lessonId });
201
+ if (completedLessonIdsRef.current.has(lessonId)) return;
202
+ completedLessonIdsRef.current = new Set(completedLessonIdsRef.current).add(lessonId);
203
+ setCompletedLessonIds(completedLessonIdsRef.current);
204
+ const startedAt = lessonStartTimesRef.current.get(lessonId);
205
+ lessonStartTimesRef.current.delete(lessonId);
206
+ const durationMs = typeof startedAt === "number" ? Math.max(0, Date.now() - startedAt) : void 0;
207
+ track("lesson_completed", { lessonId, durationMs }, { lessonId });
208
+ if (durationMs !== void 0) {
209
+ track("lesson_time_on_task", { lessonId, durationMs }, { lessonId });
210
+ }
211
+ xapiRef.current?.completeLesson({ lessonId, durationMs });
87
212
  },
88
- [track, xapi]
213
+ [track]
89
214
  );
90
215
  const completeCourse = (0, import_react.useCallback)(() => {
216
+ if (courseCompletedRef.current) return;
217
+ courseCompletedRef.current = true;
91
218
  setCourseCompleted(true);
92
219
  track("course_completed");
93
- xapi?.completeCourse({});
94
- }, [track, xapi]);
220
+ xapiRef.current?.completeCourse();
221
+ }, [track]);
222
+ const progress = (0, import_react.useMemo)(
223
+ () => ({
224
+ activeLessonId,
225
+ completedLessonIds: new Set(completedLessonIds),
226
+ courseCompleted
227
+ }),
228
+ [activeLessonId, completedLessonIds, courseCompleted]
229
+ );
95
230
  const runtime = (0, import_react.useMemo)(
96
231
  () => ({
97
232
  config,
98
233
  tracking,
99
234
  xapi,
100
- progress: { activeLessonId, completedLessonIds, courseCompleted },
235
+ session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
236
+ progress,
101
237
  setActiveLesson,
102
238
  completeLesson,
103
239
  completeCourse,
104
240
  track
105
241
  }),
106
- [
107
- config,
108
- tracking,
109
- xapi,
110
- activeLessonId,
111
- completedLessonIds,
112
- courseCompleted,
113
- setActiveLesson,
114
- completeLesson,
115
- completeCourse,
116
- track
117
- ]
242
+ [config, tracking, xapi, progress, setActiveLesson, completeLesson, completeCourse, track]
118
243
  );
119
244
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LessonkitContext.Provider, { value: runtime, children: props.children });
120
245
  }
@@ -156,7 +281,11 @@ function useQuizState() {
156
281
  // src/components.tsx
157
282
  var import_jsx_runtime2 = require("react/jsx-runtime");
158
283
  function Course(props) {
159
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonkitProvider, { config: { courseId: props.courseId }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": props.title, children: [
284
+ const providerConfig = (0, import_react3.useMemo)(
285
+ () => ({ ...props.config, courseId: props.courseId }),
286
+ [props.config, props.courseId]
287
+ );
288
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": props.title, children: [
160
289
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { children: props.title }),
161
290
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: props.children })
162
291
  ] }) });
@@ -164,7 +293,9 @@ function Course(props) {
164
293
  function Lesson(props) {
165
294
  const { setActiveLesson } = useLessonkit();
166
295
  const { completeLesson } = useCompletion();
167
- const id = props.lessonId ?? (0, import_react3.useMemo)(() => `lesson-${cryptoRandomId()}`, []);
296
+ const reactId = (0, import_react3.useId)();
297
+ const generatedId = (0, import_react3.useMemo)(() => `lesson-${sanitizeLessonId(reactId)}`, [reactId]);
298
+ const id = props.lessonId ?? generatedId;
168
299
  (0, import_react3.useEffect)(() => {
169
300
  setActiveLesson(id);
170
301
  return () => {
@@ -184,7 +315,13 @@ function Reflection(props) {
184
315
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Reflection", children: [
185
316
  props.prompt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: promptId, children: props.prompt }) : null,
186
317
  props.children,
187
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("textarea", { "aria-labelledby": props.prompt ? promptId : void 0 })
318
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
319
+ "textarea",
320
+ {
321
+ "aria-labelledby": props.prompt ? promptId : void 0,
322
+ "aria-label": props.prompt ? void 0 : "Reflection response"
323
+ }
324
+ )
188
325
  ] });
189
326
  }
190
327
  function KnowledgeCheck(props) {
@@ -193,12 +330,13 @@ function KnowledgeCheck(props) {
193
330
  function Quiz(props) {
194
331
  const quiz = useQuizState();
195
332
  const [selected, setSelected] = (0, import_react3.useState)(null);
333
+ const completedRef = (0, import_react3.useRef)(false);
196
334
  const questionId = (0, import_react3.useId)();
197
335
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", children: [
198
336
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
199
337
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
200
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { className: "sr-only", children: "Quiz choices" }),
201
- props.choices.map((c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: { display: "block" }, children: [
338
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("legend", { style: import_accessibility.visuallyHiddenStyle, children: "Quiz choices" }),
339
+ props.choices.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: { display: "block" }, children: [
202
340
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
203
341
  "input",
204
342
  {
@@ -208,12 +346,17 @@ function Quiz(props) {
208
346
  checked: selected === c,
209
347
  onChange: () => {
210
348
  setSelected(c);
211
- quiz.answer({ question: props.question, choice: c, correct: c === props.answer });
349
+ const correct = c === props.answer;
350
+ quiz.answer({ question: props.question, choice: c, correct });
351
+ if (correct && !completedRef.current) {
352
+ completedRef.current = true;
353
+ quiz.complete({ score: 1, maxScore: 1 });
354
+ }
212
355
  }
213
356
  }
214
357
  ),
215
358
  c
216
- ] }, c))
359
+ ] }, `${questionId}-${i}`))
217
360
  ] }),
218
361
  selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selected === props.answer ? "Correct" : "Try again" }) : null
219
362
  ] });
@@ -226,16 +369,16 @@ function ProgressTracker() {
226
369
  completed
227
370
  ] }) });
228
371
  }
229
- function cryptoRandomId() {
230
- const g = globalThis;
231
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
232
- return Math.random().toString(16).slice(2);
372
+ function sanitizeLessonId(id) {
373
+ const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
374
+ return s.length ? s : "id";
233
375
  }
234
376
  // Annotate the CommonJS export names for ESM import in node:
235
377
  0 && (module.exports = {
236
378
  Course,
237
379
  KnowledgeCheck,
238
380
  Lesson,
381
+ LessonkitProvider,
239
382
  ProgressTracker,
240
383
  Quiz,
241
384
  Reflection,
package/dist/index.d.cts CHANGED
@@ -1,12 +1,63 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import * as _lessonkit_core from '@lessonkit/core';
4
- import { LessonId, CourseId, TelemetryEvent, TrackingClient } from '@lessonkit/core';
5
- import { XAPIClient } from '@lessonkit/xapi';
4
+ import { CourseId, TelemetryUser, TelemetryEvent, TrackingClient, LessonId } from '@lessonkit/core';
5
+ import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
6
+
7
+ type LessonkitConfig = {
8
+ courseId?: CourseId;
9
+ session?: {
10
+ sessionId?: string;
11
+ attemptId?: string;
12
+ user?: TelemetryUser;
13
+ };
14
+ tracking?: {
15
+ enabled?: boolean;
16
+ sink?: (event: TelemetryEvent) => void | Promise<void>;
17
+ batchSink?: (events: TelemetryEvent[]) => void | Promise<void>;
18
+ batch?: {
19
+ enabled?: boolean;
20
+ flushIntervalMs?: number;
21
+ maxBatchSize?: number;
22
+ };
23
+ };
24
+ xapi?: {
25
+ enabled?: boolean;
26
+ transport?: XAPITransport;
27
+ client?: XAPIClient;
28
+ };
29
+ };
30
+ type ProgressState = {
31
+ activeLessonId?: LessonId;
32
+ completedLessonIds: ReadonlySet<LessonId>;
33
+ courseCompleted: boolean;
34
+ };
35
+ type LessonkitRuntime = {
36
+ config: LessonkitConfig;
37
+ tracking: TrackingClient;
38
+ xapi: XAPIClient | null;
39
+ progress: ProgressState;
40
+ session: {
41
+ sessionId: string;
42
+ attemptId?: string;
43
+ user?: TelemetryUser;
44
+ };
45
+ setActiveLesson: (lessonId: LessonId) => void;
46
+ completeLesson: (lessonId: LessonId) => void;
47
+ completeCourse: () => void;
48
+ track: (name: TelemetryEvent["name"], data?: TelemetryEvent["data"], opts?: {
49
+ lessonId?: LessonId;
50
+ }) => void;
51
+ };
52
+ declare function LessonkitProvider(props: {
53
+ config?: LessonkitConfig;
54
+ children: React.ReactNode;
55
+ }): react_jsx_runtime.JSX.Element;
6
56
 
7
57
  declare function Course(props: {
8
58
  title: string;
9
- courseId?: string;
59
+ courseId?: CourseId;
60
+ config?: Omit<React.ComponentProps<typeof LessonkitProvider>["config"], "courseId">;
10
61
  children: React.ReactNode;
11
62
  }): react_jsx_runtime.JSX.Element;
12
63
  declare function Lesson(props: {
@@ -33,37 +84,12 @@ declare function Quiz(props: {
33
84
  }): react_jsx_runtime.JSX.Element;
34
85
  declare function ProgressTracker(): react_jsx_runtime.JSX.Element;
35
86
 
36
- type LessonkitConfig = {
37
- courseId?: CourseId;
38
- tracking?: {
39
- enabled?: boolean;
40
- sink?: (event: TelemetryEvent) => void | Promise<void>;
41
- };
42
- xapi?: {
43
- enabled?: boolean;
44
- client?: XAPIClient;
45
- };
46
- };
47
- type ProgressState = {
48
- activeLessonId?: LessonId;
49
- completedLessonIds: Set<LessonId>;
50
- courseCompleted: boolean;
51
- };
52
- type LessonkitRuntime = {
53
- config: LessonkitConfig;
54
- tracking: TrackingClient;
55
- xapi: XAPIClient | null;
56
- progress: ProgressState;
57
- setActiveLesson: (lessonId: LessonId) => void;
58
- completeLesson: (lessonId: LessonId) => void;
59
- completeCourse: () => void;
60
- track: (name: TelemetryEvent["name"], data?: TelemetryEvent["data"]) => void;
61
- };
62
-
63
87
  declare function useLessonkit(): LessonkitRuntime;
64
88
  declare function useProgress(): ProgressState;
65
89
  declare function useTracking(): {
66
- track: (name: _lessonkit_core.TelemetryEvent["name"], data?: _lessonkit_core.TelemetryEvent["data"]) => void;
90
+ track: (name: _lessonkit_core.TelemetryEvent["name"], data?: _lessonkit_core.TelemetryEvent["data"], opts?: {
91
+ lessonId?: _lessonkit_core.LessonId;
92
+ }) => void;
67
93
  };
68
94
  declare function useCompletion(): {
69
95
  completeLesson: (lessonId: _lessonkit_core.LessonId) => void;
@@ -81,4 +107,4 @@ declare function useQuizState(): {
81
107
  }) => void;
82
108
  };
83
109
 
84
- export { Course, KnowledgeCheck, Lesson, ProgressTracker, Quiz, Reflection, Scenario, useCompletion, useLessonkit, useProgress, useQuizState, useTracking };
110
+ export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, useCompletion, useLessonkit, useProgress, useQuizState, useTracking };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,63 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import * as _lessonkit_core from '@lessonkit/core';
4
- import { LessonId, CourseId, TelemetryEvent, TrackingClient } from '@lessonkit/core';
5
- import { XAPIClient } from '@lessonkit/xapi';
4
+ import { CourseId, TelemetryUser, TelemetryEvent, TrackingClient, LessonId } from '@lessonkit/core';
5
+ import { XAPITransport, XAPIClient } from '@lessonkit/xapi';
6
+
7
+ type LessonkitConfig = {
8
+ courseId?: CourseId;
9
+ session?: {
10
+ sessionId?: string;
11
+ attemptId?: string;
12
+ user?: TelemetryUser;
13
+ };
14
+ tracking?: {
15
+ enabled?: boolean;
16
+ sink?: (event: TelemetryEvent) => void | Promise<void>;
17
+ batchSink?: (events: TelemetryEvent[]) => void | Promise<void>;
18
+ batch?: {
19
+ enabled?: boolean;
20
+ flushIntervalMs?: number;
21
+ maxBatchSize?: number;
22
+ };
23
+ };
24
+ xapi?: {
25
+ enabled?: boolean;
26
+ transport?: XAPITransport;
27
+ client?: XAPIClient;
28
+ };
29
+ };
30
+ type ProgressState = {
31
+ activeLessonId?: LessonId;
32
+ completedLessonIds: ReadonlySet<LessonId>;
33
+ courseCompleted: boolean;
34
+ };
35
+ type LessonkitRuntime = {
36
+ config: LessonkitConfig;
37
+ tracking: TrackingClient;
38
+ xapi: XAPIClient | null;
39
+ progress: ProgressState;
40
+ session: {
41
+ sessionId: string;
42
+ attemptId?: string;
43
+ user?: TelemetryUser;
44
+ };
45
+ setActiveLesson: (lessonId: LessonId) => void;
46
+ completeLesson: (lessonId: LessonId) => void;
47
+ completeCourse: () => void;
48
+ track: (name: TelemetryEvent["name"], data?: TelemetryEvent["data"], opts?: {
49
+ lessonId?: LessonId;
50
+ }) => void;
51
+ };
52
+ declare function LessonkitProvider(props: {
53
+ config?: LessonkitConfig;
54
+ children: React.ReactNode;
55
+ }): react_jsx_runtime.JSX.Element;
6
56
 
7
57
  declare function Course(props: {
8
58
  title: string;
9
- courseId?: string;
59
+ courseId?: CourseId;
60
+ config?: Omit<React.ComponentProps<typeof LessonkitProvider>["config"], "courseId">;
10
61
  children: React.ReactNode;
11
62
  }): react_jsx_runtime.JSX.Element;
12
63
  declare function Lesson(props: {
@@ -33,37 +84,12 @@ declare function Quiz(props: {
33
84
  }): react_jsx_runtime.JSX.Element;
34
85
  declare function ProgressTracker(): react_jsx_runtime.JSX.Element;
35
86
 
36
- type LessonkitConfig = {
37
- courseId?: CourseId;
38
- tracking?: {
39
- enabled?: boolean;
40
- sink?: (event: TelemetryEvent) => void | Promise<void>;
41
- };
42
- xapi?: {
43
- enabled?: boolean;
44
- client?: XAPIClient;
45
- };
46
- };
47
- type ProgressState = {
48
- activeLessonId?: LessonId;
49
- completedLessonIds: Set<LessonId>;
50
- courseCompleted: boolean;
51
- };
52
- type LessonkitRuntime = {
53
- config: LessonkitConfig;
54
- tracking: TrackingClient;
55
- xapi: XAPIClient | null;
56
- progress: ProgressState;
57
- setActiveLesson: (lessonId: LessonId) => void;
58
- completeLesson: (lessonId: LessonId) => void;
59
- completeCourse: () => void;
60
- track: (name: TelemetryEvent["name"], data?: TelemetryEvent["data"]) => void;
61
- };
62
-
63
87
  declare function useLessonkit(): LessonkitRuntime;
64
88
  declare function useProgress(): ProgressState;
65
89
  declare function useTracking(): {
66
- track: (name: _lessonkit_core.TelemetryEvent["name"], data?: _lessonkit_core.TelemetryEvent["data"]) => void;
90
+ track: (name: _lessonkit_core.TelemetryEvent["name"], data?: _lessonkit_core.TelemetryEvent["data"], opts?: {
91
+ lessonId?: _lessonkit_core.LessonId;
92
+ }) => void;
67
93
  };
68
94
  declare function useCompletion(): {
69
95
  completeLesson: (lessonId: _lessonkit_core.LessonId) => void;
@@ -81,4 +107,4 @@ declare function useQuizState(): {
81
107
  }) => void;
82
108
  };
83
109
 
84
- export { Course, KnowledgeCheck, Lesson, ProgressTracker, Quiz, Reflection, Scenario, useCompletion, useLessonkit, useProgress, useQuizState, useTracking };
110
+ export { Course, KnowledgeCheck, Lesson, type LessonkitConfig, LessonkitProvider, type LessonkitRuntime, ProgressTracker, Quiz, Reflection, Scenario, useCompletion, useLessonkit, useProgress, useQuizState, useTracking };
package/dist/index.js CHANGED
@@ -1,83 +1,215 @@
1
1
  // src/components.tsx
2
- import { useEffect, useId, useMemo as useMemo3, useState as useState2 } from "react";
2
+ import { useEffect as useEffect2, useId, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
3
+ import { visuallyHiddenStyle } from "@lessonkit/accessibility";
3
4
 
4
5
  // src/context.tsx
5
- import { createContext, useCallback, useMemo, useRef, useState } from "react";
6
- import { createTrackingClient, nowIso } from "@lessonkit/core";
7
- import { createXAPIClient } from "@lessonkit/xapi";
6
+ import {
7
+ createContext,
8
+ useCallback,
9
+ useEffect,
10
+ useLayoutEffect,
11
+ useMemo,
12
+ useRef,
13
+ useState
14
+ } from "react";
15
+ import { createSessionId, createTrackingClient, nowIso } from "@lessonkit/core";
16
+ import { createInMemoryXAPIQueue, createXAPIClient } from "@lessonkit/xapi";
8
17
  import { jsx } from "react/jsx-runtime";
9
18
  var LessonkitContext = createContext(null);
19
+ var SESSION_STORAGE_KEY = "lessonkit:sessionId";
20
+ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
21
+ function disposeTrackingClient(client) {
22
+ client?.flush?.();
23
+ client?.dispose?.();
24
+ }
25
+ function resolveSessionId(provided) {
26
+ if (provided) return provided;
27
+ if (typeof sessionStorage !== "undefined") {
28
+ const existing = sessionStorage.getItem(SESSION_STORAGE_KEY);
29
+ if (existing) return existing;
30
+ const id = createSessionId();
31
+ sessionStorage.setItem(SESSION_STORAGE_KEY, id);
32
+ return id;
33
+ }
34
+ return createSessionId();
35
+ }
36
+ function courseStartedStorageKey(sessionId, courseId) {
37
+ return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
38
+ }
39
+ function hasCourseStarted(sessionId, courseId) {
40
+ if (typeof sessionStorage === "undefined") return false;
41
+ return sessionStorage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
42
+ }
43
+ function markCourseStarted(sessionId, courseId) {
44
+ if (typeof sessionStorage === "undefined") return;
45
+ sessionStorage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
46
+ }
47
+ function createTrackingClientFromConfig(config) {
48
+ if (config.tracking?.enabled === false) {
49
+ return createTrackingClient();
50
+ }
51
+ return createTrackingClient({
52
+ sink: config.tracking?.sink,
53
+ batchSink: config.tracking?.batchSink,
54
+ batch: config.tracking?.batch
55
+ });
56
+ }
57
+ function createXapiClientFromConfig(config, queue) {
58
+ if (config.xapi?.enabled === false) return null;
59
+ if (config.xapi?.client) return config.xapi.client;
60
+ const baseId = config.courseId ? `urn:lessonkit:course:${config.courseId}` : void 0;
61
+ return createXAPIClient({ baseId, transport: config.xapi?.transport, queue });
62
+ }
10
63
  function LessonkitProvider(props) {
11
64
  const config = props.config ?? {};
12
- const tracking = useMemo(() => {
13
- if (config.tracking?.enabled === false) return createTrackingClient();
14
- return createTrackingClient({ sink: config.tracking?.sink });
15
- }, [config.tracking?.enabled, config.tracking?.sink]);
16
- const xapi = useMemo(() => {
17
- if (config.xapi?.enabled === false) return null;
18
- return config.xapi?.client ?? createXAPIClient();
19
- }, [config.xapi?.enabled, config.xapi?.client]);
65
+ const sessionIdRef = useRef(resolveSessionId(config.session?.sessionId));
66
+ if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
67
+ const attemptIdRef = useRef(config.session?.attemptId);
68
+ const userRef = useRef(config.session?.user);
69
+ attemptIdRef.current = config.session?.attemptId;
70
+ userRef.current = config.session?.user;
71
+ const courseIdRef = useRef(config.courseId);
72
+ courseIdRef.current = config.courseId;
73
+ const trackingRef = useRef(createTrackingClient());
74
+ const [tracking, setTracking] = useState(() => trackingRef.current);
75
+ const trackingEnabled = config.tracking?.enabled;
76
+ const trackingSink = config.tracking?.sink;
77
+ const trackingBatchSink = config.tracking?.batchSink;
78
+ const batchEnabled = config.tracking?.batch?.enabled;
79
+ const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
80
+ const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
81
+ useLayoutEffect(() => {
82
+ const prev = trackingRef.current;
83
+ const next = createTrackingClientFromConfig(config);
84
+ trackingRef.current = next;
85
+ setTracking(next);
86
+ const sessionId = sessionIdRef.current;
87
+ const cid = courseIdRef.current;
88
+ if (!hasCourseStarted(sessionId, cid)) {
89
+ markCourseStarted(sessionId, cid);
90
+ next.track({
91
+ name: "course_started",
92
+ timestamp: nowIso(),
93
+ courseId: cid,
94
+ sessionId,
95
+ attemptId: attemptIdRef.current,
96
+ user: userRef.current
97
+ });
98
+ }
99
+ return () => {
100
+ disposeTrackingClient(prev);
101
+ };
102
+ }, [
103
+ trackingEnabled,
104
+ trackingSink,
105
+ trackingBatchSink,
106
+ batchEnabled,
107
+ batchFlushIntervalMs,
108
+ batchMaxBatchSize
109
+ ]);
110
+ const xapiQueueRef = useRef(createInMemoryXAPIQueue());
111
+ const xapiRef = useRef(null);
112
+ const [xapi, setXapi] = useState(null);
113
+ const xapiEnabled = config.xapi?.enabled;
114
+ const xapiClient = config.xapi?.client;
115
+ const xapiTransport = config.xapi?.transport;
116
+ const courseId = config.courseId;
117
+ useLayoutEffect(() => {
118
+ const prev = xapiRef.current;
119
+ const next = createXapiClientFromConfig(config, xapiQueueRef.current);
120
+ xapiRef.current = next;
121
+ setXapi(next);
122
+ void (async () => {
123
+ if (prev) await prev.flush();
124
+ await next?.flush();
125
+ })();
126
+ return () => {
127
+ void prev?.flush();
128
+ };
129
+ }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
20
130
  const [completedLessonIds, setCompletedLessonIds] = useState(() => /* @__PURE__ */ new Set());
131
+ const completedLessonIdsRef = useRef(completedLessonIds);
132
+ completedLessonIdsRef.current = completedLessonIds;
21
133
  const [activeLessonId, setActiveLessonId] = useState(void 0);
22
134
  const [courseCompleted, setCourseCompleted] = useState(false);
23
- const courseIdRef = useRef(config.courseId);
24
- courseIdRef.current = config.courseId;
135
+ const courseCompletedRef = useRef(false);
136
+ courseCompletedRef.current = courseCompleted;
137
+ const activeLessonIdRef = useRef(void 0);
138
+ activeLessonIdRef.current = activeLessonId;
139
+ const lessonStartTimesRef = useRef(/* @__PURE__ */ new Map());
25
140
  const track = useCallback(
26
- (name, data) => {
27
- tracking.track({
141
+ (name, data, opts) => {
142
+ trackingRef.current?.track({
28
143
  name,
29
144
  timestamp: nowIso(),
30
145
  courseId: courseIdRef.current,
31
- lessonId: activeLessonId,
146
+ lessonId: opts?.lessonId ?? activeLessonIdRef.current,
147
+ sessionId: sessionIdRef.current,
148
+ attemptId: attemptIdRef.current,
149
+ user: userRef.current,
32
150
  data
33
151
  });
34
152
  },
35
- [tracking, activeLessonId]
36
- );
37
- const setActiveLesson = useCallback(
38
- (lessonId) => {
39
- setActiveLessonId(lessonId);
40
- track("lesson_started", { lessonId });
41
- xapi?.startedLesson({ lessonId });
42
- },
43
- [track, xapi]
153
+ []
44
154
  );
155
+ useEffect(() => {
156
+ return () => {
157
+ trackingRef.current?.flush?.();
158
+ void xapiRef.current?.flush();
159
+ };
160
+ }, []);
161
+ const setActiveLesson = useCallback((lessonId) => {
162
+ if (activeLessonIdRef.current === lessonId) return;
163
+ activeLessonIdRef.current = lessonId;
164
+ setActiveLessonId(lessonId);
165
+ lessonStartTimesRef.current.set(lessonId, Date.now());
166
+ track("lesson_started", { lessonId }, { lessonId });
167
+ xapiRef.current?.startedLesson({ lessonId });
168
+ }, [track]);
45
169
  const completeLesson = useCallback(
46
170
  (lessonId) => {
47
- setCompletedLessonIds((prev) => new Set(prev).add(lessonId));
48
- track("lesson_completed", { lessonId });
49
- xapi?.completeLesson({ lessonId });
171
+ if (completedLessonIdsRef.current.has(lessonId)) return;
172
+ completedLessonIdsRef.current = new Set(completedLessonIdsRef.current).add(lessonId);
173
+ setCompletedLessonIds(completedLessonIdsRef.current);
174
+ const startedAt = lessonStartTimesRef.current.get(lessonId);
175
+ lessonStartTimesRef.current.delete(lessonId);
176
+ const durationMs = typeof startedAt === "number" ? Math.max(0, Date.now() - startedAt) : void 0;
177
+ track("lesson_completed", { lessonId, durationMs }, { lessonId });
178
+ if (durationMs !== void 0) {
179
+ track("lesson_time_on_task", { lessonId, durationMs }, { lessonId });
180
+ }
181
+ xapiRef.current?.completeLesson({ lessonId, durationMs });
50
182
  },
51
- [track, xapi]
183
+ [track]
52
184
  );
53
185
  const completeCourse = useCallback(() => {
186
+ if (courseCompletedRef.current) return;
187
+ courseCompletedRef.current = true;
54
188
  setCourseCompleted(true);
55
189
  track("course_completed");
56
- xapi?.completeCourse({});
57
- }, [track, xapi]);
190
+ xapiRef.current?.completeCourse();
191
+ }, [track]);
192
+ const progress = useMemo(
193
+ () => ({
194
+ activeLessonId,
195
+ completedLessonIds: new Set(completedLessonIds),
196
+ courseCompleted
197
+ }),
198
+ [activeLessonId, completedLessonIds, courseCompleted]
199
+ );
58
200
  const runtime = useMemo(
59
201
  () => ({
60
202
  config,
61
203
  tracking,
62
204
  xapi,
63
- progress: { activeLessonId, completedLessonIds, courseCompleted },
205
+ session: { sessionId: sessionIdRef.current, attemptId: attemptIdRef.current, user: userRef.current },
206
+ progress,
64
207
  setActiveLesson,
65
208
  completeLesson,
66
209
  completeCourse,
67
210
  track
68
211
  }),
69
- [
70
- config,
71
- tracking,
72
- xapi,
73
- activeLessonId,
74
- completedLessonIds,
75
- courseCompleted,
76
- setActiveLesson,
77
- completeLesson,
78
- completeCourse,
79
- track
80
- ]
212
+ [config, tracking, xapi, progress, setActiveLesson, completeLesson, completeCourse, track]
81
213
  );
82
214
  return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
83
215
  }
@@ -119,7 +251,11 @@ function useQuizState() {
119
251
  // src/components.tsx
120
252
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
121
253
  function Course(props) {
122
- return /* @__PURE__ */ jsx2(LessonkitProvider, { config: { courseId: props.courseId }, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
254
+ const providerConfig = useMemo3(
255
+ () => ({ ...props.config, courseId: props.courseId }),
256
+ [props.config, props.courseId]
257
+ );
258
+ return /* @__PURE__ */ jsx2(LessonkitProvider, { config: providerConfig, children: /* @__PURE__ */ jsxs("section", { "aria-label": props.title, children: [
123
259
  /* @__PURE__ */ jsx2("h1", { children: props.title }),
124
260
  /* @__PURE__ */ jsx2("div", { children: props.children })
125
261
  ] }) });
@@ -127,8 +263,10 @@ function Course(props) {
127
263
  function Lesson(props) {
128
264
  const { setActiveLesson } = useLessonkit();
129
265
  const { completeLesson } = useCompletion();
130
- const id = props.lessonId ?? useMemo3(() => `lesson-${cryptoRandomId()}`, []);
131
- useEffect(() => {
266
+ const reactId = useId();
267
+ const generatedId = useMemo3(() => `lesson-${sanitizeLessonId(reactId)}`, [reactId]);
268
+ const id = props.lessonId ?? generatedId;
269
+ useEffect2(() => {
132
270
  setActiveLesson(id);
133
271
  return () => {
134
272
  completeLesson(id);
@@ -147,7 +285,13 @@ function Reflection(props) {
147
285
  return /* @__PURE__ */ jsxs("section", { "aria-label": "Reflection", children: [
148
286
  props.prompt ? /* @__PURE__ */ jsx2("p", { id: promptId, children: props.prompt }) : null,
149
287
  props.children,
150
- /* @__PURE__ */ jsx2("textarea", { "aria-labelledby": props.prompt ? promptId : void 0 })
288
+ /* @__PURE__ */ jsx2(
289
+ "textarea",
290
+ {
291
+ "aria-labelledby": props.prompt ? promptId : void 0,
292
+ "aria-label": props.prompt ? void 0 : "Reflection response"
293
+ }
294
+ )
151
295
  ] });
152
296
  }
153
297
  function KnowledgeCheck(props) {
@@ -156,12 +300,13 @@ function KnowledgeCheck(props) {
156
300
  function Quiz(props) {
157
301
  const quiz = useQuizState();
158
302
  const [selected, setSelected] = useState2(null);
303
+ const completedRef = useRef2(false);
159
304
  const questionId = useId();
160
305
  return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", children: [
161
306
  /* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
162
307
  /* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
163
- /* @__PURE__ */ jsx2("legend", { className: "sr-only", children: "Quiz choices" }),
164
- props.choices.map((c) => /* @__PURE__ */ jsxs("label", { style: { display: "block" }, children: [
308
+ /* @__PURE__ */ jsx2("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
309
+ props.choices.map((c, i) => /* @__PURE__ */ jsxs("label", { style: { display: "block" }, children: [
165
310
  /* @__PURE__ */ jsx2(
166
311
  "input",
167
312
  {
@@ -171,12 +316,17 @@ function Quiz(props) {
171
316
  checked: selected === c,
172
317
  onChange: () => {
173
318
  setSelected(c);
174
- quiz.answer({ question: props.question, choice: c, correct: c === props.answer });
319
+ const correct = c === props.answer;
320
+ quiz.answer({ question: props.question, choice: c, correct });
321
+ if (correct && !completedRef.current) {
322
+ completedRef.current = true;
323
+ quiz.complete({ score: 1, maxScore: 1 });
324
+ }
175
325
  }
176
326
  }
177
327
  ),
178
328
  c
179
- ] }, c))
329
+ ] }, `${questionId}-${i}`))
180
330
  ] }),
181
331
  selected ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selected === props.answer ? "Correct" : "Try again" }) : null
182
332
  ] });
@@ -189,15 +339,15 @@ function ProgressTracker() {
189
339
  completed
190
340
  ] }) });
191
341
  }
192
- function cryptoRandomId() {
193
- const g = globalThis;
194
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
195
- return Math.random().toString(16).slice(2);
342
+ function sanitizeLessonId(id) {
343
+ const s = id.replace(/[^a-zA-Z0-9_-]/g, "");
344
+ return s.length ? s : "id";
196
345
  }
197
346
  export {
198
347
  Course,
199
348
  KnowledgeCheck,
200
349
  Lesson,
350
+ LessonkitProvider,
201
351
  ProgressTracker,
202
352
  Quiz,
203
353
  Reflection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -37,11 +37,12 @@
37
37
  "dist"
38
38
  ],
39
39
  "scripts": {
40
- "build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom",
41
- "dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom",
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",
42
42
  "prepublishOnly": "npm run build",
43
43
  "typecheck": "tsc -p tsconfig.json",
44
44
  "test": "vitest run --passWithNoTests",
45
+ "test:coverage": "vitest run --coverage --passWithNoTests=false",
45
46
  "lint": "echo \"(no lint configured yet)\""
46
47
  },
47
48
  "peerDependencies": {
@@ -49,8 +50,9 @@
49
50
  "react-dom": ">=18"
50
51
  },
51
52
  "dependencies": {
52
- "@lessonkit/core": "0.1.0",
53
- "@lessonkit/xapi": "0.1.0"
53
+ "@lessonkit/accessibility": "0.2.1",
54
+ "@lessonkit/core": "0.2.1",
55
+ "@lessonkit/xapi": "0.2.1"
54
56
  },
55
57
  "devDependencies": {
56
58
  "@testing-library/react": "^16.3.0",