@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
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-Cnx7k82Z.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,7 @@ function ContentPicker({ value, onChange }) {
994
1117
  })
995
1118
  ] });
996
1119
  }
997
- function LessonEditor({ lesson, update, remove }) {
1120
+ function LessonEditor({ lesson, previewUrl, update, remove }) {
998
1121
  return /* @__PURE__ */ jsxs(Fragment, { children: [
999
1122
  /* @__PURE__ */ jsxs("div", {
1000
1123
  className: "lms-grid",
@@ -1061,7 +1184,58 @@ function LessonEditor({ lesson, update, remove }) {
1061
1184
  onChange: (event) => update({ summary: event.target.value })
1062
1185
  })
1063
1186
  }),
1064
- /* @__PURE__ */ jsx(Field, {
1187
+ /* @__PURE__ */ jsxs("section", {
1188
+ className: "lms-notice",
1189
+ children: [
1190
+ /* @__PURE__ */ jsxs("strong", { children: [
1191
+ "Content source:",
1192
+ " ",
1193
+ lesson.contentSource ? "Published CMS content" : lesson.portableText.length ? "Imported rich content" : "Inline text"
1194
+ ] }),
1195
+ previewUrl ? /* @__PURE__ */ jsxs("p", { children: [
1196
+ /* @__PURE__ */ jsx("a", {
1197
+ href: previewUrl,
1198
+ target: "_blank",
1199
+ rel: "noopener noreferrer",
1200
+ children: "Preview saved lesson ↗"
1201
+ }),
1202
+ " ",
1203
+ "· Save changes first to include them in this preview."
1204
+ ] }) : /* @__PURE__ */ jsx("p", { children: "Save this lesson to enable its preview." }),
1205
+ lesson.contentSource ? /* @__PURE__ */ jsxs(Fragment, { children: [
1206
+ /* @__PURE__ */ jsxs("p", { children: [
1207
+ "This lesson displays the published “",
1208
+ lesson.contentSource.field,
1209
+ "” field from",
1210
+ " ",
1211
+ lesson.contentSource.collection,
1212
+ ". Local content is retained while linked."
1213
+ ] }),
1214
+ /* @__PURE__ */ jsxs("p", { children: [
1215
+ /* @__PURE__ */ jsx("a", {
1216
+ href: `/_emdash/admin/content/${encodeURIComponent(lesson.contentSource.collection)}/${encodeURIComponent(lesson.contentSource.id)}`,
1217
+ target: "_blank",
1218
+ rel: "noopener noreferrer",
1219
+ children: "Edit linked entry in EmDash ↗"
1220
+ }),
1221
+ " ",
1222
+ "· Publish entry changes, then publish the saved course draft to create a new learner edition."
1223
+ ] }),
1224
+ /* @__PURE__ */ jsx(Button, {
1225
+ onClick: () => {
1226
+ if (window.confirm("Unlink the CMS entry and use this lesson's retained local content? Save the course to apply.")) update({ contentSource: null });
1227
+ },
1228
+ children: "Use retained local content"
1229
+ })
1230
+ ] }) : 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, {
1231
+ onClick: () => {
1232
+ 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: [] });
1233
+ },
1234
+ children: "Replace rich content with inline text"
1235
+ })] }) : /* @__PURE__ */ jsx("p", { children: "Edit the inline text below, or select a published CMS entry." })
1236
+ ]
1237
+ }),
1238
+ !lesson.contentSource && lesson.portableText.length === 0 && /* @__PURE__ */ jsx(Field, {
1065
1239
  label: "Lesson text",
1066
1240
  children: /* @__PURE__ */ jsx("textarea", {
1067
1241
  rows: 12,
@@ -1090,6 +1264,65 @@ function LessonEditor({ lesson, update, remove }) {
1090
1264
  onChange: (event) => update({ liveStartsAt: isoDate(event.target.value) })
1091
1265
  })
1092
1266
  }),
1267
+ ["video", "audio"].includes(lesson.kind) && /* @__PURE__ */ jsxs("section", { children: [
1268
+ /* @__PURE__ */ jsx(Field, {
1269
+ label: "Transcript",
1270
+ help: "Readable text shown alongside native and embedded media.",
1271
+ children: /* @__PURE__ */ jsx("textarea", {
1272
+ rows: 6,
1273
+ maxLength: 5e4,
1274
+ value: lesson.transcript ?? "",
1275
+ onChange: (event) => update({ transcript: event.target.value })
1276
+ })
1277
+ }),
1278
+ /* @__PURE__ */ jsx("h3", { children: "Caption tracks" }),
1279
+ /* @__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." }),
1280
+ (lesson.captions ?? []).map((track, index) => /* @__PURE__ */ jsxs("div", { children: [
1281
+ /* @__PURE__ */ jsx(Field, {
1282
+ label: `Track ${index + 1} URL`,
1283
+ children: /* @__PURE__ */ jsx("input", {
1284
+ value: track.src,
1285
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1286
+ ...item,
1287
+ src: event.target.value
1288
+ } : item) })
1289
+ })
1290
+ }),
1291
+ /* @__PURE__ */ jsx(Field, {
1292
+ label: "Language code",
1293
+ children: /* @__PURE__ */ jsx("input", {
1294
+ value: track.language,
1295
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1296
+ ...item,
1297
+ language: event.target.value
1298
+ } : item) })
1299
+ })
1300
+ }),
1301
+ /* @__PURE__ */ jsx(Field, {
1302
+ label: "Caption label",
1303
+ children: /* @__PURE__ */ jsx("input", {
1304
+ value: track.label,
1305
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1306
+ ...item,
1307
+ label: event.target.value
1308
+ } : item) })
1309
+ })
1310
+ }),
1311
+ /* @__PURE__ */ jsx(Button, {
1312
+ onClick: () => update({ captions: lesson.captions.filter((_, i) => i !== index) }),
1313
+ children: "Remove track"
1314
+ })
1315
+ ] }, index)),
1316
+ /* @__PURE__ */ jsx(Button, {
1317
+ disabled: (lesson.captions?.length ?? 0) >= 20,
1318
+ onClick: () => update({ captions: [...lesson.captions ?? [], {
1319
+ src: "",
1320
+ language: "en",
1321
+ label: "English"
1322
+ }] }),
1323
+ children: "Add captions"
1324
+ })
1325
+ ] }),
1093
1326
  /* @__PURE__ */ jsxs("div", {
1094
1327
  className: "lms-actions",
1095
1328
  children: [/* @__PURE__ */ jsxs("label", {
@@ -1114,7 +1347,9 @@ function LessonEditor({ lesson, update, remove }) {
1114
1347
  }),
1115
1348
  /* @__PURE__ */ jsx(ContentPicker, {
1116
1349
  value: lesson.contentSource,
1117
- onChange: (contentSource) => update({ contentSource })
1350
+ onChange: (contentSource) => {
1351
+ 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 });
1352
+ }
1118
1353
  }, lesson.id),
1119
1354
  lesson.kind === "quiz" && lesson.quiz && /* @__PURE__ */ jsx(QuizEditor, {
1120
1355
  quiz: lesson.quiz,
@@ -2199,6 +2434,165 @@ function CohortMembershipQueue() {
2199
2434
  ]
2200
2435
  });
2201
2436
  }
2437
+ function PathsPage() {
2438
+ const [cursor, setCursor] = useState();
2439
+ const list = useLoad("admin/paths", { cursor });
2440
+ const courses = useChoices("admin/courses");
2441
+ const [edit, setEdit] = useState();
2442
+ const action = useAction();
2443
+ return /* @__PURE__ */ jsxs(PageFrame, {
2444
+ title: "Learning paths",
2445
+ description: "Build ordered programs. Learners retain the course list they joined; individual course enrollment still controls access.",
2446
+ children: [
2447
+ /* @__PURE__ */ jsx(Notice, {
2448
+ error: list.error || courses.error || action.error,
2449
+ message: action.message
2450
+ }),
2451
+ !edit && /* @__PURE__ */ jsx(Button, {
2452
+ onClick: () => setEdit({
2453
+ revision: null,
2454
+ title: "New learning path",
2455
+ description: "",
2456
+ status: "draft",
2457
+ courseIds: []
2458
+ }),
2459
+ children: "Create learning path"
2460
+ }),
2461
+ edit && /* @__PURE__ */ jsx(Panel, {
2462
+ title: edit.id ? "Edit path" : "Create path",
2463
+ children: /* @__PURE__ */ jsxs("form", {
2464
+ onSubmit: (event) => {
2465
+ event.preventDefault();
2466
+ action.run(async () => {
2467
+ const { id, revision, ...data } = edit;
2468
+ await api("admin/path/save", {
2469
+ id,
2470
+ expectedRevision: revision,
2471
+ data
2472
+ }, "POST");
2473
+ setEdit(void 0);
2474
+ list.reload();
2475
+ }, "Path saved. Existing learners retain their joined course lists.");
2476
+ },
2477
+ children: [
2478
+ /* @__PURE__ */ jsx(Field, {
2479
+ label: "Title",
2480
+ children: /* @__PURE__ */ jsx("input", {
2481
+ required: true,
2482
+ maxLength: 200,
2483
+ value: edit.title,
2484
+ onChange: (event) => setEdit({
2485
+ ...edit,
2486
+ title: event.target.value
2487
+ })
2488
+ })
2489
+ }),
2490
+ /* @__PURE__ */ jsx(Field, {
2491
+ label: "Description",
2492
+ children: /* @__PURE__ */ jsx("textarea", {
2493
+ maxLength: 5e3,
2494
+ rows: 4,
2495
+ value: edit.description,
2496
+ onChange: (event) => setEdit({
2497
+ ...edit,
2498
+ description: event.target.value
2499
+ })
2500
+ })
2501
+ }),
2502
+ /* @__PURE__ */ jsx(Field, {
2503
+ label: "Visibility",
2504
+ children: /* @__PURE__ */ jsxs("select", {
2505
+ value: edit.status,
2506
+ onChange: (event) => setEdit({
2507
+ ...edit,
2508
+ status: event.target.value
2509
+ }),
2510
+ children: [
2511
+ /* @__PURE__ */ jsx("option", {
2512
+ value: "draft",
2513
+ children: "Draft"
2514
+ }),
2515
+ /* @__PURE__ */ jsx("option", {
2516
+ value: "published",
2517
+ children: "Published"
2518
+ }),
2519
+ /* @__PURE__ */ jsx("option", {
2520
+ value: "archived",
2521
+ children: "Archived"
2522
+ })
2523
+ ]
2524
+ })
2525
+ }),
2526
+ /* @__PURE__ */ jsx(ChoicesField, {
2527
+ label: "Courses",
2528
+ choices: courses.items.map((course) => ({
2529
+ id: course.id,
2530
+ label: `${course.title} (${course.status})`
2531
+ })),
2532
+ value: edit.courseIds,
2533
+ onChange: (courseIds) => setEdit({
2534
+ ...edit,
2535
+ courseIds
2536
+ })
2537
+ }),
2538
+ courses.data?.hasMore && /* @__PURE__ */ jsx(Button, {
2539
+ onClick: courses.more,
2540
+ children: "Load more courses"
2541
+ }),
2542
+ /* @__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, {
2543
+ disabled: index === 0,
2544
+ "aria-label": "Move course earlier",
2545
+ onClick: () => {
2546
+ const ids = [...edit.courseIds];
2547
+ [ids[index], ids[index - 1]] = [ids[index - 1], ids[index]];
2548
+ setEdit({
2549
+ ...edit,
2550
+ courseIds: ids
2551
+ });
2552
+ },
2553
+ children: "Move up"
2554
+ })] }, id)) }),
2555
+ /* @__PURE__ */ jsx(Button, {
2556
+ type: "submit",
2557
+ busy: action.busy,
2558
+ children: "Save path"
2559
+ }),
2560
+ /* @__PURE__ */ jsx(Button, {
2561
+ disabled: action.busy,
2562
+ onClick: () => setEdit(void 0),
2563
+ children: "Cancel"
2564
+ })
2565
+ ]
2566
+ })
2567
+ }),
2568
+ list.data?.items.map((path) => /* @__PURE__ */ jsxs(Panel, {
2569
+ title: path.title,
2570
+ children: [/* @__PURE__ */ jsxs("p", { children: [
2571
+ path.status,
2572
+ " · ",
2573
+ path.courseIds.length,
2574
+ " courses"
2575
+ ] }), /* @__PURE__ */ jsx(Button, {
2576
+ disabled: Boolean(edit),
2577
+ onClick: () => setEdit({
2578
+ id: path.id,
2579
+ revision: path.revision,
2580
+ title: path.title,
2581
+ description: path.description,
2582
+ courseIds: path.courseIds,
2583
+ status: path.status
2584
+ }),
2585
+ children: "Edit path"
2586
+ })]
2587
+ }, path.id)),
2588
+ /* @__PURE__ */ jsx(Pager, {
2589
+ page: list.data,
2590
+ next: setCursor,
2591
+ reset: () => setCursor(void 0)
2592
+ })
2593
+ ]
2594
+ });
2595
+ }
2202
2596
  function CohortsPage() {
2203
2597
  const [cursor, setCursor] = useState();
2204
2598
  const list = useLoad("admin/cohorts", { cursor });
@@ -2606,6 +3000,75 @@ function AuditPage() {
2606
3000
  ]
2607
3001
  });
