@anantagnanam/lms 0.1.0 → 0.2.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.
Files changed (40) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +23 -2
  3. package/astro/client.ts +73 -151
  4. package/astro/components/CourseOverview.astro +25 -3
  5. package/astro/components/LearnerDashboard.astro +6 -0
  6. package/astro/components/LearningShell.astro +10 -1
  7. package/astro/components/LessonAvailabilityNote.astro +26 -0
  8. package/astro/components/LessonContent.astro +53 -0
  9. package/astro/components/LessonPlayer.astro +40 -61
  10. package/astro/components/MediaPlayer.astro +55 -4
  11. package/astro/media-player.ts +99 -0
  12. package/astro/pages/author-preview.astro +71 -0
  13. package/astro/pages/course.astro +10 -0
  14. package/astro/pages/lesson.astro +3 -0
  15. package/astro/pages/library.astro +173 -0
  16. package/astro/pages/paths.astro +73 -0
  17. package/astro/quiz-drafts.ts +87 -0
  18. package/astro/quiz-player.ts +362 -0
  19. package/astro/styles.css +12 -0
  20. package/dist/admin.d.ts +3 -1
  21. package/dist/admin.js +585 -116
  22. package/dist/admin.js.map +1 -1
  23. package/dist/astro.js +3 -0
  24. package/dist/astro.js.map +1 -1
  25. package/dist/{constants-C0kT94tc.js → constants-Cnx7k82Z.js} +25 -3
  26. package/dist/constants-Cnx7k82Z.js.map +1 -0
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/runtime.d.ts +104 -12
  30. package/dist/runtime.js +896 -116
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/schemas.d.ts +68 -1
  33. package/dist/schemas.js +34 -2
  34. package/dist/schemas.js.map +1 -1
  35. package/dist/server.d.ts +11 -1
  36. package/dist/server.js +13 -2
  37. package/dist/server.js.map +1 -1
  38. package/dist/types.d.ts +107 -4
  39. package/package.json +1 -1
  40. package/dist/constants-C0kT94tc.js.map +0 -1
