@anantagnanam/lms 0.1.0 → 0.3.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 (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +40 -2
  3. package/astro/assignment-player.ts +231 -0
  4. package/astro/client.ts +97 -152
  5. package/astro/components/AssignmentFeedback.astro +32 -0
  6. package/astro/components/AssignmentRubric.astro +27 -0
  7. package/astro/components/CourseOverview.astro +25 -3
  8. package/astro/components/LearnerDashboard.astro +9 -0
  9. package/astro/components/LearningShell.astro +16 -1
  10. package/astro/components/LessonAvailabilityNote.astro +26 -0
  11. package/astro/components/LessonContent.astro +67 -0
  12. package/astro/components/LessonPlayer.astro +166 -99
  13. package/astro/components/MediaPlayer.astro +55 -4
  14. package/astro/draft-queue.ts +95 -0
  15. package/astro/media-player.ts +124 -0
  16. package/astro/pages/author-preview.astro +73 -0
  17. package/astro/pages/course.astro +10 -0
  18. package/astro/pages/lesson.astro +3 -0
  19. package/astro/pages/library.astro +173 -0
  20. package/astro/pages/paths.astro +73 -0
  21. package/astro/pages/study.astro +180 -0
  22. package/astro/quiz-drafts.ts +23 -0
  23. package/astro/quiz-player.ts +362 -0
  24. package/astro/study-tools.ts +386 -0
  25. package/astro/styles.css +30 -0
  26. package/astro/transcript-search.ts +56 -0
  27. package/dist/admin.d.ts +3 -1
  28. package/dist/admin.js +756 -127
  29. package/dist/admin.js.map +1 -1
  30. package/dist/assignments-CsuAjxGO.js +75 -0
  31. package/dist/assignments-CsuAjxGO.js.map +1 -0
  32. package/dist/astro.js +4 -0
  33. package/dist/astro.js.map +1 -1
  34. package/dist/{constants-C0kT94tc.js → constants-DyQbwNbP.js} +25 -3
  35. package/dist/constants-DyQbwNbP.js.map +1 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/runtime.d.ts +129 -16
  39. package/dist/runtime.js +1201 -130
  40. package/dist/runtime.js.map +1 -1
  41. package/dist/schemas.d.ts +136 -3
  42. package/dist/schemas.js +80 -7
  43. package/dist/schemas.js.map +1 -1
  44. package/dist/server.d.ts +18 -1
  45. package/dist/server.js +17 -3
  46. package/dist/server.js.map +1 -1
  47. package/dist/types.d.ts +172 -4
  48. package/package.json +1 -1
  49. package/dist/constants-C0kT94tc.js.map +0 -1
  50. package/dist/errors-ChzJS9al.js +0 -16
  51. package/dist/errors-ChzJS9al.js.map +0 -1
package/dist/admin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { r as PLUGIN_ID, t as API_BASE } from "./constants-C0kT94tc.js";
1
+ import { r as PLUGIN_ID, t as API_BASE } from "./constants-DyQbwNbP.js";
2
2
  import { assignmentSchema, courseDataSchema, lessonSchema, moduleSchema, questionSchema, quizSchema, settingsSchema } from "./schemas.js";
3
3
  import { n as enrollmentCsvLines, t as enrollmentCsvHeader } from "./csv-DKq_4OWU.js";
4
4
  import { useEffect, useState } from "react";
@@ -208,6 +208,7 @@ function PageFrame({ title, description, children }) {
208
208
  ["/assignments", "Assignments"],
209
209
  ["/community", "Community"],
210
210
  ["/cohorts", "Cohorts"],
211
+ ["/paths", "Learning paths"],
211
212
  ["/reports", "Reports"],
212
213
  ["/notifications", "Notifications"],
213
214
  ["/audit", "Audit history"],
@@ -443,8 +444,48 @@ function CoursesPage() {
443
444
  ]
444
445
  });
445
446
  }
447
+ function CourseHistory({ courseId, disabled, restore }) {
448
+ const [cursor, setCursor] = useState();
449
+ const load = useLoad("admin/course/history", {
450
+ id: courseId,
451
+ cursor
452
+ });
453
+ return /* @__PURE__ */ jsxs("details", {
454
+ className: "lms-panel",
455
+ children: [
456
+ /* @__PURE__ */ jsx("summary", { children: "Saved revision history" }),
457
+ /* @__PURE__ */ jsx(Notice, { error: load.error }),
458
+ /* @__PURE__ */ jsx("p", { children: "Restore a revision into a draft, then review and publish it. Save current edits before restoring." }),
459
+ load.data?.items.map((item) => /* @__PURE__ */ jsxs("div", {
460
+ className: "lms-toolbar",
461
+ children: [/* @__PURE__ */ jsxs("span", { children: [
462
+ new Date(item.createdAt).toLocaleString(),
463
+ " · ",
464
+ item.kind,
465
+ " · ",
466
+ item.title
467
+ ] }), /* @__PURE__ */ jsx(Button, {
468
+ disabled: disabled || load.loading,
469
+ onClick: () => restore(item.id),
470
+ children: "Restore as draft"
471
+ })]
472
+ }, item.id)),
473
+ !load.data?.items.length && /* @__PURE__ */ jsx("p", { children: "No saved history yet. Legacy courses gain history on their next save or migration." }),
474
+ cursor && /* @__PURE__ */ jsx(Button, {
475
+ onClick: () => setCursor(void 0),
476
+ children: "Latest revisions"
477
+ }),
478
+ load.data?.hasMore && /* @__PURE__ */ jsx(Button, {
479
+ disabled: load.loading,
480
+ onClick: () => setCursor(load.data?.cursor),
481
+ children: "Older revisions"
482
+ })
483
+ ]
484
+ });
485
+ }
446
486
  function CourseEditor({ initial, initialRevision, onClose }) {
447
487
  const [course, setCourse] = useState(initial);
488
+ const academy = useLoad("academy");
448
489
  const [revision, setRevision] = useState(initialRevision);
449
490
  const [saved, setSaved] = useState(JSON.stringify(initial));
450
491
  const [selected, setSelected] = useState(initial.modules[0]?.lessons[0]?.id ?? null);
@@ -483,21 +524,29 @@ function CourseEditor({ initial, initialRevision, onClose }) {
483
524
  }))
484
525
  }));
485
526
  const selectedLesson = course.modules.flatMap((module) => module.lessons).find((lesson) => lesson.id === selected);
486
- const save = (status = course.status) => void action.run(async () => {
527
+ const acceptSaved = (result) => {
528
+ setCourse(result.course);
529
+ setRevision(result.revision);
530
+ setSaved(JSON.stringify(result.course));
531
+ };
532
+ const publishOrArchive = (operation) => void action.run(async () => {
533
+ acceptSaved(await api(`admin/course/${operation}`, {
534
+ id: course.id,
535
+ expectedRevision: revision
536
+ }, "POST"));
537
+ }, operation === "publish" ? "Saved draft published. Existing learners keep their enrolled edition." : "Course archived.");
538
+ const save = () => void action.run(async () => {
487
539
  const parsed = courseDataSchema.safeParse({
488
540
  ...course,
489
- status
541
+ status: "draft"
490
542
  });
491
543
  if (!parsed.success) throw new Error(parsed.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" "));
492
- const result = await api("admin/course/save", {
544
+ acceptSaved(await api("admin/course/save", {
493
545
  id: course.id,
494
546
  expectedRevision: revision,
495
547
  data: parsed.data
496
- }, "POST");
497
- setCourse(result.course);
498
- setRevision(result.revision);
499
- setSaved(JSON.stringify(result.course));
500
- }, status === "published" ? "Course published." : "Course saved.");
548
+ }, "POST"));
549
+ }, "Draft saved. Publish it when it is ready for new learners.");
501
550
  const reorderLesson = (moduleIndex, index, delta) => {
502
551
  const items = [...course.modules[moduleIndex].lessons];
503
552
  const to = index + delta;
@@ -522,19 +571,37 @@ function CourseEditor({ initial, initialRevision, onClose }) {
522
571
  },
523
572
  children: "Back to courses"
524
573
  }),