2608
3002
  }
3003
+ function MigrationPanel() {
3004
+ const load = useLoad("admin/migrations");
3005
+ const action = useAction();
3006
+ return /* @__PURE__ */ jsxs(Panel, { children: [
3007
+ /* @__PURE__ */ jsx("h2", { children: "Course edition migration" }),
3008
+ /* @__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." }),
3009
+ /* @__PURE__ */ jsx(Notice, { error: action.error || load.error || load.data?.lastError || void 0 }),
3010
+ load.data && /* @__PURE__ */ jsxs("p", { children: [
3011
+ "Phase: ",
3012
+ load.data.phase,
3013
+ " · Courses processed: ",
3014
+ load.data.coursesProcessed,
3015
+ " · Enrollments processed: ",
3016
+ load.data.enrollmentsProcessed
3017
+ ] }),
3018
+ /* @__PURE__ */ jsx(Button, {
3019
+ busy: action.busy,
3020
+ disabled: load.loading || load.data?.phase === "complete",
3021
+ onClick: () => void action.run(async () => {
3022
+ try {
3023
+ await api("admin/migrations/run", {}, "POST");
3024
+ } finally {
3025
+ load.reload();
3026
+ }
3027
+ }, "Migration checkpoint saved."),
3028
+ children: "Run next batch"
3029
+ }),
3030
+ /* @__PURE__ */ jsx(Button, {
3031
+ onClick: load.reload,
3032
+ children: "Refresh status"
3033
+ }),
3034
+ /* @__PURE__ */ jsx(Notice, { message: action.message })
3035
+ ] });
3036
+ }
3037
+ function HealthPanel() {
3038
+ const load = useLoad("admin/health");
3039
+ return /* @__PURE__ */ jsxs(Panel, {
3040
+ title: "Operational health",
3041
+ children: [
3042
+ /* @__PURE__ */ jsx(Notice, { error: load.error }),
3043
+ load.data && /* @__PURE__ */ jsxs(Fragment, { children: [
3044
+ /* @__PURE__ */ jsxs("p", { children: [
3045
+ "Plugin ",
3046
+ load.data.version,
3047
+ " · Email ",
3048
+ load.data.emailEnabled ? "enabled" : "disabled"
3049
+ ] }),
3050
+ /* @__PURE__ */ jsx("p", {
3051
+ role: load.data.maintenanceStale ? "alert" : void 0,
3052
+ 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."
3053
+ }),
3054
+ /* @__PURE__ */ jsxs("p", { children: ["Last completed: ", load.data.maintenance?.completedAt ?? "No successful run reported"] }),
3055
+ /* @__PURE__ */ jsxs("p", { children: [
3056
+ "Pending emails: ",
3057
+ load.data.pendingEmails,
3058
+ " · Failed emails: ",
3059
+ load.data.failedEmails,
3060
+ " · Oldest pending: ",
3061
+ load.data.oldestPendingAt ?? "None"
3062
+ ] }),
3063
+ /* @__PURE__ */ jsxs("p", { children: ["Migration phase: ", load.data.migration?.phase ?? "Not started"] })
3064
+ ] }),
3065
+ /* @__PURE__ */ jsx(Button, {
3066
+ onClick: load.reload,
3067
+ children: "Refresh health"
3068
+ })
3069
+ ]
3070
+ });
3071
+ }
2609
3072
  function SettingsPage() {
2610
3073
  const load = useLoad("admin/settings");
2611
3074
  const [settings, setSettings] = useState(settingsSchema.parse({}));
@@ -2623,97 +3086,102 @@ function SettingsPage() {
2623
3086
  return /* @__PURE__ */ jsxs(PageFrame, {
2624
3087
  title: "LMS settings",
2625
3088
  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
3089
+ children: [
3090
+ /* @__PURE__ */ jsx(Notice, {
3091
+ error: load.error || action.error,
3092
+ message: action.message
3093
+ }),
3094
+ /* @__PURE__ */ jsx(MigrationPanel, {}),
3095
+ /* @__PURE__ */ jsx(HealthPanel, {}),
3096
+ /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsxs("form", {
3097
+ onSubmit: save,
3098
+ children: [
3099
+ /* @__PURE__ */ jsx(Field, {
3100
+ label: "Academy title",
3101
+ children: /* @__PURE__ */ jsx("input", {
3102
+ value: settings.academyTitle,
3103
+ onChange: (event) => setSettings({
3104
+ ...settings,
3105
+ academyTitle: event.target.value
3106
+ })
2639
3107
  })
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
3108
+ }),
3109
+ /* @__PURE__ */ jsx(Field, {
3110
+ label: "Academy introduction",
3111
+ children: /* @__PURE__ */ jsx("textarea", {
3112
+ rows: 3,
3113
+ value: settings.academyDescription,
3114
+ onChange: (event) => setSettings({
3115
+ ...settings,
3116
+ academyDescription: event.target.value
3117
+ })
2650
3118
  })
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
3119
+ }),
3120
+ /* @__PURE__ */ jsx(Field, {
3121
+ label: "Certificate issuer",
3122
+ children: /* @__PURE__ */ jsx("input", {
3123
+ value: settings.issuerName,
3124
+ onChange: (event) => setSettings({
3125
+ ...settings,
3126
+ issuerName: event.target.value
3127
+ })
2660
3128
  })
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)
3129
+ }),
3130
+ /* @__PURE__ */ jsx(Field, {
3131
+ label: "Remind inactive learners after days",
3132
+ help: "0 disables reminders. Scheduled maintenance must be enabled in the host.",
3133
+ children: /* @__PURE__ */ jsx("input", {
3134
+ type: "number",
3135
+ min: 0,
3136
+ max: 90,
3137
+ value: settings.reminderDays,
3138
+ onChange: (event) => setSettings({
3139
+ ...settings,
3140
+ reminderDays: Number(event.target.value)
3141
+ })
2674
3142
  })
3143
+ }),
3144
+ /* @__PURE__ */ jsxs("label", {
3145
+ className: "lms-check",
3146
+ children: [/* @__PURE__ */ jsx("input", {
3147
+ type: "checkbox",
3148
+ checked: settings.moderateDiscussions,
3149
+ onChange: (event) => setSettings({
3150
+ ...settings,
3151
+ moderateDiscussions: event.target.checked
3152
+ })
3153
+ }), "Review discussions before publishing"]
3154
+ }),
3155
+ /* @__PURE__ */ jsxs("label", {
3156
+ className: "lms-check",
3157
+ children: [/* @__PURE__ */ jsx("input", {
3158
+ type: "checkbox",
3159
+ checked: settings.moderateReviews,
3160
+ onChange: (event) => setSettings({
3161
+ ...settings,
3162
+ moderateReviews: event.target.checked
3163
+ })
3164
+ }), "Review course reviews before publishing"]
3165
+ }),
3166
+ /* @__PURE__ */ jsxs("label", {
3167
+ className: "lms-check",
3168
+ children: [/* @__PURE__ */ jsx("input", {
3169
+ type: "checkbox",
3170
+ checked: settings.emailNotifications,
3171
+ onChange: (event) => setSettings({
3172
+ ...settings,
3173
+ emailNotifications: event.target.checked
3174
+ })
3175
+ }), "Send notifications through the site's EmDash email transport"]
3176
+ }),
3177
+ /* @__PURE__ */ jsx(Button, {
3178
+ type: "submit",
3179
+ busy: action.busy,
3180
+ children: "Save settings"
2675
3181
  })
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
- }) })]
3182
+ ]
3183
+ }) })
3184
+ ]
2717
3185
  });
2718
3186
  }
2719
3187
  function LearningWidget() {
@@ -2747,6 +3215,7 @@ const pages = {
2747
3215
  "/assignments": AssignmentsPage,
2748
3216
  "/community": CommunityPage,
2749
3217
  "/cohorts": CohortsPage,
3218
+ "/paths": PathsPage,
2750
3219
  "/reports": ReportsPage,
2751
3220
  "/notifications": NotificationsPage,
2752
3221
  "/audit": AuditPage,
@@ -2758,5 +3227,5 @@ const css = `
2758
3227
  .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
3228
 
2760
3229
  //#endregion
2761
- export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, ReportsPage, SettingsPage, pages, widgets };
3230
+ export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, PathsPage, ReportsPage, SettingsPage, pages, widgets };
2762
3231
  //# sourceMappingURL=admin.js.map