@@ -0,0 +1,73 @@
1
+ ---
2
+ import Layout from "virtual:anantagnanam-lms/layout";
3
+ import LearningShell from "../components/LearningShell.astro";
4
+ import { lmsPublic, courseUrl, loginUrl, LmsError } from "@anantagnanam/lms/server";
5
+ import { basePath, loginPath } from "virtual:anantagnanam-lms/config";
6
+ import type { Page, PublicPath } from "@anantagnanam/lms/types";
7
+ export const prerender = false;
8
+ Astro.response.headers.set("Cache-Control", "private, no-store");
9
+ let page: Page<PublicPath> = { items: [], hasMore: false };
10
+ let error = "";
11
+ try {
12
+ page = await lmsPublic<typeof page>(
13
+ Astro.locals,
14
+ "paths",
15
+ { cursor: Astro.url.searchParams.get("cursor") ?? undefined },
16
+ Astro.url.origin,
17
+ );
18
+ } catch (cause) {
19
+ Astro.response.status = cause instanceof LmsError ? cause.status : 503;
20
+ error = cause instanceof Error ? cause.message : "Learning paths unavailable.";
21
+ }
22
+ const next = new URL(Astro.url);
23
+ next.searchParams.set("cursor", page.cursor ?? "");
24
+ ---
25
+
26
+ <Layout title="Learning paths">
27
+ <LearningShell>
28
+ <h1>Learning paths</h1>
29
+ <p class="lms-lead">
30
+ Follow an ordered program of courses and track your progress in your library.
31
+ </p>
32
+ <p>
33
+ Join a path to save its course list. Enroll in each course separately; approval and
34
+ prerequisites still apply.
35
+ </p>
36
+ {error && <p role="alert">{error}</p>}
37
+ {!error && !page.items.length && <p>No learning paths have been published yet.</p>}
38
+ {page.items.map((path) => (
39
+ <article class="lms-learning-card">
40
+ <h2>{path.title}</h2>
41
+ <p class="lms-text">{path.description}</p>
42
+ <ol>
43
+ {path.courses.map((course) => (
44
+ <li>
45
+ {course ? (
46
+ <a href={courseUrl(basePath, course.slug)}>{course.title}</a>
47
+ ) : (
48
+ "Course currently unavailable"
49
+ )}
50
+ </li>
51
+ ))}
52
+ </ol>
53
+ {Astro.locals.user ? (
54
+ <form data-lms-route="learner/path/join">
55
+ <input type="hidden" name="pathId" value={path.id} />
56
+ <button class="lms-btn" type="submit">
57
+ Join path
58
+ </button>
59
+ <a class="lms-btn lms-btn-outline" href={`${basePath}/my-learning/library`}>
60
+ View my paths
61
+ </a>
62
+ <p data-lms-feedback aria-live="polite"></p>
63
+ </form>
64
+ ) : (
65
+ <a class="lms-btn" href={loginUrl(loginPath, Astro.url.pathname)}>
66
+ Sign in to join
67
+ </a>
68
+ )}
69
+ </article>
70
+ ))}
71
+ {page.hasMore && page.cursor && <a href={next.href}>More learning paths →</a>}
72
+ </LearningShell>
73
+ </Layout>
@@ -0,0 +1,87 @@
1
+ import type { Answers } from "@anantagnanam/lms/types";
2
+
3
+ export type DraftState = "saved" | "pending" | "saving" | "failed" | "conflict";
4
+ export interface DraftWrite {
5
+ answers: Answers;
6
+ expectedDraftRevision: number;
7
+ }
8
+ const fingerprint = (answers: Answers) =>
9
+ JSON.stringify(Object.entries(answers).sort(([a], [b]) => a.localeCompare(b)));
10
+
11
+ /** One writer per form. Keep an unacknowledged request intact for lost-response retries. */
12
+ export class QuizDraftQueue {
13
+ revision: number;
14
+ state: DraftState = "saved";
15
+ private latest: Answers;
16
+ private acknowledged: string;
17
+ private retry?: DraftWrite;
18
+ private running?: Promise<void>;
19
+ private disposed = false;
20
+ constructor(
21
+ answers: Answers,
22
+ revision: number,
23
+ private send: (write: DraftWrite) => Promise<{ draftRevision: number }>,
24
+ private changed: (state: DraftState) => void,
25
+ ) {
26
+ this.latest = structuredClone(answers);
27
+ this.acknowledged = fingerprint(answers);
28
+ this.revision = revision;
29
+ }
30
+ get dirty() {
31
+ return !!this.retry || fingerprint(this.latest) !== this.acknowledged;
32
+ }
33
+ private emit(state: DraftState) {
34
+ this.state = state;
35
+ if (!this.disposed) this.changed(state);
36
+ }
37
+ update(answers: Answers) {
38
+ this.latest = structuredClone(answers);
39
+ if (!["failed", "conflict", "saving"].includes(this.state))
40
+ this.emit(this.dirty ? "pending" : "saved");
41
+ }
42
+ flush(): Promise<void> {
43
+ if (this.disposed) return Promise.reject(new Error("This quiz form has closed."));
44
+ if (this.state === "conflict")
45
+ return Promise.reject(new Error("Review the saved answers before continuing."));
46
+ if (this.running) return this.running;
47
+ this.running = this.drain().finally(() => {
48
+ this.running = undefined;
49
+ });
50
+ return this.running;
51
+ }
52
+ private async drain() {
53
+ while (this.dirty && !this.disposed) {
54
+ const write = this.retry ?? {
55
+ answers: structuredClone(this.latest),
56
+ expectedDraftRevision: this.revision,
57
+ };
58
+ this.retry = write;
59
+ this.emit("saving");
60
+ try {
61
+ const result = await this.send(structuredClone(write));
62
+ this.revision = result.draftRevision;
63
+ this.acknowledged = fingerprint(write.answers);
64
+ this.retry = undefined;
65
+ } catch (error) {
66
+ this.emit((error as { code?: string })?.code === "DRAFT_CONFLICT" ? "conflict" : "failed");
67
+ throw error;
68
+ }
69
+ }
70
+ this.emit(this.dirty ? "pending" : "saved");
71
+ }
72
+ /** Called only after the learner reviews the current remote draft. */
73
+ conflict() {
74
+ this.emit("conflict");
75
+ }
76
+ reconcile(answers: Answers, revision: number, keepLocal: boolean) {
77
+ if (this.running) throw new Error("Wait for the current save to finish.");
78
+ this.revision = revision;
79
+ this.acknowledged = fingerprint(answers);
80
+ this.retry = undefined;
81
+ if (!keepLocal) this.latest = structuredClone(answers);
82
+ this.emit(this.dirty ? "pending" : "saved");
83
+ }
84
+ dispose() {
85
+ this.disposed = true;
86
+ }
87
+ }
@@ -0,0 +1,362 @@
1
+ import type {
2
+ Answers,
3
+ ProgressSummary,
4
+ PublicEnrollment,
5
+ PublicQuizAttempt,
6
+ } from "@anantagnanam/lms/types";
7
+ import { QuizDraftQueue, type DraftState } from "./quiz-drafts.js";
8
+
9
+ type Call = <T>(route: string, input: Record<string, unknown>, method?: string) => Promise<T>;
10
+ type LessonState = { enrollment: PublicEnrollment; progress: ProgressSummary };
11
+ const mounted = new WeakMap<HTMLElement, () => void>();
12
+ function node<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, className?: string) {
13
+ const element = document.createElement(tag);
14
+ if (text !== undefined) element.textContent = text;
15
+ if (className) element.className = className;
16
+ return element;
17
+ }
18
+ function readAnswers(form: HTMLFormElement, attempt: PublicQuizAttempt): Answers {
19
+ const answers: Answers = {};
20
+ const values = new FormData(form);
21
+ for (const question of attempt.questions) {
22
+ const picked = values.getAll(question.id).map(String);
23
+ if (question.type === "multiple") answers[question.id] = picked;
24
+ else if (picked[0]) answers[question.id] = picked[0];
25
+ }
26
+ return answers;
27
+ }
28
+ function writeAnswers(form: HTMLFormElement, answers: Answers) {
29
+ for (const input of form.querySelectorAll<HTMLInputElement>("input[name]")) {
30
+ const value = answers[input.name];
31
+ if (input.type === "text") input.value = typeof value === "string" ? value : "";
32
+ else input.checked = Array.isArray(value) ? value.includes(input.value) : value === input.value;
33
+ }
34
+ }
35
+ function errorMessage(error: unknown) {
36
+ return error instanceof Error ? error.message : "Please try again.";
37
+ }
38
+
39
+ export function renderQuiz(root: HTMLElement, attempt: PublicQuizAttempt, call: Call) {
40
+ mounted.get(root)?.();
41
+ const content = root.querySelector<HTMLElement>("[data-lms-quiz-content]")!;
42
+ const feedback = (message: string, error = false) => {
43
+ if (!root.isConnected) return;
44
+ const target = root.querySelector<HTMLElement>("[data-lms-feedback]")!;
45
+ target.textContent = message;
46
+ target.dataset.tone = error ? "error" : "success";
47
+ target.setAttribute("role", error ? "alert" : "status");
48
+ };
49
+ const ids = {
50
+ courseId: root.dataset.courseId!,
51
+ lessonId: root.dataset.lessonId!,
52
+ attemptId: attempt.id,
53
+ };
54
+ const form = node("form");
55
+ const timer = node("p", "", "lms-quiz-timer");
56
+ timer.setAttribute("aria-live", "off");
57
+ form.append(timer);
58
+ for (const question of attempt.questions) {
59
+ const fieldset = node("fieldset", undefined, "lms-quiz-question");
60
+ fieldset.append(node("legend", question.prompt));
61
+ if (question.type === "short") {
62
+ const input = node("input");
63
+ input.name = question.id;
64
+ input.type = "text";
65
+ input.maxLength = 2000;
66
+ input.setAttribute("aria-label", "Your answer");
67
+ fieldset.append(input);
68
+ } else
69
+ for (const option of question.options) {
70
+ const label = node("label", undefined, "lms-quiz-option");
71
+ const input = node("input");
72
+ input.name = question.id;
73
+ input.type = question.type === "multiple" ? "checkbox" : "radio";
74
+ input.value = option.id;
75
+ label.append(input, node("span", option.text));
76
+ fieldset.append(label);
77
+ }
78
+ form.append(fieldset);
79
+ }
80
+ writeAnswers(form, attempt.answers);
81
+ const button = node("button", "Submit answers", "lms-btn");
82
+ button.type = "submit";
83
+ const status = node("p", "Saved answers loaded.", "lms-draft-status");
84
+ status.setAttribute("role", "status");
85
+ status.setAttribute("aria-live", "polite");
86
+ const retry = node("button", "Retry saving", "lms-btn lms-btn-outline");
87
+ retry.type = "button";
88
+ retry.hidden = true;
89
+ const review = node("button", "Review saved answers", "lms-btn lms-btn-outline");
90
+ review.type = "button";
91
+ review.hidden = true;
92
+ const recovery = node("div", undefined, "lms-notice");
93
+ recovery.hidden = true;
94
+ form.append(status, retry, review, recovery, button);
95
+ content.replaceChildren(form);
96
+ let interval: ReturnType<typeof setInterval> | undefined;
97
+ let debounce: ReturnType<typeof setTimeout> | undefined;
98
+ let submitting = false;
99
+ let uncertainSubmission = false;
100
+ let ended = false;
101
+ let timeoutSent = false;
102
+ const labels: Record<DraftState, string> = {
103
+ pending: "Unsaved answers · waiting to save…",
104
+ saving: "Saving answers…",
105
+ saved: "All answers saved.",
106
+ failed: "Answers could not be saved. Keep this page open and retry.",
107
+ conflict: "Saved answers changed in another tab. Review them before continuing.",
108
+ };
109
+ const queue = new QuizDraftQueue(
110
+ attempt.answers,
111
+ attempt.draftRevision,
112
+ (write) => call("learner/quiz/draft", { ...ids, ...write }),
113
+ (state) => {
114
+ status.textContent = labels[state];
115
+ status.dataset.tone = state === "failed" || state === "conflict" ? "error" : "success";
116
+ retry.hidden = state !== "failed";
117
+ review.hidden = state !== "conflict";
118
+ },
119
+ );
120
+ const atRisk = () =>
121
+ !ended && (queue.dirty || queue.state === "conflict" || submitting || uncertainSubmission);
122
+ const beforeUnload = (event: BeforeUnloadEvent) => {
123
+ if (atRisk()) event.preventDefault();
124
+ };
125
+ const beforeNavigate = (event: Event) => {
126
+ if (atRisk() && !window.confirm("Some quiz answers may not be saved. Leave this page?"))
127
+ event.preventDefault();
128
+ };
129
+ const cleanup = () => {
130
+ ended = true;
131
+ clearInterval(interval);
132
+ clearTimeout(debounce);
133
+ queue.dispose();
134
+ window.removeEventListener("beforeunload", beforeUnload);
135
+ document.removeEventListener("astro:before-preparation", beforeNavigate);
136
+ document.removeEventListener("astro:before-swap", cleanup);
137
+ observer.disconnect();
138
+ mounted.delete(root);
139
+ };
140
+ const observer = new MutationObserver(() => {
141
+ if (!form.isConnected) cleanup();
142
+ });
143
+ observer.observe(document.body, { childList: true, subtree: true });
144
+ window.addEventListener("beforeunload", beforeUnload);
145
+ document.addEventListener("astro:before-preparation", beforeNavigate);
146
+ document.addEventListener("astro:before-swap", cleanup, { once: true });
147
+ mounted.set(root, cleanup);
148
+
149
+ const finish = (final: PublicQuizAttempt, progress: ProgressSummary) => {
150
+ if (ended) return;
151
+ cleanup();
152
+ feedback(
153
+ final.status === "expired"
154
+ ? "The quiz time limit expired. Your saved attempt is retained."
155
+ : `${final.passed ? "Passed" : "Not passed yet"} · ${final.percent}%`,
156
+ );
157
+ const outcome = node("div");
158
+ for (const item of final.feedback) {
159
+ const section = node("div", undefined, "lms-notice");
160
+ const question = attempt.questions.find((entry) => entry.id === item.questionId);
161
+ section.append(
162
+ node("strong", `${item.correct ? "✓" : "○"} ${question?.prompt ?? "Question"}`),
163
+ );
164
+ if (item.explanation) section.append(node("p", item.explanation));
165
+ outcome.append(section);
166
+ }
167
+ content.replaceChildren(outcome);
168
+ const start = root.querySelector<HTMLButtonElement>("[data-lms-quiz-start]");
169
+ if (start) {
170
+ start.hidden = final.passed;
171
+ start.disabled = false;
172
+ start.textContent = "Try another attempt";
173
+ }
174
+ if (final.passed) {
175
+ const courseUrl = root.dataset.courseUrl!;
176
+ const link = node(
177
+ "a",
178
+ progress.nextLessonId ? "Continue learning →" : "Course overview →",
179
+ "lms-btn",
180
+ );
181
+ link.href = progress.nextLessonId
182
+ ? `${courseUrl}/lessons/${encodeURIComponent(progress.nextLessonId)}`
183
+ : courseUrl;
184
+ if (progress.blockedLessonId) {
185
+ const blocked = progress.availability[progress.blockedLessonId];
186
+ content.append(
187
+ node(
188
+ "p",
189
+ `${blocked?.reason ?? "The next lesson is unavailable."}${blocked?.releaseAt ? ` Available from ${new Date(blocked.releaseAt).toLocaleString(undefined, { timeZoneName: "short" })}.` : ""}`,
190
+ "lms-notice",
191
+ ),
192
+ );
193
+ }
194
+ content.append(link);
195
+ }
196
+ };
197
+ const fetchAttempt = async (expiredByServer = false) => {
198
+ const state = await call<LessonState>("learner/lesson", ids, "GET");
199
+ const remote = state.enrollment.progress[ids.lessonId]?.quizAttempts.find(
200
+ (item) => item.id === attempt.id,
201
+ );
202
+ if (!remote)
203
+ throw new Error(
204
+ "This attempt could not be found. Keep this page open and contact an instructor.",
205
+ );
206
+ if (remote.status !== "in_progress" || expiredByServer) {
207
+ finish(
208
+ remote.status === "in_progress" ? { ...remote, status: "expired" } : remote,
209
+ state.progress,
210
+ );
211
+ return null;
212
+ }
213
+ return remote;
214
+ };
215
+ const save = async () => {
216
+ try {
217
+ await queue.flush();
218
+ if (!ended) feedback("Answers saved.");
219
+ } catch (error) {
220
+ if (ended) return;
221
+ feedback(errorMessage(error), true);
222
+ if (["QUIZ_EXPIRED", "ATTEMPT_CLOSED"].includes((error as { code?: string })?.code ?? "")) {
223
+ try {
224
+ await fetchAttempt((error as { code?: string }).code === "QUIZ_EXPIRED");
225
+ } catch {
226
+ /* The visible save error and retry remain. */
227
+ }
228
+ }
229
+ }
230
+ };
231
+ const showConflict = (remote: PublicQuizAttempt) => {
232
+ if (ended) return;
233
+ queue.conflict();
234
+ clearTimeout(debounce);
235
+ recovery.hidden = false;
236
+ recovery.replaceChildren(
237
+ node("h3", "Review the saved draft"),
238
+ node(
239
+ "p",
240
+ "Compare the server answers below with your answers in the form. Choose which set to keep. A newer change in another tab will require another review.",
241
+ ),
242
+ );
243
+ for (const question of attempt.questions) {
244
+ const value = remote.answers[question.id];
245
+ const values = Array.isArray(value) ? value : value ? [value] : [];
246
+ recovery.append(
247
+ node(
248
+ "p",
249
+ `${question.prompt}: ${values.map((item) => question.options.find((option) => option.id === item)?.text ?? item).join(", ") || "Unanswered"}`,
250
+ ),
251
+ );
252
+ }
253
+ const useSaved = node("button", "Use saved answers", "lms-btn lms-btn-outline");
254
+ useSaved.type = "button";
255
+ const keepLocal = node("button", "Keep my answers and save", "lms-btn");
256
+ keepLocal.type = "button";
257
+ const choose = (keep: boolean) => {
258
+ queue.update(readAnswers(form, attempt));
259
+ queue.reconcile(remote.answers, remote.draftRevision, keep);
260
+ if (!keep) writeAnswers(form, remote.answers);
261
+ recovery.hidden = true;
262
+ feedback(keep ? "Saving your chosen answers…" : "Saved answers loaded.");
263
+ if (keep) void save();
264
+ };
265
+ useSaved.addEventListener("click", () => choose(false));
266
+ keepLocal.addEventListener("click", () => choose(true));
267
+ recovery.append(useSaved, keepLocal);
268
+ };
269
+ review.addEventListener("click", async () => {
270
+ review.disabled = true;
271
+ feedback("Loading saved answers for review…");
272
+ try {
273
+ const remote = await fetchAttempt();
274
+ if (remote) showConflict(remote);
275
+ } catch (error) {
276
+ feedback(errorMessage(error), true);
277
+ } finally {
278
+ review.disabled = false;
279
+ }
280
+ });
281
+ retry.addEventListener("click", () => void save());
282
+ form.addEventListener("input", () => {
283
+ queue.update(readAnswers(form, attempt));
284
+ clearTimeout(debounce);
285
+ if (!submitting && !["failed", "conflict"].includes(queue.state))
286
+ debounce = setTimeout(() => {
287
+ if (!ended) void save();
288
+ }, 700);
289
+ });
290
+ form.addEventListener("submit", async (event) => {
291
+ event.preventDefault();
292
+ if (submitting || ended) return;
293
+ clearTimeout(debounce);
294
+ const answers = readAnswers(form, attempt);
295
+ queue.update(answers);
296
+ submitting = true;
297
+ button.disabled = true;
298
+ retry.disabled = true;
299
+ review.disabled = true;
300
+ recovery.hidden = true;
301
+ form.querySelectorAll("fieldset").forEach((fieldset) => {
302
+ fieldset.disabled = true;
303
+ });
304
+ feedback("Checking your answers…");
305
+ try {
306
+ await queue.flush();
307
+ if (ended) return;
308
+ uncertainSubmission = true;
309
+ const result = await call<{ attempt: PublicQuizAttempt; progress: ProgressSummary }>(
310
+ "learner/quiz/submit",
311
+ { ...ids, answers, expectedDraftRevision: queue.revision },
312
+ );
313
+ finish(result.attempt, result.progress);
314
+ } catch (error) {
315
+ if (ended) return;
316
+ feedback(`${errorMessage(error)} Checking the original attempt…`, true);
317
+ try {
318
+ const remote = await fetchAttempt((error as { code?: string })?.code === "QUIZ_EXPIRED");
319
+ if (remote && !ended) {
320
+ uncertainSubmission = false;
321
+ if (queue.state === "conflict" || remote.draftRevision !== queue.revision)
322
+ showConflict(remote);
323
+ feedback(
324
+ `${errorMessage(error)} Your answers remain in this form. Retry this attempt when ready.`,
325
+ true,
326
+ );
327
+ }
328
+ } catch {
329
+ feedback(
330
+ `${errorMessage(error)} Could not confirm the attempt status. Keep this page open and retry this same attempt.`,
331
+ true,
332
+ );
333
+ }
334
+ } finally {
335
+ submitting = false;
336
+ button.disabled = false;
337
+ retry.disabled = false;
338
+ review.disabled = false;
339
+ form.querySelectorAll("fieldset").forEach((fieldset) => {
340
+ fieldset.disabled = false;
341
+ });
342
+ }
343
+ });
344
+ if (attempt.expiresAt) {
345
+ const tick = () => {
346
+ if (ended) return;
347
+ const remaining = Math.max(
348
+ 0,
349
+ Math.ceil((Date.parse(attempt.expiresAt!) - Date.now()) / 1000),
350
+ );
351
+ timer.textContent = remaining
352
+ ? `Time remaining ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`
353
+ : "Time limit reached. Check the attempt result or retry to confirm its status.";
354
+ if (!remaining && !timeoutSent) {
355
+ timeoutSent = true;
356
+ form.requestSubmit();
357
+ }
358
+ };
359
+ interval = setInterval(tick, 1000);
360
+ tick();
361
+ } else timer.textContent = "Take your time. This quiz is untimed.";
362
+ }
package/astro/styles.css CHANGED
@@ -1286,3 +1286,15 @@
1286
1286
  display: none;