525
- /* @__PURE__ */ jsxs(Button, {
574
+ /* @__PURE__ */ jsx(Button, {
526
575
  busy: action.busy,
527
576
  onClick: () => save(),
528
- children: ["Save ", course.status]
577
+ children: "Save draft"
529
578
  }),
530
- course.status !== "published" && /* @__PURE__ */ jsx(Button, {
579
+ course.id && /* @__PURE__ */ jsx(Button, {
531
580
  busy: action.busy,
532
- onClick: () => save("published"),
533
- children: "Publish"
581
+ disabled: dirty,
582
+ onClick: () => void action.run(async () => {
583
+ const exported = await api("admin/course/export", { id: course.id });
584
+ acceptSaved(await api("admin/course/import", {
585
+ ...exported,
586
+ course: {
587
+ ...exported.course,
588
+ title: `Copy of ${course.title}`.slice(0, 200),
589
+ slug: `${course.slug.slice(0, 65)}-copy-${crypto.randomUUID().slice(0, 8)}`
590
+ }
591
+ }, "POST"));
592
+ }, "Course duplicated as a new draft. Review its dates and access settings before publishing."),
593
+ children: "Duplicate as draft"
594
+ }),
595
+ /* @__PURE__ */ jsx(Button, {
596
+ busy: action.busy,
597
+ disabled: dirty,
598
+ onClick: () => publishOrArchive("publish"),
599
+ children: "Publish saved draft"
534
600
  }),
535
601
  course.status === "published" && /* @__PURE__ */ jsx(Button, {
536
602
  busy: action.busy,
537
- onClick: () => save("archived"),
603
+ disabled: dirty,
604
+ onClick: () => publishOrArchive("archive"),
538
605
  children: "Archive"
539
606
  }),
540
607
  course.id && course.status === "draft" && /* @__PURE__ */ jsx(Button, {
@@ -553,6 +620,29 @@ function CourseEditor({ initial, initialRevision, onClose }) {
553
620
  })
554
621
  ]
555
622
  }),
623
+ /* @__PURE__ */ jsxs("p", {
624
+ role: "status",
625
+ children: [
626
+ "Course visibility: ",
627
+ course.status,
628
+ ".",
629
+ dirty ? " Save changes before publishing." : course.publishedEditionId && course.draftRevisionId !== course.publishedDraftRevisionId ? " Saved draft changes are awaiting publication." : "",
630
+ course.publishedEditionId ? " Publishing creates a new edition for new enrollments; existing learners keep their course material and progress." : ""
631
+ ]
632
+ }),
633
+ course.id && /* @__PURE__ */ jsx(CourseHistory, {
634
+ courseId: course.id,
635
+ disabled: dirty || action.busy,
636
+ restore: (revisionId) => void action.run(async () => {
637
+ const result = await api("admin/course/restore", {
638
+ id: course.id,
639
+ expectedRevision: revision,
640
+ revisionId
641
+ }, "POST");
642
+ acceptSaved(result);
643
+ setSelected(result.course.modules[0]?.lessons[0]?.id ?? null);
644
+ }, "Revision restored into a saved draft. Review it before publishing.")
645
+ }, revision),
556
646
  /* @__PURE__ */ jsx("div", {
557
647
  className: "lms-tabs",
558
648
  role: "tablist",
@@ -887,10 +977,44 @@ function CourseEditor({ initial, initialRevision, onClose }) {
887
977
  children: "Add module"
888
978
  })
889
979
  ]
890
- }), /* @__PURE__ */ jsx(Panel, {
980
+ }), /* @__PURE__ */ jsxs(Panel, {
891
981
  title: selectedLesson?.title ?? "Choose a lesson",
892
- children: selectedLesson ? /* @__PURE__ */ jsx(LessonEditor, {
982
+ children: [selectedLesson && /* @__PURE__ */ jsxs("div", {
983
+ className: "lms-actions",
984
+ children: [/* @__PURE__ */ jsx(Button, {
985
+ onClick: () => {
986
+ const copy = {
987
+ ...structuredClone(selectedLesson),
988
+ id: crypto.randomUUID(),
989
+ title: `Copy of ${selectedLesson.title}`.slice(0, 200)
990
+ };
991
+ patch({ modules: course.modules.map((module) => ({
992
+ ...module,
993
+ lessons: module.lessons.flatMap((lesson) => lesson.id === selectedLesson.id ? [lesson, copy] : [lesson])
994
+ })) });
995
+ setSelected(copy.id);
996
+ },
997
+ children: "Duplicate lesson"
998
+ }), /* @__PURE__ */ jsx(Field, {
999
+ label: "Move lesson to module",
1000
+ children: /* @__PURE__ */ jsx("select", {
1001
+ value: course.modules.find((m) => m.lessons.some((l) => l.id === selectedLesson.id))?.id,
1002
+ onChange: (event) => {
1003
+ const target = event.target.value;
1004
+ patch({ modules: course.modules.map((module) => ({
1005
+ ...module,
1006
+ lessons: module.id === target ? [...module.lessons, selectedLesson] : module.lessons.filter((lesson) => lesson.id !== selectedLesson.id)
1007
+ })) });
1008
+ },
1009
+ children: course.modules.map((module) => /* @__PURE__ */ jsx("option", {
1010
+ value: module.id,
1011
+ children: module.title
1012
+ }, module.id))
1013
+ })
1014
+ })]
1015
+ }), selectedLesson ? /* @__PURE__ */ jsx(LessonEditor, {
893
1016
  lesson: selectedLesson,
1017
+ previewUrl: course.id && academy.data && JSON.parse(saved).modules.some((module) => module.lessons.some((lesson) => lesson.id === selectedLesson.id)) ? `${academy.data.basePath}/teach/courses/${encodeURIComponent(course.id)}/lessons/${encodeURIComponent(selectedLesson.id)}` : void 0,
894
1018
  update: (data) => updateLesson(selectedLesson.id, data),
895
1019
  remove: () => {
896
1020
  if (window.confirm(`Remove “${selectedLesson.title}” from the draft?`)) {
@@ -901,7 +1025,7 @@ function CourseEditor({ initial, initialRevision, onClose }) {
901
1025
  setSelected(null);
902
1026
  }
903
1027
  }
904
- }) : /* @__PURE__ */ jsx("p", { children: "Add a module and a lesson to begin." })
1028
+ }, selectedLesson.id) : /* @__PURE__ */ jsx("p", { children: "Add a module and a lesson to begin." })]
905
1029
  })]
906
1030
  })
907
1031
  ]
@@ -935,11 +1059,10 @@ function ContentPicker({ value, onChange }) {
935
1059
  setCollection(event.target.value);
936
1060
  setCursor(void 0);
937
1061
  setEntries([]);
938
- onChange(null);
939
1062
  },