1287
1287
  }
1288
1288
  }
1289
+ .lms-root .lms-lock-note {
1290
+ display: block;
1291
+ margin-block: 0.4rem;
1292
+ color: #526357;
1293
+ font-size: 0.85rem;
1294
+ }
1295
+ .lms-root .lms-locked-lesson {
1296
+ padding: 0.7rem 0.9rem;
1297
+ }
1298
+ .lms-root .lms-draft-status[data-tone="error"] {
1299
+ color: #9d2837;
1300
+ }
package/dist/admin.d.ts CHANGED
@@ -6,6 +6,7 @@ declare function CoursesPage(): react.JSX.Element;
6
6
  declare function LearnersPage(): react.JSX.Element;
7
7
  declare function AssignmentsPage(): react.JSX.Element;
8
8
  declare function CommunityPage(): react.JSX.Element;
9
+ declare function PathsPage(): react.JSX.Element;
9
10
  declare function CohortsPage(): react.JSX.Element;
10
11
  declare function ReportsPage(): react.JSX.Element;
11
12
  declare function NotificationsPage(): react.JSX.Element;
@@ -19,6 +20,7 @@ declare const pages: {
19
20
  "/assignments": typeof AssignmentsPage;
20
21
  "/community": typeof CommunityPage;
21
22
  "/cohorts": typeof CohortsPage;
23
+ "/paths": typeof PathsPage;
22
24
  "/reports": typeof ReportsPage;
23
25
  "/notifications": typeof NotificationsPage;
24
26
  "/audit": typeof AuditPage;
@@ -28,5 +30,5 @@ declare const widgets: {
28
30
  learning: typeof LearningWidget;
29
31
  };
30
32
  //#endregion
31
- export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, ReportsPage, SettingsPage, pages, widgets };
33
+ export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, PathsPage, ReportsPage, SettingsPage, pages, widgets };
32
34
  //# sourceMappingURL=admin.d.ts.map