940
1063
  children: [/* @__PURE__ */ jsx("option", {
941
1064
  value: "",
942
- children: "Use this lesson's own content"
1065
+ children: "Choose a collection"
943
1066
  }), schema.data?.map((entry) => /* @__PURE__ */ jsx("option", {
944
1067
  value: entry.slug,
945
1068
  children: entry.label
@@ -948,18 +1071,18 @@ function ContentPicker({ value, onChange }) {
948
1071
  }), collection && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Field, {
949
1072
  label: "Published entry",
950
1073
  children: /* @__PURE__ */ jsxs("select", {
951
- value: value?.id ?? "",
952
- onChange: (event) => onChange(event.target.value ? {
1074
+ value: value?.collection === collection ? value.id : "",
1075
+ onChange: (event) => event.target.value && onChange(event.target.value ? {
953
1076
  collection,
954
1077
  id: event.target.value,
955
- field: value?.field ?? fields.find((field) => field.type === "portableText")?.slug ?? fields[0]?.slug ?? "content"
1078
+ field: (value?.collection === collection ? value.field : void 0) ?? fields.find((field) => field.type === "portableText")?.slug ?? fields[0]?.slug ?? "content"
956
1079
  } : null),
957
1080
  children: [
958
1081
  /* @__PURE__ */ jsx("option", {
959
1082
  value: "",
960
1083
  children: "Choose an entry"
961
1084
  }),
962
- value && !entries.some((entry) => entry.id === value.id) && /* @__PURE__ */ jsx("option", {
1085
+ value?.collection === collection && !entries.some((entry) => entry.id === value.id) && /* @__PURE__ */ jsx("option", {
963
1086
  value: value.id,
964
1087
  children: "Current linked entry"
965
1088
  }),
@@ -972,8 +1095,8 @@ function ContentPicker({ value, onChange }) {
972
1095
  }), /* @__PURE__ */ jsx(Field, {
973
1096
  label: "Content field",
974
1097
  children: /* @__PURE__ */ jsxs("select", {
975
- value: value?.field ?? "",
976
- disabled: !value,
1098
+ value: value?.collection === collection ? value.field : "",
1099
+ disabled: !value || value.collection !== collection,
977
1100
  onChange: (event) => value && onChange({
978
1101
  ...value,
979
1102
  field: event.target.value
@@ -994,7 +1117,84 @@ function ContentPicker({ value, onChange }) {
994
1117
  })
995
1118
  ] });
996
1119
  }
997
- function LessonEditor({ lesson, update, remove }) {
1120
+ function RubricEditor({ assignment, update }) {
1121
+ const rubric = assignment.rubric ?? [];
1122
+ const change = (criteria, removed = false) => {
1123
+ const maxPoints = criteria.length ? criteria.reduce((sum, item) => sum + item.maxPoints, 0) : assignment.maxPoints;
1124
+ update({
1125
+ ...assignment,
1126
+ rubric: criteria,
1127
+ maxPoints,
1128
+ passingPoints: removed ? Math.min(assignment.passingPoints, maxPoints) : assignment.passingPoints
1129
+ });
1130
+ };
1131
+ return /* @__PURE__ */ jsxs("section", { children: [
1132
+ /* @__PURE__ */ jsx("h3", { children: "Grading rubric" }),
1133
+ /* @__PURE__ */ jsx("p", { children: "Describe what learners should demonstrate. Criterion points add up to the assignment maximum. Submitted work keeps its original rubric." }),
1134
+ rubric.map((criterion, index) => /* @__PURE__ */ jsxs("fieldset", {
1135
+ className: "lms-resource",
1136
+ children: [
1137
+ /* @__PURE__ */ jsxs("legend", { children: ["Criterion ", index + 1] }),
1138
+ /* @__PURE__ */ jsx(Field, {
1139
+ label: "Criterion title",
1140
+ children: /* @__PURE__ */ jsx("input", {
1141
+ required: true,
1142
+ maxLength: 200,
1143
+ value: criterion.title,
1144
+ onChange: (event) => change(rubric.map((item) => item.id === criterion.id ? {
1145
+ ...item,
1146
+ title: event.target.value
1147
+ } : item))
1148
+ })
1149
+ }),
1150
+ /* @__PURE__ */ jsx(Field, {
1151
+ label: "What earns these points",
1152
+ children: /* @__PURE__ */ jsx("textarea", {
1153
+ rows: 3,
1154
+ maxLength: 2e3,
1155
+ value: criterion.description,
1156
+ onChange: (event) => change(rubric.map((item) => item.id === criterion.id ? {
1157
+ ...item,
1158
+ description: event.target.value
1159
+ } : item))
1160
+ })
1161
+ }),
1162
+ /* @__PURE__ */ jsx(Field, {
1163
+ label: "Maximum criterion points",
1164
+ children: /* @__PURE__ */ jsx("input", {
1165
+ type: "number",
1166
+ required: true,
1167
+ min: 1,
1168
+ max: 1e3 - (assignment.maxPoints - criterion.maxPoints),
1169
+ step: 1,
1170
+ value: criterion.maxPoints,
1171
+ onChange: (event) => change(rubric.map((item) => item.id === criterion.id ? {
1172
+ ...item,
1173
+ maxPoints: Number(event.target.value)
1174
+ } : item))
1175
+ })
1176
+ }),
1177
+ /* @__PURE__ */ jsxs(Button, {
1178
+ danger: true,
1179
+ onClick: () => change(rubric.filter((item) => item.id !== criterion.id), true),
1180
+ children: ["Remove criterion ", index + 1]
1181
+ })
1182
+ ]
1183
+ }, criterion.id)),
1184
+ /* @__PURE__ */ jsx(Button, {
1185
+ disabled: rubric.length >= 20 || rubric.length > 0 && assignment.maxPoints >= 1e3,
1186
+ onClick: () => change([...rubric, {
1187
+ id: crypto.randomUUID(),
1188
+ title: "New criterion",
1189
+ description: "",
1190
+ maxPoints: rubric.length ? Math.min(10, 1e3 - assignment.maxPoints) : assignment.maxPoints
1191
+ }]),
1192
+ children: rubric.length ? "Add criterion" : "Add grading rubric"
1193
+ }),
1194
+ /* @__PURE__ */ jsx("p", { children: rubric.length ? `${rubric.length} ${rubric.length === 1 ? "criterion" : "criteria"} · ${assignment.maxPoints} points total (maximum 1000). Review the passing points when changing scores.` : "Without a rubric, graders enter a single overall score." })
1195
+ ] });
1196
+ }
1197
+ function LessonEditor({ lesson, previewUrl, update, remove }) {
998
1198
  return /* @__PURE__ */ jsxs(Fragment, { children: [
999
1199
  /* @__PURE__ */ jsxs("div", {
1000
1200
  className: "lms-grid",
@@ -1061,7 +1261,58 @@ function LessonEditor({ lesson, update, remove }) {
1061
1261
  onChange: (event) => update({ summary: event.target.value })
1062
1262
  })
1063
1263
  }),
1064
- /* @__PURE__ */ jsx(Field, {
1264
+ /* @__PURE__ */ jsxs("section", {
1265
+ className: "lms-notice",
1266
+ children: [
1267
+ /* @__PURE__ */ jsxs("strong", { children: [
1268
+ "Content source:",
1269
+ " ",
1270
+ lesson.contentSource ? "Published CMS content" : lesson.portableText.length ? "Imported rich content" : "Inline text"
1271
+ ] }),
1272
+ previewUrl ? /* @__PURE__ */ jsxs("p", { children: [
1273
+ /* @__PURE__ */ jsx("a", {
1274
+ href: previewUrl,
1275
+ target: "_blank",
1276
+ rel: "noopener noreferrer",
1277
+ children: "Preview saved lesson ↗"
1278
+ }),
1279
+ " ",
1280
+ "· Save changes first to include them in this preview."
1281
+ ] }) : /* @__PURE__ */ jsx("p", { children: "Save this lesson to enable its preview." }),
1282
+ lesson.contentSource ? /* @__PURE__ */ jsxs(Fragment, { children: [
1283
+ /* @__PURE__ */ jsxs("p", { children: [
1284
+ "This lesson displays the published “",
1285
+ lesson.contentSource.field,
1286
+ "” field from",
1287
+ " ",
1288
+ lesson.contentSource.collection,
1289
+ ". Local content is retained while linked."
1290
+ ] }),
1291
+ /* @__PURE__ */ jsxs("p", { children: [
1292
+ /* @__PURE__ */ jsx("a", {
1293
+ href: `/_emdash/admin/content/${encodeURIComponent(lesson.contentSource.collection)}/${encodeURIComponent(lesson.contentSource.id)}`,
1294
+ target: "_blank",
1295
+ rel: "noopener noreferrer",
1296
+ children: "Edit linked entry in EmDash ↗"
1297
+ }),
1298
+ " ",
1299
+ "· Publish entry changes, then publish the saved course draft to create a new learner edition."
1300
+ ] }),
1301
+ /* @__PURE__ */ jsx(Button, {
1302
+ onClick: () => {
1303
+ if (window.confirm("Unlink the CMS entry and use this lesson's retained local content? Save the course to apply.")) update({ contentSource: null });
1304
+ },
1305
+ children: "Use retained local content"
1306
+ })
1307
+ ] }) : lesson.portableText.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: "Imported rich content is read-only here. Use the saved preview to inspect it, or link a published CMS entry below to edit rich content in EmDash." }), /* @__PURE__ */ jsx(Button, {
1308
+ onClick: () => {
1309
+ if (window.confirm("Remove this lesson's imported rich content and use its retained plain text instead? This removes formatting when you save.")) update({ portableText: [] });
1310
+ },
1311
+ children: "Replace rich content with inline text"
1312
+ })] }) : /* @__PURE__ */ jsx("p", { children: "Edit the inline text below, or select a published CMS entry." })
1313
+ ]
1314
+ }),
1315
+ !lesson.contentSource && lesson.portableText.length === 0 && /* @__PURE__ */ jsx(Field, {
1065
1316
  label: "Lesson text",
1066
1317
  children: /* @__PURE__ */ jsx("textarea", {
1067
1318
  rows: 12,
@@ -1090,6 +1341,65 @@ function LessonEditor({ lesson, update, remove }) {
1090
1341
  onChange: (event) => update({ liveStartsAt: isoDate(event.target.value) })
1091
1342
  })
1092
1343
  }),
1344
+ ["video", "audio"].includes(lesson.kind) && /* @__PURE__ */ jsxs("section", { children: [
1345
+ /* @__PURE__ */ jsx(Field, {
1346
+ label: "Transcript",
1347
+ help: "Readable text shown alongside native and embedded media.",
1348
+ children: /* @__PURE__ */ jsx("textarea", {
1349
+ rows: 6,
1350
+ maxLength: 5e4,
1351
+ value: lesson.transcript ?? "",
1352
+ onChange: (event) => update({ transcript: event.target.value })
1353
+ })
1354
+ }),
1355
+ /* @__PURE__ */ jsx("h3", { children: "Caption tracks" }),
1356
+ /* @__PURE__ */ jsx("p", { children: "WebVTT tracks apply to native media. Use same-origin files or configure CORS on both media and caption hosts. Embedded players manage their own captions." }),
1357
+ (lesson.captions ?? []).map((track, index) => /* @__PURE__ */ jsxs("div", { children: [
1358
+ /* @__PURE__ */ jsx(Field, {
1359
+ label: `Track ${index + 1} URL`,
1360
+ children: /* @__PURE__ */ jsx("input", {
1361
+ value: track.src,
1362
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1363
+ ...item,
1364
+ src: event.target.value
1365
+ } : item) })
1366
+ })
1367
+ }),
1368
+ /* @__PURE__ */ jsx(Field, {
1369
+ label: "Language code",
1370
+ children: /* @__PURE__ */ jsx("input", {
1371
+ value: track.language,
1372
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1373
+ ...item,
1374
+ language: event.target.value
1375
+ } : item) })
1376
+ })
1377
+ }),
1378
+ /* @__PURE__ */ jsx(Field, {
1379
+ label: "Caption label",
1380
+ children: /* @__PURE__ */ jsx("input", {
1381
+ value: track.label,
1382
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1383
+ ...item,
1384
+ label: event.target.value
1385
+ } : item) })
1386
+ })
1387
+ }),
1388
+ /* @__PURE__ */ jsx(Button, {
1389
+ onClick: () => update({ captions: lesson.captions.filter((_, i) => i !== index) }),
1390
+ children: "Remove track"
1391
+ })
1392
+ ] }, index)),
1393
+ /* @__PURE__ */ jsx(Button, {
1394
+ disabled: (lesson.captions?.length ?? 0) >= 20,
1395
+ onClick: () => update({ captions: [...lesson.captions ?? [], {
1396
+ src: "",
1397
+ language: "en",
1398
+ label: "English"
1399
+ }] }),
1400
+ children: "Add captions"
1401
+ })
1402
+ ] }),
1093
1403
  /* @__PURE__ */ jsxs("div", {
1094
1404
  className: "lms-actions",
1095
1405
  children: [/* @__PURE__ */ jsxs("label", {
@@ -1114,7 +1424,9 @@ function LessonEditor({ lesson, update, remove }) {
1114
1424
  }),
1115
1425
  /* @__PURE__ */ jsx(ContentPicker, {
1116
1426
  value: lesson.contentSource,
1117
- onChange: (contentSource) => update({ contentSource })
1427
+ onChange: (contentSource) => {
1428
+ if (contentSource && window.confirm("Use this published CMS field as the lesson content? Local content will be retained. Save the course to apply.")) update({ contentSource });
1429
+ }
1118
1430
  }, lesson.id),
1119
1431
  lesson.kind === "quiz" && lesson.quiz && /* @__PURE__ */ jsx(QuizEditor, {
1120
1432
  quiz: lesson.quiz,
@@ -1142,6 +1454,8 @@ function LessonEditor({ lesson, update, remove }) {
1142
1454
  children: /* @__PURE__ */ jsx("input", {
1143
1455
  type: "number",
1144
1456
  min: 1,
1457
+ max: 1e3,
1458
+ readOnly: Boolean(lesson.assignment.rubric?.length),
1145
1459
  value: lesson.assignment.maxPoints,
1146
1460
  onChange: (event) => update({ assignment: {
1147
1461
  ...lesson.assignment,
@@ -1154,6 +1468,7 @@ function LessonEditor({ lesson, update, remove }) {
1154
1468
  children: /* @__PURE__ */ jsx("input", {
1155
1469
  type: "number",
1156
1470
  min: 1,
1471
+ max: lesson.assignment.maxPoints,
1157
1472
  value: lesson.assignment.passingPoints,
1158
1473
  onChange: (event) => update({ assignment: {
1159
1474
  ...lesson.assignment,
@@ -1184,6 +1499,10 @@ function LessonEditor({ lesson, update, remove }) {
1184
1499
  allowResubmission: event.target.checked
1185
1500
  } })
1186
1501
  }), "Allow another submission after a failing grade"]
1502
+ }),
1503
+ /* @__PURE__ */ jsx(RubricEditor, {
1504
+ assignment: lesson.assignment,
1505
+ update: (assignment) => update({ assignment })
1187
1506
  })
1188
1507
  ]
1189
1508
  }),
@@ -1821,7 +2140,9 @@ function AssignmentsPage() {
1821
2140
  }, !!courseId);
1822
2141
  const [selected, setSelected] = useState();
1823
2142
  const [points, setPoints] = useState(0);
2143
+ const [rubricScores, setRubricScores] = useState([]);
1824
2144
  const [feedback, setFeedback] = useState("");
2145
+ const rubricTotal = rubricScores.reduce((sum, item) => sum + item.points, 0);
1825
2146
  const action = useAction();
1826
2147
  const grade = (event) => {
1827
2148
  event.preventDefault();
@@ -1832,7 +2153,7 @@ function AssignmentsPage() {
1832
2153
  lessonId: selected.lessonId,
1833
2154
  userId: selected.userId,
1834
2155
  submissionId: selected.id,
1835
- points,
2156
+ ...selected.rubric?.length ? { rubricScores } : { points },
1836
2157
  feedback
1837
2158
  }, "POST");
1838
2159
  setSelected(void 0);
@@ -1872,7 +2193,59 @@ function AssignmentsPage() {
1872
2193
  /* @__PURE__ */ jsxs("form", {
1873
2194
  onSubmit: grade,
1874
2195
  children: [
1875
- /* @__PURE__ */ jsx(Field, {
2196
+ selected.rubric?.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
2197
+ /* @__PURE__ */ jsx("p", { children: "Score each criterion using the rubric saved with this submission." }),
2198
+ selected.rubric.map((criterion) => {
2199
+ const score = rubricScores.find((item) => item.criterionId === criterion.id);
2200
+ const updateScore = (values) => setRubricScores((items) => items.map((item) => item.criterionId === criterion.id ? {
2201
+ ...item,
2202
+ ...values
2203
+ } : item));
2204
+ return /* @__PURE__ */ jsxs("fieldset", {
2205
+ className: "lms-resource",
2206
+ children: [
2207
+ /* @__PURE__ */ jsx("legend", { children: criterion.title }),
2208
+ /* @__PURE__ */ jsx("p", {
2209
+ className: "lms-body",
2210
+ children: criterion.description
2211
+ }),
2212
+ /* @__PURE__ */ jsx(Field, {
2213
+ label: `Points for ${criterion.title} (out of ${criterion.maxPoints})`,
2214
+ children: /* @__PURE__ */ jsx("input", {
2215
+ type: "number",
2216
+ required: true,
2217
+ min: 0,
2218
+ max: criterion.maxPoints,
2219
+ step: 1,
2220
+ value: score.points,
2221
+ onChange: (event) => updateScore({ points: Number(event.target.value) })
2222
+ })
2223
+ }),
2224
+ /* @__PURE__ */ jsx(Field, {
2225
+ label: `Feedback for ${criterion.title}`,
2226
+ children: /* @__PURE__ */ jsx("textarea", {
2227
+ rows: 3,
2228
+ maxLength: 2e3,
2229
+ value: score.feedback,
2230
+ onChange: (event) => updateScore({ feedback: event.target.value })
2231
+ })
2232
+ })
2233
+ ]
2234
+ }, criterion.id);
2235
+ }),
2236
+ /* @__PURE__ */ jsxs("p", {
2237
+ role: "status",
2238
+ children: [
2239
+ "Total: ",
2240
+ rubricTotal,
2241
+ "/",
2242
+ selected.maxPoints,
2243
+ " · Passing grade:",
2244
+ " ",
2245
+ selected.passingPoints
2246
+ ]
2247
+ })
2248
+ ] }) : /* @__PURE__ */ jsx(Field, {
1876
2249
  label: `Points out of ${selected.maxPoints}`,
1877
2250
  help: `Passing grade: ${selected.passingPoints}`,
1878
2251
  children: /* @__PURE__ */ jsx("input", {
@@ -1888,6 +2261,7 @@ function AssignmentsPage() {
1888
2261
  label: "Feedback",
1889
2262
  children: /* @__PURE__ */ jsx("textarea", {
1890
2263
  rows: 6,
2264
+ maxLength: 1e4,
1891
2265
  value: feedback,
1892
2266
  onChange: (event) => setFeedback(event.target.value)
1893
2267
  })
@@ -1929,18 +2303,39 @@ function AssignmentsPage() {
1929
2303
  rel: "noopener noreferrer",
1930
2304
  children: "Open attachment ↗"
1931
2305
  }) }),
1932
- item.status === "graded" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("p", { children: [
1933
- item.points,
1934
- "/",
1935
- item.maxPoints,
1936
- " points"
1937
- ] }), /* @__PURE__ */ jsx("p", {
1938
- className: "lms-body",
1939
- children: item.feedback
1940
- })] }) : /* @__PURE__ */ jsx(Button, {
2306
+ item.status === "graded" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2307
+ /* @__PURE__ */ jsxs("p", { children: [
2308
+ item.points,
2309
+ "/",
2310
+ item.maxPoints,
2311
+ " points"
2312
+ ] }),
2313
+ /* @__PURE__ */ jsx("p", {
2314
+ className: "lms-body",
2315
+ children: item.feedback
2316
+ }),
2317
+ (item.rubric ?? []).map((criterion) => {
2318
+ const score = item.rubricScores?.find((entry) => entry.criterionId === criterion.id);
2319
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("strong", { children: [
2320
+ criterion.title,
2321
+ ": ",
2322
+ score?.points ?? "—",
2323
+ "/",
2324
+ criterion.maxPoints
2325
+ ] }), score?.feedback && /* @__PURE__ */ jsx("p", {
2326
+ className: "lms-body",
2327
+ children: score.feedback
2328
+ })] }, criterion.id);
2329
+ })
2330
+ ] }) : /* @__PURE__ */ jsx(Button, {
1941
2331
  onClick: () => {
1942
2332
  setSelected(item);
1943
2333
  setPoints(0);
2334
+ setRubricScores((item.rubric ?? []).map((criterion) => ({
2335
+ criterionId: criterion.id,
2336
+ points: 0,
2337
+ feedback: ""
2338
+ })));
1944
2339
  setFeedback("");
1945
2340
  },
1946
2341
  children: "Review & grade"
@@ -2199,6 +2594,165 @@ function CohortMembershipQueue() {
2199
2594
  ]
2200
2595
  });
2201
2596
  }
2597
+ function PathsPage() {
2598
+ const [cursor, setCursor] = useState();
2599
+ const list = useLoad("admin/paths", { cursor });
2600
+ const courses = useChoices("admin/courses");
2601
+ const [edit, setEdit] = useState();
2602
+ const action = useAction();
2603
+ return /* @__PURE__ */ jsxs(PageFrame, {
2604
+ title: "Learning paths",
2605
+ description: "Build ordered programs. Learners retain the course list they joined; individual course enrollment still controls access.",
2606
+ children: [
2607
+ /* @__PURE__ */ jsx(Notice, {
2608
+ error: list.error || courses.error || action.error,
2609
+ message: action.message
2610
+ }),
2611
+ !edit && /* @__PURE__ */ jsx(Button, {
2612
+ onClick: () => setEdit({
2613
+ revision: null,
2614
+ title: "New learning path",
2615
+ description: "",
2616
+ status: "draft",
2617
+ courseIds: []
2618
+ }),
2619
+ children: "Create learning path"
2620
+ }),
2621
+ edit && /* @__PURE__ */ jsx(Panel, {
2622
+ title: edit.id ? "Edit path" : "Create path",
2623
+ children: /* @__PURE__ */ jsxs("form", {
2624
+ onSubmit: (event) => {
2625
+ event.preventDefault();
2626
+ action.run(async () => {
2627
+ const { id, revision, ...data } = edit;
2628
+ await api("admin/path/save", {
2629
+ id,
2630
+ expectedRevision: revision,
2631
+ data
2632
+ }, "POST");
2633
+ setEdit(void 0);
2634
+ list.reload();
2635
+ }, "Path saved. Existing learners retain their joined course lists.");
2636
+ },
2637
+ children: [
2638
+ /* @__PURE__ */ jsx(Field, {
2639
+ label: "Title",
2640
+ children: /* @__PURE__ */ jsx("input", {
2641
+ required: true,
2642
+ maxLength: 200,
2643
+ value: edit.title,
2644
+ onChange: (event) => setEdit({
2645
+ ...edit,
2646
+ title: event.target.value
2647
+ })
2648
+ })
2649
+ }),
2650
+ /* @__PURE__ */ jsx(Field, {
2651
+ label: "Description",
2652
+ children: /* @__PURE__ */ jsx("textarea", {
2653
+ maxLength: 5e3,
2654
+ rows: 4,
2655
+ value: edit.description,
2656
+ onChange: (event) => setEdit({
2657
+ ...edit,
2658
+ description: event.target.value
2659
+ })
2660
+ })
2661
+ }),
2662
+ /* @__PURE__ */ jsx(Field, {
2663
+ label: "Visibility",
2664
+ children: /* @__PURE__ */ jsxs("select", {
2665
+ value: edit.status,
2666
+ onChange: (event) => setEdit({
2667
+ ...edit,
2668
+ status: event.target.value
2669
+ }),
2670
+ children: [
2671
+ /* @__PURE__ */ jsx("option", {
2672
+ value: "draft",
2673
+ children: "Draft"
2674
+ }),
2675
+ /* @__PURE__ */ jsx("option", {
2676
+ value: "published",
2677
+ children: "Published"
2678
+ }),
2679
+ /* @__PURE__ */ jsx("option", {
2680
+ value: "archived",
2681
+ children: "Archived"
2682
+ })
2683
+ ]
2684
+ })
2685
+ }),
2686
+ /* @__PURE__ */ jsx(ChoicesField, {
2687
+ label: "Courses",
2688
+ choices: courses.items.map((course) => ({
2689
+ id: course.id,
2690
+ label: `${course.title} (${course.status})`
2691
+ })),
2692
+ value: edit.courseIds,
2693
+ onChange: (courseIds) => setEdit({
2694
+ ...edit,
2695
+ courseIds
2696
+ })
2697
+ }),
2698
+ courses.data?.hasMore && /* @__PURE__ */ jsx(Button, {
2699
+ onClick: courses.more,
2700
+ children: "Load more courses"
2701
+ }),
2702
+ /* @__PURE__ */ jsx("ol", { children: edit.courseIds.map((id, index) => /* @__PURE__ */ jsxs("li", { children: [courses.items.find((course) => course.id === id)?.title ?? id, /* @__PURE__ */ jsx(Button, {
2703
+ disabled: index === 0,
2704
+ "aria-label": "Move course earlier",
2705
+ onClick: () => {
2706
+ const ids = [...edit.courseIds];
2707
+ [ids[index], ids[index - 1]] = [ids[index - 1], ids[index]];
2708
+ setEdit({
2709
+ ...edit,
2710
+ courseIds: ids
2711
+ });
2712
+ },
2713
+ children: "Move up"
2714
+ })] }, id)) }),
2715
+ /* @__PURE__ */ jsx(Button, {
2716
+ type: "submit",
2717
+ busy: action.busy,
2718
+ children: "Save path"
2719
+ }),
2720
+ /* @__PURE__ */ jsx(Button, {
2721
+ disabled: action.busy,
2722
+ onClick: () => setEdit(void 0),
2723
+ children: "Cancel"
2724
+ })
2725
+ ]
2726
+ })
2727
+ }),
2728
+ list.data?.items.map((path) => /* @__PURE__ */ jsxs(Panel, {
2729
+ title: path.title,
2730
+ children: [/* @__PURE__ */ jsxs("p", { children: [
2731
+ path.status,
2732
+ " · ",
2733
+ path.courseIds.length,
2734
+ " courses"
2735
+ ] }), /* @__PURE__ */ jsx(Button, {
2736
+ disabled: Boolean(edit),
2737
+ onClick: () => setEdit({
2738
+ id: path.id,
2739
+ revision: path.revision,
2740
+ title: path.title,
2741
+ description: path.description,
2742
+ courseIds: path.courseIds,
2743
+ status: path.status
2744
+ }),
2745
+ children: "Edit path"
2746
+ })]
2747
+ }, path.id)),
2748
+ /* @__PURE__ */ jsx(Pager, {
2749
+ page: list.data,
2750
+ next: setCursor,
2751
+ reset: () => setCursor(void 0)
2752
+ })
2753
+ ]
2754
+ });
2755
+ }
2202
2756
  function CohortsPage() {
2203
2757
  const [cursor, setCursor] = useState();
2204
2758
  const list = useLoad("admin/cohorts", { cursor });
@@ -2606,6 +3160,75 @@ function AuditPage() {
2606
3160
  ]
2607
3161
  });
2608
3162
  }
3163
+ function MigrationPanel() {
3164
+ const load = useLoad("admin/migrations");
3165
+ const action = useAction();
3166
+ return /* @__PURE__ */ jsxs(Panel, { children: [
3167
+ /* @__PURE__ */ jsx("h2", { children: "Course edition migration" }),
3168
+ /* @__PURE__ */ jsx("p", { children: "Upgrade existing courses and pin enrollments to their original curriculum. Progress, assessments, grants and certificates are retained. Each batch handles up to 20 records and can be resumed after interruption." }),
3169
+ /* @__PURE__ */ jsx(Notice, { error: action.error || load.error || load.data?.lastError || void 0 }),
3170
+ load.data && /* @__PURE__ */ jsxs("p", { children: [
3171
+ "Phase: ",
3172
+ load.data.phase,
3173
+ " · Courses processed: ",
3174
+ load.data.coursesProcessed,
3175
+ " · Enrollments processed: ",
3176
+ load.data.enrollmentsProcessed
3177
+ ] }),
3178
+ /* @__PURE__ */ jsx(Button, {
3179
+ busy: action.busy,
3180
+ disabled: load.loading || load.data?.phase === "complete",
3181
+ onClick: () => void action.run(async () => {
3182
+ try {
3183
+ await api("admin/migrations/run", {}, "POST");
3184
+ } finally {
3185
+ load.reload();
3186
+ }
3187
+ }, "Migration checkpoint saved."),
3188
+ children: "Run next batch"
3189
+ }),
3190
+ /* @__PURE__ */ jsx(Button, {
3191
+ onClick: load.reload,
3192
+ children: "Refresh status"
3193
+ }),
3194
+ /* @__PURE__ */ jsx(Notice, { message: action.message })
3195
+ ] });
3196
+ }
3197
+ function HealthPanel() {
3198
+ const load = useLoad("admin/health");
3199
+ return /* @__PURE__ */ jsxs(Panel, {
3200
+ title: "Operational health",
3201
+ children: [
3202
+ /* @__PURE__ */ jsx(Notice, { error: load.error }),
3203
+ load.data && /* @__PURE__ */ jsxs(Fragment, { children: [
3204
+ /* @__PURE__ */ jsxs("p", { children: [
3205
+ "Plugin ",
3206
+ load.data.version,
3207
+ " · Email ",
3208
+ load.data.emailEnabled ? "enabled" : "disabled"
3209
+ ] }),
3210
+ /* @__PURE__ */ jsx("p", {
3211
+ role: load.data.maintenanceStale ? "alert" : void 0,
3212
+ children: load.data.maintenanceStale ? "Maintenance has not completed successfully in the last 45 minutes. Check the host scheduler and logs." : "Scheduled maintenance is reporting successfully."
3213
+ }),
3214
+ /* @__PURE__ */ jsxs("p", { children: ["Last completed: ", load.data.maintenance?.completedAt ?? "No successful run reported"] }),
3215
+ /* @__PURE__ */ jsxs("p", { children: [
3216
+ "Pending emails: ",
3217
+ load.data.pendingEmails,
3218
+ " · Failed emails: ",
3219
+ load.data.failedEmails,
3220
+ " · Oldest pending: ",
3221
+ load.data.oldestPendingAt ?? "None"
3222
+ ] }),
3223
+ /* @__PURE__ */ jsxs("p", { children: ["Migration phase: ", load.data.migration?.phase ?? "Not started"] })
3224
+ ] }),
3225
+ /* @__PURE__ */ jsx(Button, {
3226
+ onClick: load.reload,
3227
+ children: "Refresh health"
3228
+ })
3229
+ ]
3230
+ });
3231
+ }
2609
3232
  function SettingsPage() {
2610
3233
  const load = useLoad("admin/settings");
2611
3234
  const [settings, setSettings] = useState(settingsSchema.parse({}));
@@ -2623,97 +3246,102 @@ function SettingsPage() {
2623
3246
  return /* @__PURE__ */ jsxs(PageFrame, {
2624
3247
  title: "LMS settings",
2625
3248
  description: "Configure your academy, certificates, moderation, and reminders.",
2626
- children: [/* @__PURE__ */ jsx(Notice, {
2627
- error: load.error || action.error,
2628
- message: action.message
2629
- }), /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsxs("form", {
2630
- onSubmit: save,
2631
- children: [
2632
- /* @__PURE__ */ jsx(Field, {
2633
- label: "Academy title",
2634
- children: /* @__PURE__ */ jsx("input", {
2635
- value: settings.academyTitle,
2636
- onChange: (event) => setSettings({
2637
- ...settings,
2638
- academyTitle: event.target.value
3249
+ children: [
3250
+ /* @__PURE__ */ jsx(Notice, {
3251
+ error: load.error || action.error,
3252
+ message: action.message
3253
+ }),
3254
+ /* @__PURE__ */ jsx(MigrationPanel, {}),
3255
+ /* @__PURE__ */ jsx(HealthPanel, {}),
3256
+ /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsxs("form", {
3257
+ onSubmit: save,
3258
+ children: [
3259
+ /* @__PURE__ */ jsx(Field, {
3260
+ label: "Academy title",
3261
+ children: /* @__PURE__ */ jsx("input", {
3262
+ value: settings.academyTitle,
3263
+ onChange: (event) => setSettings({
3264
+ ...settings,
3265
+ academyTitle: event.target.value
3266
+ })
2639
3267
  })
2640
- })
2641
- }),
2642
- /* @__PURE__ */ jsx(Field, {
2643
- label: "Academy introduction",
2644
- children: /* @__PURE__ */ jsx("textarea", {
2645
- rows: 3,
2646
- value: settings.academyDescription,
2647
- onChange: (event) => setSettings({
2648
- ...settings,
2649
- academyDescription: event.target.value
3268
+ }),
3269
+ /* @__PURE__ */ jsx(Field, {
3270
+ label: "Academy introduction",
3271
+ children: /* @__PURE__ */ jsx("textarea", {
3272
+ rows: 3,
3273
+ value: settings.academyDescription,
3274
+ onChange: (event) => setSettings({
3275
+ ...settings,
3276
+ academyDescription: event.target.value
3277
+ })
2650
3278
  })
2651
- })
2652
- }),
2653
- /* @__PURE__ */ jsx(Field, {
2654
- label: "Certificate issuer",
2655
- children: /* @__PURE__ */ jsx("input", {
2656
- value: settings.issuerName,
2657
- onChange: (event) => setSettings({
2658
- ...settings,
2659
- issuerName: event.target.value
3279
+ }),
3280
+ /* @__PURE__ */ jsx(Field, {
3281
+ label: "Certificate issuer",
3282
+ children: /* @__PURE__ */ jsx("input", {
3283
+ value: settings.issuerName,
3284
+ onChange: (event) => setSettings({
3285
+ ...settings,
3286
+ issuerName: event.target.value
3287
+ })
2660
3288
  })
2661
- })
2662
- }),
2663
- /* @__PURE__ */ jsx(Field, {
2664
- label: "Remind inactive learners after days",
2665
- help: "0 disables reminders. Scheduled maintenance must be enabled in the host.",
2666
- children: /* @__PURE__ */ jsx("input", {
2667
- type: "number",
2668
- min: 0,
2669
- max: 90,
2670
- value: settings.reminderDays,
2671
- onChange: (event) => setSettings({
2672
- ...settings,
2673
- reminderDays: Number(event.target.value)
3289
+ }),
3290
+ /* @__PURE__ */ jsx(Field, {
3291
+ label: "Remind inactive learners after days",
3292
+ help: "0 disables reminders. Scheduled maintenance must be enabled in the host.",
3293
+ children: /* @__PURE__ */ jsx("input", {
3294
+ type: "number",
3295
+ min: 0,
3296
+ max: 90,
3297
+ value: settings.reminderDays,
3298
+ onChange: (event) => setSettings({
3299
+ ...settings,
3300
+ reminderDays: Number(event.target.value)
3301
+ })
2674
3302
  })
3303
+ }),
3304
+ /* @__PURE__ */ jsxs("label", {
3305
+ className: "lms-check",
3306
+ children: [/* @__PURE__ */ jsx("input", {
3307
+ type: "checkbox",
3308
+ checked: settings.moderateDiscussions,
3309
+ onChange: (event) => setSettings({
3310
+ ...settings,
3311
+ moderateDiscussions: event.target.checked
3312
+ })
3313
+ }), "Review discussions before publishing"]
3314
+ }),
3315
+ /* @__PURE__ */ jsxs("label", {
3316
+ className: "lms-check",
3317
+ children: [/* @__PURE__ */ jsx("input", {
3318
+ type: "checkbox",
3319
+ checked: settings.moderateReviews,
3320
+ onChange: (event) => setSettings({
3321
+ ...settings,
3322
+ moderateReviews: event.target.checked
3323
+ })
3324
+ }), "Review course reviews before publishing"]
3325
+ }),
3326
+ /* @__PURE__ */ jsxs("label", {
3327
+ className: "lms-check",
3328
+ children: [/* @__PURE__ */ jsx("input", {
3329
+ type: "checkbox",
3330
+ checked: settings.emailNotifications,
3331
+ onChange: (event) => setSettings({
3332
+ ...settings,
3333
+ emailNotifications: event.target.checked
3334
+ })
3335
+ }), "Send notifications through the site's EmDash email transport"]
3336
+ }),
3337
+ /* @__PURE__ */ jsx(Button, {
3338
+ type: "submit",
3339
+ busy: action.busy,
3340
+ children: "Save settings"
2675
3341
  })
2676
- }),
2677
- /* @__PURE__ */ jsxs("label", {
2678
- className: "lms-check",
2679
- children: [/* @__PURE__ */ jsx("input", {
2680
- type: "checkbox",
2681
- checked: settings.moderateDiscussions,
2682
- onChange: (event) => setSettings({
2683
- ...settings,
2684
- moderateDiscussions: event.target.checked
2685
- })
2686
- }), "Review discussions before publishing"]
2687
- }),
2688
- /* @__PURE__ */ jsxs("label", {
2689
- className: "lms-check",
2690
- children: [/* @__PURE__ */ jsx("input", {
2691
- type: "checkbox",
2692
- checked: settings.moderateReviews,
2693
- onChange: (event) => setSettings({
2694
- ...settings,
2695
- moderateReviews: event.target.checked
2696
- })
2697
- }), "Review course reviews before publishing"]
2698
- }),
2699
- /* @__PURE__ */ jsxs("label", {
2700
- className: "lms-check",
2701
- children: [/* @__PURE__ */ jsx("input", {
2702
- type: "checkbox",
2703
- checked: settings.emailNotifications,
2704
- onChange: (event) => setSettings({
2705
- ...settings,
2706
- emailNotifications: event.target.checked
2707
- })
2708
- }), "Send notifications through the site's EmDash email transport"]
2709
- }),
2710
- /* @__PURE__ */ jsx(Button, {
2711
- type: "submit",
2712
- busy: action.busy,
2713
- children: "Save settings"
2714
- })
2715
- ]
2716
- }) })]
3342
+ ]
3343
+ }) })
3344
+ ]
2717
3345
  });
2718
3346
  }
2719
3347
  function LearningWidget() {
@@ -2747,6 +3375,7 @@ const pages = {
2747
3375
  "/assignments": AssignmentsPage,
2748
3376
  "/community": CommunityPage,
2749
3377
  "/cohorts": CohortsPage,
3378
+ "/paths": PathsPage,
2750
3379
  "/reports": ReportsPage,
2751
3380
  "/notifications": NotificationsPage,
2752
3381
  "/audit": AuditPage,
@@ -2758,5 +3387,5 @@ const css = `
2758
3387
  .ag-lms-admin *{box-sizing:border-box}.ag-lms-admin h1{font-size:clamp(1.6rem,3vw,2.15rem);margin:0}.ag-lms-admin h2{font-size:1.15rem;margin-block:0 .8rem}.ag-lms-admin h3{font-size:1rem}.ag-lms-admin p{margin-block:.5rem 1rem}.lms-page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-block-end:1.5rem}.lms-page-header>div>p{max-width:65ch;color:var(--text-color-kumo-subtle,#64748b)}.lms-eyebrow{font-size:.7rem;letter-spacing:.15em;font-weight:700}.lms-tabs{display:flex;gap:.3rem;flex-wrap:wrap;margin-block:1rem 1.5rem}.lms-tabs a{padding:.4rem .7rem;border-radius:.4rem;text-decoration:none;color:inherit;background:var(--color-kumo-control,#f1f5f9)}.lms-panel{background:var(--color-kumo-base,#fff);border:1px solid var(--color-kumo-line,#d1d5db);border-radius:.65rem;padding:1.3rem;margin-block-end:1rem;min-width:0}.lms-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.lms-course-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:1rem}.lms-course-grid .lms-panel{display:flex;flex-direction:column;align-items:flex-start}.lms-course-grid .lms-actions{margin-block-start:auto}.lms-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem}.lms-metrics strong{display:block;font-size:2rem;margin-block-start:.5rem}.lms-metrics span,.lms-muted{font-size:.85rem;color:var(--text-color-kumo-subtle,#64748b)}.lms-actions,.lms-toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-block:.6rem 1rem}.lms-toolbar>.lms-field{min-width:160px;margin:0}.lms-button{display:inline-flex;justify-content:center;align-items:center;gap:.4rem;min-height:38px;padding:.5rem .8rem;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;background:var(--color-kumo-control,#f8fafc);color:var(--text-color-kumo-default,#1e293b);font:inherit;font-size:.875rem;font-weight:600;text-decoration:none;cursor:pointer}.lms-button:hover{background:var(--color-kumo-interact,#e2e8f0)}.lms-button:disabled{opacity:.55;cursor:default}.lms-danger{color:var(--text-color-kumo-danger,#b91c1c)}.lms-field{display:grid;gap:.35rem;margin-block-end:1rem;min-width:0;font-size:.875rem;font-weight:600}.lms-field small{font-weight:400;color:var(--text-color-kumo-subtle,#64748b)}.ag-lms-admin input:not([type=checkbox]):not([type=radio]),.ag-lms-admin textarea,.ag-lms-admin select{width:100%;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.35rem;background:var(--color-kumo-control,#fff);color:inherit;padding:.6rem;font:inherit;font-size:.875rem;font-weight:400}.ag-lms-admin input[type=checkbox],.ag-lms-admin input[type=radio]{width:1rem;height:1rem;accent-color:var(--color-kumo-brand,#2563eb)}.ag-lms-admin :focus-visible{outline:2px solid var(--color-kumo-focus,#2563eb);outline-offset:3px}.lms-check{display:flex;gap:.6rem;align-items:center;font-size:.9rem;margin-block:.7rem}.lms-notice{padding:.8rem 1rem;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;background:var(--color-kumo-tint,#f1f5f9)}.lms-error{color:var(--text-color-kumo-danger,#b91c1c);border-color:currentColor}.lms-badge{font-size:.7rem;text-transform:uppercase;font-weight:700;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:1rem;padding:.2rem .6rem;margin-block-end:.6rem}.lms-studio{display:grid;grid-template-columns:330px minmax(0,1fr);gap:1.3rem;align-items:start}.lms-module{border-block-end:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-lesson-list{list-style:none;padding:0;margin:0}.lms-lesson-list li{display:flex;align-items:center;gap:.3rem}.lms-select-lesson{background:transparent;color:inherit;border:0;border-radius:.35rem;cursor:pointer;text-align:start;padding:.6rem;flex:1;font:inherit;font-size:.9rem}.lms-select-lesson[aria-current=true]{background:var(--color-kumo-control,#eff6ff);font-weight:600}.lms-select-lesson small{display:block;font-weight:400;opacity:.75}.lms-lesson-list .lms-actions{margin:0}.lms-option{display:flex;align-items:center;gap:.5rem;margin-block:.5rem}.lms-question{border-block-start:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-question fieldset{border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;margin-block:1rem;padding:1rem}.lms-resource{border-block-end:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-file{position:absolute;width:1px!important;height:1px;opacity:0}.lms-table-wrap{overflow:auto}.lms-table{width:100%;border-collapse:collapse;font-size:.875rem}.lms-table th,.lms-table td{padding:.7rem;text-align:start;border-block-end:1px solid var(--color-kumo-line,#e5e7eb);vertical-align:top}.lms-table th{font-weight:600}.lms-table small{display:block;color:var(--text-color-kumo-subtle,#64748b)}.lms-table .lms-actions{margin:0}.lms-body{white-space:pre-wrap;overflow-wrap:anywhere}.ag-lms-admin details{padding:.7rem;border:1px solid var(--color-kumo-line,#d1d5db);border-radius:.4rem;margin-block:1rem}.ag-lms-admin summary{cursor:pointer;font-weight:600}.lms-notice:empty{display:none}@media(max-width:900px){.lms-studio{grid-template-columns:1fr}.lms-page-header{align-items:start;flex-direction:column}}@media(max-width:600px){.lms-grid{grid-template-columns:1fr}.lms-toolbar{align-items:stretch}.lms-toolbar>.lms-field{width:100%}.lms-panel{padding:1rem}}`;
2759
3388
 
2760
3389
  //#endregion
2761
- export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, ReportsPage, SettingsPage, pages, widgets };
3390
+ export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, PathsPage, ReportsPage, SettingsPage, pages, widgets };
2762
3391
  //# sourceMappingURL=admin.js.map