@hydralms/components 0.1.3 → 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 (344) hide show
  1. package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
  2. package/dist/StudentProfile-DeMxdrL3.js +3275 -0
  3. package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
  4. package/dist/assessment-toolbar/index.d.ts +5 -1
  5. package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
  6. package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
  7. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  8. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  9. package/dist/assessment-toolbar/types.d.ts +52 -4
  10. package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
  11. package/dist/common/index.d.ts +3 -1
  12. package/dist/common/pagination.d.ts +26 -0
  13. package/dist/common/stepper.d.ts +6 -0
  14. package/dist/common/types.d.ts +38 -0
  15. package/dist/components.css +1 -1
  16. package/dist/content/attachment-list.d.ts +6 -0
  17. package/dist/content/audio-player.d.ts +22 -0
  18. package/dist/content/code-block.d.ts +30 -0
  19. package/dist/content/content-block.d.ts +1 -1
  20. package/dist/content/embed-block.d.ts +28 -0
  21. package/dist/content/index.d.ts +8 -1
  22. package/dist/content/types.d.ts +63 -0
  23. package/dist/curriculum/course-card.d.ts +51 -0
  24. package/dist/curriculum/curriculum-item.d.ts +1 -1
  25. package/dist/curriculum/index.d.ts +2 -0
  26. package/dist/curriculum/types.d.ts +2 -2
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +597 -308
  30. package/dist/license/HydraContext.d.ts +16 -0
  31. package/dist/license/ProBadge.d.ts +6 -0
  32. package/dist/license/index.d.ts +7 -0
  33. package/dist/license/tiers.d.ts +3 -0
  34. package/dist/license/useHydraLicense.d.ts +6 -0
  35. package/dist/license/validate.d.ts +13 -0
  36. package/dist/license/withProGate.d.ts +6 -0
  37. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
  38. package/dist/modules/AssignmentModule/types.d.ts +69 -0
  39. package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
  40. package/dist/modules/CertificateModule/types.d.ts +51 -0
  41. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  42. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  43. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  44. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
  45. package/dist/modules/DiscussionModule/types.d.ts +47 -0
  46. package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
  47. package/dist/modules/ExamModule/types.d.ts +55 -0
  48. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  49. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  50. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
  51. package/dist/modules/GradeCenterModule/types.d.ts +56 -0
  52. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  53. package/dist/modules/QuizModule/types.d.ts +10 -14
  54. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  55. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  56. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  57. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  58. package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
  59. package/dist/modules/SurveyModule/types.d.ts +51 -0
  60. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  61. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  62. package/dist/modules/_shared/types.d.ts +10 -0
  63. package/dist/modules/_shared/use-timer.d.ts +9 -0
  64. package/dist/modules/index.d.ts +18 -0
  65. package/dist/modules.cjs +1 -0
  66. package/dist/modules.js +1834 -0
  67. package/dist/progress/achievement-badge.d.ts +6 -0
  68. package/dist/progress/activity-timeline.d.ts +6 -0
  69. package/dist/progress/index.d.ts +4 -1
  70. package/dist/progress/stat-card.d.ts +1 -1
  71. package/dist/progress/streak-badge.d.ts +6 -0
  72. package/dist/progress/types.d.ts +99 -0
  73. package/dist/provider/HydraProvider.d.ts +5 -1
  74. package/dist/questions/choice.d.ts +1 -1
  75. package/dist/questions/confidence-indicator.d.ts +37 -0
  76. package/dist/questions/essay.d.ts +2 -2
  77. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  78. package/dist/questions/hotspot.d.ts +21 -0
  79. package/dist/questions/index.d.ts +11 -1
  80. package/dist/questions/inline-choice.d.ts +21 -0
  81. package/dist/questions/matching.d.ts +22 -0
  82. package/dist/questions/multiple-choice.d.ts +1 -1
  83. package/dist/questions/numeric.d.ts +11 -0
  84. package/dist/questions/ordering.d.ts +12 -0
  85. package/dist/questions/question-renderer.d.ts +1 -1
  86. package/dist/questions/scenario.d.ts +23 -0
  87. package/dist/questions/scoring.d.ts +22 -0
  88. package/dist/questions/spreadsheet.d.ts +29 -0
  89. package/dist/questions/true-false.d.ts +1 -1
  90. package/dist/questions/types.d.ts +106 -1
  91. package/dist/questions/use-drag-reorder.d.ts +17 -0
  92. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  93. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  94. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  95. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  96. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  97. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  98. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  99. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  100. package/dist/sections/CertificateViewer/types.d.ts +13 -5
  101. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  102. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  103. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  104. package/dist/sections/CourseOutline/types.d.ts +6 -0
  105. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  106. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  107. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  108. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  109. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  110. package/dist/sections/ExamSession/types.d.ts +12 -1
  111. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  112. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  113. package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
  114. package/dist/sections/ForumBoard/types.d.ts +78 -0
  115. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  116. package/dist/sections/GradebookTable/types.d.ts +14 -0
  117. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  118. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  119. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  120. package/dist/sections/LessonPage/types.d.ts +6 -0
  121. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  122. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  123. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  124. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  125. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  126. package/dist/sections/QuizSession/types.d.ts +12 -1
  127. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
  128. package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
  129. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  130. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  131. package/dist/sections/RubricView/RubricView.d.ts +9 -0
  132. package/dist/sections/RubricView/types.d.ts +56 -0
  133. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  134. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  135. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  136. package/dist/sections/StudentProfile/types.d.ts +98 -0
  137. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  138. package/dist/sections/SurveyForm/types.d.ts +6 -0
  139. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  140. package/dist/sections/_shared/section-shell.d.ts +20 -0
  141. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  142. package/dist/sections/index.d.ts +13 -1
  143. package/dist/sections.cjs +1 -1
  144. package/dist/sections.js +282 -1786
  145. package/dist/social/post-card.d.ts +1 -1
  146. package/dist/tabs-BsfVo2Bl.cjs +173 -0
  147. package/dist/tabs-BuY1iNJE.js +22305 -0
  148. package/dist/ui/alert.d.ts +1 -1
  149. package/dist/ui/badge.d.ts +1 -1
  150. package/dist/ui/button.d.ts +1 -1
  151. package/dist/ui/drawer.d.ts +84 -0
  152. package/dist/ui/index.d.ts +5 -0
  153. package/dist/ui/progress.d.ts +1 -1
  154. package/dist/ui/rich-text-editor.d.ts +32 -0
  155. package/dist/ui/rich-text-toolbar.d.ts +8 -0
  156. package/dist/ui/toast.d.ts +43 -0
  157. package/dist/utils/array-utils.d.ts +4 -0
  158. package/dist/utils/debounce.d.ts +5 -1
  159. package/dist/utils/flatten-leaves.d.ts +6 -0
  160. package/dist/utils/format-file-size.d.ts +1 -0
  161. package/dist/utils/format-timestamp.d.ts +1 -0
  162. package/dist/utils/is-empty-html.d.ts +5 -0
  163. package/dist/utils/pick-palette-color.d.ts +19 -0
  164. package/dist/utils/shuffle.d.ts +1 -0
  165. package/dist/utils/string-utils.d.ts +12 -0
  166. package/dist/video/types.d.ts +15 -0
  167. package/dist/video/video-bookmark.d.ts +1 -1
  168. package/dist/video/video-player.d.ts +1 -1
  169. package/dist/video/video-playlist-item.d.ts +1 -1
  170. package/dist/withProGate-BWqcKdPM.js +137 -0
  171. package/dist/withProGate-DX6XqKLp.cjs +1 -0
  172. package/package.json +40 -137
  173. package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
  174. package/src/assessment-toolbar/index.ts +6 -0
  175. package/src/assessment-toolbar/question-header-bar.tsx +61 -0
  176. package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
  177. package/src/assessment-toolbar/question-navigator.tsx +13 -36
  178. package/src/assessment-toolbar/timer-display.tsx +6 -5
  179. package/src/assessment-toolbar/types.ts +54 -4
  180. package/src/assessment-toolbar/use-countdown.ts +153 -0
  181. package/src/common/empty-state.tsx +1 -0
  182. package/src/common/index.ts +5 -0
  183. package/src/common/pagination.tsx +135 -0
  184. package/src/common/search-input.tsx +8 -6
  185. package/src/common/stepper.tsx +100 -0
  186. package/src/common/types.ts +41 -0
  187. package/src/content/attachment-list.tsx +92 -0
  188. package/src/content/audio-player.tsx +196 -0
  189. package/src/content/code-block.tsx +113 -0
  190. package/src/content/content-block.tsx +68 -2
  191. package/src/content/embed-block.tsx +78 -0
  192. package/src/content/file-upload-zone.tsx +11 -6
  193. package/src/content/index.ts +9 -0
  194. package/src/content/types.ts +46 -0
  195. package/src/curriculum/course-card.tsx +199 -0
  196. package/src/curriculum/curriculum-item.tsx +9 -5
  197. package/src/curriculum/curriculum-tree.tsx +20 -13
  198. package/src/curriculum/index.ts +2 -0
  199. package/src/curriculum/types.ts +2 -2
  200. package/src/feedback/feedback-banner.tsx +12 -14
  201. package/src/flashcards/flashcard-deck.tsx +1 -9
  202. package/src/flashcards/flashcard.tsx +29 -9
  203. package/src/index.ts +3 -0
  204. package/src/license/HydraContext.tsx +62 -0
  205. package/src/license/ProBadge.tsx +43 -0
  206. package/src/license/index.ts +7 -0
  207. package/src/license/tiers.ts +24 -0
  208. package/src/license/useHydraLicense.ts +10 -0
  209. package/src/license/validate.ts +90 -0
  210. package/src/license/withProGate.tsx +21 -0
  211. package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
  212. package/src/modules/AssignmentModule/types.ts +77 -0
  213. package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
  214. package/src/modules/CertificateModule/types.ts +49 -0
  215. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  216. package/src/modules/CourseCatalogModule/types.ts +47 -0
  217. package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
  218. package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
  219. package/src/modules/DiscussionModule/types.ts +54 -0
  220. package/src/modules/ExamModule/ExamModule.tsx +151 -0
  221. package/src/modules/ExamModule/types.ts +57 -0
  222. package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
  223. package/src/modules/FlashcardLab/types.ts +2 -0
  224. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
  225. package/src/modules/GradeCenterModule/types.ts +65 -0
  226. package/src/modules/QuizModule/QuizModule.tsx +58 -178
  227. package/src/modules/QuizModule/types.ts +10 -15
  228. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  229. package/src/modules/StudentDashboardModule/types.ts +56 -0
  230. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  231. package/src/modules/StudentProfileModule/types.ts +45 -0
  232. package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
  233. package/src/modules/SurveyModule/types.ts +53 -0
  234. package/src/modules/_shared/assessment-intro.tsx +75 -0
  235. package/src/modules/_shared/assessment-results.tsx +133 -0
  236. package/src/modules/_shared/types.ts +11 -0
  237. package/src/modules/_shared/use-timer.ts +49 -0
  238. package/src/modules/index.ts +33 -0
  239. package/src/progress/achievement-badge.tsx +52 -0
  240. package/src/progress/activity-timeline.tsx +84 -0
  241. package/src/progress/grade-indicator.tsx +9 -1
  242. package/src/progress/index.ts +7 -0
  243. package/src/progress/progress-ring.tsx +2 -1
  244. package/src/progress/stat-card.tsx +37 -18
  245. package/src/progress/streak-badge.tsx +35 -0
  246. package/src/progress/types.ts +103 -0
  247. package/src/provider/HydraProvider.tsx +15 -6
  248. package/src/questions/choice.tsx +19 -14
  249. package/src/questions/confidence-indicator.tsx +107 -0
  250. package/src/questions/essay.tsx +28 -28
  251. package/src/questions/fill-in-the-blank.tsx +20 -19
  252. package/src/questions/hotspot.tsx +154 -0
  253. package/src/questions/index.ts +18 -0
  254. package/src/questions/inline-choice.tsx +152 -0
  255. package/src/questions/matching.tsx +229 -0
  256. package/src/questions/multiple-choice.tsx +19 -14
  257. package/src/questions/numeric.tsx +106 -0
  258. package/src/questions/ordering.tsx +167 -0
  259. package/src/questions/question-renderer.tsx +24 -2
  260. package/src/questions/scenario.tsx +140 -0
  261. package/src/questions/scoring.ts +201 -0
  262. package/src/questions/spreadsheet.tsx +260 -0
  263. package/src/questions/true-false.tsx +19 -14
  264. package/src/questions/types.ts +123 -1
  265. package/src/questions/use-drag-reorder.ts +80 -0
  266. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
  267. package/src/sections/AnnouncementFeed/types.ts +15 -1
  268. package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
  269. package/src/sections/AssessmentReview/types.ts +6 -0
  270. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
  271. package/src/sections/AssignmentSubmission/types.ts +6 -0
  272. package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
  273. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  274. package/src/sections/CertificateViewer/types.ts +19 -5
  275. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  276. package/src/sections/CourseCatalog/types.ts +76 -0
  277. package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
  278. package/src/sections/CourseOutline/types.ts +6 -0
  279. package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
  280. package/src/sections/DiscussionThread/types.ts +6 -0
  281. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  282. package/src/sections/EnrollmentWizard/types.ts +65 -0
  283. package/src/sections/ExamSession/ExamSession.tsx +125 -82
  284. package/src/sections/ExamSession/types.ts +12 -1
  285. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  286. package/src/sections/FlashcardStudySession/types.ts +6 -0
  287. package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
  288. package/src/sections/ForumBoard/types.ts +81 -0
  289. package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
  290. package/src/sections/GradebookTable/types.ts +14 -0
  291. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  292. package/src/sections/LecturePlayer/types.ts +8 -0
  293. package/src/sections/LessonPage/LessonPage.tsx +40 -13
  294. package/src/sections/LessonPage/types.ts +6 -0
  295. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
  296. package/src/sections/PracticeQuiz/types.ts +6 -0
  297. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
  298. package/src/sections/ProgressDashboard/types.ts +6 -0
  299. package/src/sections/QuizSession/QuizSession.tsx +115 -67
  300. package/src/sections/QuizSession/types.ts +12 -1
  301. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
  302. package/src/sections/RequirementsChecklist/types.ts +44 -0
  303. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
  304. package/src/sections/ResourceLibrary/types.ts +15 -1
  305. package/src/sections/RubricView/RubricView.tsx +174 -0
  306. package/src/sections/RubricView/types.ts +58 -0
  307. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
  308. package/src/sections/ScrollableQuiz/types.ts +6 -0
  309. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  310. package/src/sections/StudentProfile/types.ts +99 -0
  311. package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
  312. package/src/sections/SurveyForm/types.ts +6 -0
  313. package/src/sections/_shared/merge-answers.ts +22 -0
  314. package/src/sections/_shared/section-shell.tsx +64 -0
  315. package/src/sections/_shared/use-assessment-session.ts +125 -0
  316. package/src/sections/index.ts +42 -1
  317. package/src/social/post-card.tsx +8 -19
  318. package/src/social/user-avatar.tsx +10 -5
  319. package/src/styles/globals.css +52 -41
  320. package/src/ui/badge.tsx +8 -0
  321. package/src/ui/drawer.tsx +600 -0
  322. package/src/ui/index.ts +21 -0
  323. package/src/ui/progress.tsx +4 -0
  324. package/src/ui/rich-text-editor.tsx +119 -0
  325. package/src/ui/rich-text-toolbar.tsx +157 -0
  326. package/src/ui/toast.tsx +170 -0
  327. package/src/utils/array-utils.ts +17 -0
  328. package/src/utils/debounce.ts +8 -2
  329. package/src/utils/flatten-leaves.ts +17 -0
  330. package/src/utils/format-file-size.ts +5 -0
  331. package/src/utils/format-timestamp.ts +13 -0
  332. package/src/utils/is-empty-html.ts +7 -0
  333. package/src/utils/pick-palette-color.ts +33 -0
  334. package/src/utils/shuffle.ts +8 -0
  335. package/src/utils/string-utils.ts +30 -0
  336. package/src/video/types.ts +16 -0
  337. package/src/video/video-bookmark.tsx +4 -3
  338. package/src/video/video-chapter-list.tsx +9 -4
  339. package/src/video/video-player.tsx +24 -5
  340. package/src/video/video-playlist-item.tsx +8 -3
  341. package/src/video/video-thumbnail-card.tsx +4 -0
  342. package/src/video/video-transcript.tsx +8 -5
  343. package/dist/table-BrS5cDQu.js +0 -2510
  344. package/dist/table-D6AkBBEo.cjs +0 -1
@@ -0,0 +1,44 @@
1
+ /**
2
+ * RequirementsChecklist section — shows completion requirements with progress tracking.
3
+ *
4
+ * Displays a list of requirements with checked/unchecked status and an overall
5
+ * progress bar. Incomplete items can be clicked to navigate to the relevant content.
6
+ *
7
+ * @example
8
+ * <RequirementsChecklist
9
+ * title="Certificate Requirements"
10
+ * requirements={requirements}
11
+ * onRequirementClick={(uid) => navigateTo(uid)}
12
+ * />
13
+ */
14
+ export interface RequirementsChecklistProps {
15
+ /** Section title */
16
+ title?: string;
17
+ /** List of requirements to display */
18
+ requirements: Requirement[];
19
+ /** Called when the user clicks an incomplete requirement */
20
+ onRequirementClick?: (uid: string) => void;
21
+ /** Render skeleton placeholders instead of content */
22
+ isLoading?: boolean;
23
+ /** Error message — renders an error state with optional retry */
24
+ error?: string | null;
25
+ /** Called when the user clicks retry in the error state */
26
+ onRetry?: () => void;
27
+ /** CSS class name for the root element */
28
+ className?: string;
29
+ /** Inline styles for the root element */
30
+ style?: React.CSSProperties;
31
+ }
32
+
33
+ export interface Requirement {
34
+ /** Unique identifier */
35
+ uid: string;
36
+ /** Requirement label */
37
+ label: string;
38
+ /** Optional description */
39
+ description?: string;
40
+ /** Whether this requirement has been completed */
41
+ completed: boolean;
42
+ /** Optional action label for clickable items */
43
+ actionLabel?: string;
44
+ }
@@ -1,19 +1,16 @@
1
1
  import { useMemo, useState } from "react";
2
- import { Download, Grid, List as ListIcon } from "lucide-react";
2
+ import { AlertCircle, Download, Grid, List as ListIcon } from "lucide-react";
3
3
  import { LearningObjectIcon } from "../../curriculum";
4
4
  import { SearchInput, EmptyState } from "../../common";
5
+ import { Skeleton } from "../../ui/skeleton";
5
6
  import { Button } from "../../ui/button";
6
7
  import { Card, CardContent } from "../../ui/card";
7
8
  import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs";
8
9
  import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
10
+ import { Pagination } from "../../common/pagination";
9
11
  import type { ResourceLibraryProps, Resource } from "./types";
10
12
  import { cn } from "../../lib/utils";
11
-
12
- function formatBytes(bytes: number): string {
13
- if (bytes < 1024) return `${bytes} B`;
14
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
15
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
16
- }
13
+ import { formatFileSize } from "../../utils/format-file-size";
17
14
 
18
15
  const TYPE_TO_ICON: Record<string, string> = {
19
16
  pdf: "document",
@@ -35,6 +32,13 @@ export function ResourceLibrary({
35
32
  showSearch = true,
36
33
  emptyMessage = "No resources found",
37
34
  readOnly = false,
35
+ isLoading,
36
+ error,
37
+ onRetry,
38
+ pageSize,
39
+ currentPage = 1,
40
+ totalItems,
41
+ onPageChange,
38
42
  className,
39
43
  style,
40
44
  }: ResourceLibraryProps) {
@@ -58,6 +62,38 @@ export function ResourceLibrary({
58
62
  return result;
59
63
  }, [resources, activeCategoryUid, searchQuery]);
60
64
 
65
+ if (isLoading) {
66
+ return (
67
+ <div className={cn("space-y-4", className)} style={style}>
68
+ <Skeleton className="h-9 w-full" />
69
+ <div className="grid grid-cols-3 gap-4">
70
+ {Array.from({ length: 6 }).map((_, i) => (
71
+ <Skeleton key={i} className="h-32 w-full rounded-lg" />
72
+ ))}
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ if (error) {
79
+ return (
80
+ <div className={cn("py-12", className)} style={style}>
81
+ <EmptyState
82
+ icon={<AlertCircle className="size-10 text-destructive" />}
83
+ title="Something went wrong"
84
+ description={error}
85
+ action={
86
+ onRetry ? (
87
+ <Button variant="outline" onClick={onRetry}>
88
+ Retry
89
+ </Button>
90
+ ) : undefined
91
+ }
92
+ />
93
+ </div>
94
+ );
95
+ }
96
+
61
97
  function renderResource(resource: Resource) {
62
98
  const iconType = TYPE_TO_ICON[resource.type] ?? "document";
63
99
 
@@ -66,10 +102,10 @@ export function ResourceLibrary({
66
102
  key={resource.uid}
67
103
  className={cn(
68
104
  "flex items-center gap-3 px-3 py-2",
69
- !readOnly && "cursor-pointer hover:bg-muted",
70
- readOnly && "opacity-70",
105
+ !readOnly && onResourceClick && "cursor-pointer hover:bg-muted",
106
+ (readOnly || !onResourceClick) && "opacity-70",
71
107
  )}
72
- onClick={() => !readOnly && onResourceClick(resource)}
108
+ onClick={() => !readOnly && onResourceClick?.(resource)}
73
109
  >
74
110
  <div className="min-w-10">
75
111
  <LearningObjectIcon type={iconType} size={20} />
@@ -77,10 +113,10 @@ export function ResourceLibrary({
77
113
  <div className="flex-1 min-w-0">
78
114
  <span className="text-sm text-foreground">{resource.name}</span>
79
115
  {(resource.description || resource.fileSize != null) && (
80
- <span className="text-sm text-muted-foreground">
116
+ <span className="block text-xs text-muted-foreground">
81
117
  {[
82
118
  resource.description,
83
- resource.fileSize != null && formatBytes(resource.fileSize),
119
+ resource.fileSize != null && formatFileSize(resource.fileSize),
84
120
  ]
85
121
  .filter(Boolean)
86
122
  .join(" \u00b7 ")}
@@ -178,18 +214,24 @@ export function ResourceLibrary({
178
214
  <EmptyState title={emptyMessage} description="Try adjusting your search or filter." />
179
215
  ) : viewMode === "list" ? (
180
216
  <Card>
181
- {filtered.map(renderResource)}
217
+ {(onPageChange && pageSize
218
+ ? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
219
+ : filtered
220
+ ).map(renderResource)}
182
221
  </Card>
183
222
  ) : (
184
223
  <div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2">
185
- {filtered.map((resource) => (
224
+ {(onPageChange && pageSize
225
+ ? filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
226
+ : filtered
227
+ ).map((resource) => (
186
228
  <Card
187
229
  key={resource.uid}
188
230
  className={cn(
189
231
  "transition-colors",
190
- !readOnly && "cursor-pointer hover:border-primary",
232
+ !readOnly && onResourceClick && "cursor-pointer hover:border-primary",
191
233
  )}
192
- onClick={() => !readOnly && onResourceClick(resource)}
234
+ onClick={() => !readOnly && onResourceClick?.(resource)}
193
235
  >
194
236
  <CardContent className="pt-4 pb-4">
195
237
  <div className="flex gap-1 items-center mb-1">
@@ -205,7 +247,7 @@ export function ResourceLibrary({
205
247
  )}
206
248
  {resource.fileSize != null && (
207
249
  <span className="text-xs text-muted-foreground">
208
- {formatBytes(resource.fileSize)}
250
+ {formatFileSize(resource.fileSize)}
209
251
  </span>
210
252
  )}
211
253
  </CardContent>
@@ -213,6 +255,15 @@ export function ResourceLibrary({
213
255
  ))}
214
256
  </div>
215
257
  )}
258
+
259
+ {onPageChange && pageSize && filtered.length > 0 && (
260
+ <Pagination
261
+ currentPage={currentPage}
262
+ totalPages={Math.ceil((totalItems ?? filtered.length) / pageSize)}
263
+ onPageChange={onPageChange}
264
+ className="mt-4"
265
+ />
266
+ )}
216
267
  </div>
217
268
  );
218
269
  }
@@ -18,7 +18,7 @@ export interface ResourceLibraryProps {
18
18
  /** Optional categories for tab filtering */
19
19
  categories?: { uid: string; label: string }[];
20
20
  /** Called when the user clicks a resource */
21
- onResourceClick: (resource: Resource) => void;
21
+ onResourceClick?: (resource: Resource) => void;
22
22
  /** Called when the user downloads a resource */
23
23
  onDownload?: (resource: Resource) => void;
24
24
  /** Layout view mode */
@@ -31,6 +31,20 @@ export interface ResourceLibraryProps {
31
31
  emptyMessage?: string;
32
32
  /** When true, disables interactions */
33
33
  readOnly?: boolean;
34
+ /** Render skeleton placeholders instead of content */
35
+ isLoading?: boolean;
36
+ /** Error message — renders an error state with optional retry */
37
+ error?: string | null;
38
+ /** Called when the user clicks retry in the error state */
39
+ onRetry?: () => void;
40
+ /** Number of items per page (enables pagination when set with onPageChange) */
41
+ pageSize?: number;
42
+ /** Current page (1-indexed) */
43
+ currentPage?: number;
44
+ /** Total number of items (for server-side pagination) */
45
+ totalItems?: number;
46
+ /** Called when the user navigates to a different page */
47
+ onPageChange?: (page: number) => void;
34
48
  /** CSS class name for the root element */
35
49
  className?: string;
36
50
  /** Inline styles for the root element */
@@ -0,0 +1,174 @@
1
+ import { AlertCircle, CheckCircle2 } from "lucide-react";
2
+ import { Skeleton } from "../../ui/skeleton";
3
+ import { EmptyState } from "../../common/empty-state";
4
+ import { Button } from "../../ui/button";
5
+ import { Card, CardContent } from "../../ui/card";
6
+ import {
7
+ Table,
8
+ TableHeader,
9
+ TableBody,
10
+ TableRow,
11
+ TableHead,
12
+ TableCell,
13
+ } from "../../ui/table";
14
+ import { Badge } from "../../ui/badge";
15
+ import { Separator } from "../../ui/separator";
16
+ import { cn } from "../../lib/utils";
17
+ import type { RubricViewProps } from "./types";
18
+
19
+ /**
20
+ * RubricView — displays a grading rubric with criteria rows and proficiency level columns.
21
+ *
22
+ * When `selectedLevels` is provided, highlights the scored level per criterion and
23
+ * shows an overall score. Useful inside AssignmentModule for showing grading criteria
24
+ * and results.
25
+ */
26
+ export function RubricView({
27
+ criteria,
28
+ selectedLevels,
29
+ totalScore,
30
+ maxScore,
31
+ feedback,
32
+ isLoading,
33
+ error,
34
+ onRetry,
35
+ className,
36
+ style,
37
+ }: RubricViewProps) {
38
+ const isScored = selectedLevels && Object.keys(selectedLevels).length > 0;
39
+
40
+ if (isLoading) {
41
+ return (
42
+ <div className={cn("space-y-4", className)} style={style}>
43
+ <Skeleton className="h-10 w-full" />
44
+ {Array.from({ length: 3 }).map((_, i) => (
45
+ <Skeleton key={i} className="h-16 w-full" />
46
+ ))}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ if (error) {
52
+ return (
53
+ <div className={cn("py-12", className)} style={style}>
54
+ <EmptyState
55
+ icon={<AlertCircle className="size-10 text-destructive" />}
56
+ title="Something went wrong"
57
+ description={error}
58
+ action={
59
+ onRetry ? (
60
+ <Button variant="outline" onClick={onRetry}>
61
+ Retry
62
+ </Button>
63
+ ) : undefined
64
+ }
65
+ />
66
+ </div>
67
+ );
68
+ }
69
+
70
+ return (
71
+ <div className={cn("flex flex-col gap-4", className)} style={style}>
72
+ {/* Score header */}
73
+ {isScored && totalScore !== undefined && maxScore !== undefined && (
74
+ <div className="flex items-center justify-between">
75
+ <h3 className="text-lg font-semibold text-foreground">Rubric</h3>
76
+ <Badge variant={totalScore >= maxScore * 0.7 ? "success" : "destructive"}>
77
+ {totalScore} / {maxScore} pts
78
+ </Badge>
79
+ </div>
80
+ )}
81
+
82
+ {!isScored && (
83
+ <h3 className="text-lg font-semibold text-foreground">Grading Rubric</h3>
84
+ )}
85
+
86
+ {/* Criteria table */}
87
+ <Card>
88
+ <CardContent className="p-0">
89
+ <div className="overflow-x-auto">
90
+ <Table className="text-sm">
91
+ <TableHeader>
92
+ <TableRow className="bg-muted/50">
93
+ <TableHead className="text-left font-medium text-muted-foreground w-1/5">
94
+ Criterion
95
+ </TableHead>
96
+ {criteria[0]?.levels.map((level) => (
97
+ <TableHead
98
+ key={level.uid}
99
+ className="text-center font-medium text-muted-foreground"
100
+ >
101
+ <div>{level.label}</div>
102
+ <div className="text-xs font-normal mt-0.5">
103
+ {level.points} pts
104
+ </div>
105
+ </TableHead>
106
+ ))}
107
+ </TableRow>
108
+ </TableHeader>
109
+ <TableBody>
110
+ {criteria.map((criterion, i) => {
111
+ const selectedLevelUid = selectedLevels?.[criterion.uid];
112
+ return (
113
+ <TableRow
114
+ key={criterion.uid}
115
+ className={cn(i % 2 === 1 && "bg-muted/20")}
116
+ >
117
+ <TableCell className="align-top">
118
+ <div className="font-medium text-foreground">
119
+ {criterion.name}
120
+ </div>
121
+ {criterion.description && (
122
+ <div className="text-xs text-muted-foreground mt-0.5">
123
+ {criterion.description}
124
+ </div>
125
+ )}
126
+ </TableCell>
127
+ {criterion.levels.map((level) => {
128
+ const isSelected = selectedLevelUid === level.uid;
129
+ return (
130
+ <TableCell
131
+ key={level.uid}
132
+ className={cn(
133
+ "align-top text-center",
134
+ isSelected &&
135
+ "bg-primary/10 ring-2 ring-primary/30 ring-inset rounded-sm"
136
+ )}
137
+ >
138
+ <div className="text-xs text-muted-foreground">
139
+ {level.description}
140
+ </div>
141
+ {isSelected && (
142
+ <div className="mt-1.5 flex justify-center">
143
+ <CheckCircle2 className="size-4 text-primary" />
144
+ </div>
145
+ )}
146
+ </TableCell>
147
+ );
148
+ })}
149
+ </TableRow>
150
+ );
151
+ })}
152
+ </TableBody>
153
+ </Table>
154
+ </div>
155
+ </CardContent>
156
+ </Card>
157
+
158
+ {/* Feedback */}
159
+ {feedback && (
160
+ <>
161
+ <Separator />
162
+ <div>
163
+ <h4 className="text-sm font-medium text-foreground mb-1">
164
+ Instructor Feedback
165
+ </h4>
166
+ <p className="text-sm text-muted-foreground whitespace-pre-wrap">
167
+ {feedback}
168
+ </p>
169
+ </div>
170
+ </>
171
+ )}
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * RubricView section — displays a grading rubric with criteria and proficiency levels.
3
+ *
4
+ * Shows criteria as rows with level columns. When `selectedLevels` is provided,
5
+ * highlights the scored level per criterion and shows total score.
6
+ *
7
+ * @example
8
+ * <RubricView
9
+ * criteria={rubricCriteria}
10
+ * selectedLevels={{ "c1": "l2", "c2": "l3" }}
11
+ * totalScore={85}
12
+ * maxScore={100}
13
+ * />
14
+ */
15
+ export interface RubricViewProps {
16
+ /** Rubric criteria (rows) */
17
+ criteria: RubricCriterion[];
18
+ /** Selected level UID per criterion UID — highlights scored cells */
19
+ selectedLevels?: Record<string, string>;
20
+ /** Overall rubric score (sum of selected levels) */
21
+ totalScore?: number;
22
+ /** Maximum possible score */
23
+ maxScore?: number;
24
+ /** Instructor feedback text */
25
+ feedback?: string;
26
+ /** Render skeleton placeholders instead of content */
27
+ isLoading?: boolean;
28
+ /** Error message — renders an error state with optional retry */
29
+ error?: string | null;
30
+ /** Called when the user clicks retry in the error state */
31
+ onRetry?: () => void;
32
+ /** CSS class name for the root element */
33
+ className?: string;
34
+ /** Inline styles for the root element */
35
+ style?: React.CSSProperties;
36
+ }
37
+
38
+ export interface RubricCriterion {
39
+ /** Unique identifier */
40
+ uid: string;
41
+ /** Criterion name (e.g. "Organization", "Evidence") */
42
+ name: string;
43
+ /** Optional description of what this criterion evaluates */
44
+ description?: string;
45
+ /** Proficiency levels from lowest to highest */
46
+ levels: RubricLevel[];
47
+ }
48
+
49
+ export interface RubricLevel {
50
+ /** Unique identifier */
51
+ uid: string;
52
+ /** Level label (e.g. "Excellent", "Good", "Needs Work") */
53
+ label: string;
54
+ /** Description of what this level looks like */
55
+ description: string;
56
+ /** Point value for this level */
57
+ points: number;
58
+ }
@@ -1,9 +1,15 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { QuestionRenderer } from "../../questions";
3
3
  import type { SessionAnswer } from "../../questions/types";
4
+
5
+ const EMPTY_ANSWERS: SessionAnswer[] = [];
4
6
  import { Button } from "../../ui/button";
5
7
  import { Card, CardContent } from "../../ui/card";
8
+ import { Separator } from "../../ui/separator";
9
+ import { Skeleton } from "../../ui/skeleton";
6
10
  import { cn } from "../../lib/utils";
11
+ import { mergeSessionAnswers } from "../_shared/merge-answers";
12
+ import { SectionShell } from "../_shared/section-shell";
7
13
  import type { ScrollableQuizProps } from "./types";
8
14
 
9
15
  export function ScrollableQuiz({
@@ -16,6 +22,9 @@ export function ScrollableQuiz({
16
22
  questionGroups,
17
23
  isSubmitting = false,
18
24
  readOnly = false,
25
+ isLoading,
26
+ error,
27
+ onRetry,
19
28
  className,
20
29
  style,
21
30
  }: ScrollableQuizProps) {
@@ -23,10 +32,20 @@ export function ScrollableQuiz({
23
32
  const [activeUid, setActiveUid] = useState<string | null>(questions[0]?.uid ?? null);
24
33
  const questionRefs = useRef<Map<string, HTMLElement>>(new Map());
25
34
 
26
- const answeredCount = useMemo(() => {
27
- const answered = new Set(sessionAnswers.map((a) => a.uid));
28
- return questions.filter((q) => answered.has(q.uid)).length;
29
- }, [questions, sessionAnswers]);
35
+ const answersByQuestion = useMemo(() => {
36
+ const map = new Map<string, SessionAnswer[]>();
37
+ for (const a of sessionAnswers) {
38
+ const list = map.get(a.uid);
39
+ if (list) list.push(a);
40
+ else map.set(a.uid, [a]);
41
+ }
42
+ return map;
43
+ }, [sessionAnswers]);
44
+
45
+ const answeredCount = useMemo(
46
+ () => questions.filter((q) => answersByQuestion.has(q.uid)).length,
47
+ [questions, answersByQuestion],
48
+ );
30
49
 
31
50
  useEffect(() => {
32
51
  const observer = new IntersectionObserver(
@@ -49,19 +68,17 @@ export function ScrollableQuiz({
49
68
  else questionRefs.current.delete(uid);
50
69
  }, []);
51
70
 
52
- function handleAnswer(questionUid: string, rawAnswers: { uid: string; content?: string }[]) {
53
- const newAnswers: SessionAnswer[] = rawAnswers.map((a) => ({
54
- uid: questionUid,
55
- answerUid: a.uid,
56
- content: a.content,
57
- }));
58
- setSessionAnswers((prev) => {
59
- const filtered = prev.filter((a) => a.uid !== questionUid);
60
- const merged = [...filtered, ...newAnswers];
61
- onAnswerChange?.(merged);
62
- return merged;
63
- });
64
- }
71
+ const onAnswerChangeRef = useRef(onAnswerChange);
72
+ onAnswerChangeRef.current = onAnswerChange;
73
+
74
+ const handleAnswer = useCallback(
75
+ (questionUid: string, rawAnswers: { uid: string; content?: string }[]) => {
76
+ setSessionAnswers((prev) =>
77
+ mergeSessionAnswers(prev, questionUid, rawAnswers, onAnswerChangeRef.current),
78
+ );
79
+ },
80
+ [],
81
+ );
65
82
 
66
83
  function scrollToQuestion(uid: string) {
67
84
  questionRefs.current.get(uid)?.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -86,11 +103,25 @@ export function ScrollableQuiz({
86
103
  let globalIndex = 0;
87
104
 
88
105
  return (
89
- <div className={cn("flex gap-3", className)} style={style}>
106
+ <SectionShell
107
+ isLoading={isLoading}
108
+ error={error}
109
+ onRetry={onRetry}
110
+ className={className}
111
+ style={style}
112
+ skeleton={
113
+ <>
114
+ <Skeleton className="h-32 w-full" />
115
+ <Skeleton className="h-32 w-full" />
116
+ <Skeleton className="h-32 w-full" />
117
+ </>
118
+ }
119
+ >
120
+ <div className="flex gap-3">
90
121
  {/* Main content */}
91
122
  <div className="flex-1 min-w-0">
92
123
  {orderedQuestions.map((group, gi) => (
93
- <div key={gi}>
124
+ <div key={group.label ?? `ungrouped-${gi}`}>
94
125
  {group.label && (
95
126
  <p className={cn("text-lg font-semibold mb-2 text-foreground", gi > 0 && "mt-4")}>
96
127
  {group.label}
@@ -113,7 +144,7 @@ export function ScrollableQuiz({
113
144
  )}
114
145
  <QuestionRenderer
115
146
  question={q}
116
- sessionAnswers={sessionAnswers.filter((a) => a.uid === q.uid)}
147
+ sessionAnswers={answersByQuestion.get(q.uid) ?? EMPTY_ANSWERS}
117
148
  onAnswer={(answers) => handleAnswer(q.uid, answers)}
118
149
  readOnly={readOnly}
119
150
  />
@@ -123,7 +154,8 @@ export function ScrollableQuiz({
123
154
  })}
124
155
  </div>
125
156
  ))}
126
- <div className="mt-3 flex justify-between items-center">
157
+ <Separator className="my-3" />
158
+ <div className="flex justify-between items-center">
127
159
  <span className="text-sm text-muted-foreground">
128
160
  {answeredCount} of {questions.length} answered
129
161
  </span>
@@ -138,11 +170,12 @@ export function ScrollableQuiz({
138
170
 
139
171
  {/* Sidebar navigator */}
140
172
  {showNavigator && (
141
- <Card className="hidden md:block w-50 shrink-0 sticky top-4 self-start p-3">
173
+ <Card className="hidden md:block w-50 shrink-0 sticky top-4 self-start">
174
+ <CardContent className="p-3">
142
175
  <p className="font-semibold text-sm mb-1 text-foreground">Questions</p>
143
176
  <div className="flex flex-wrap gap-1">
144
177
  {questions.map((q, i) => {
145
- const isAnswered = sessionAnswers.some((a) => a.uid === q.uid);
178
+ const isAnswered = answersByQuestion.has(q.uid);
146
179
  const isActive = activeUid === q.uid;
147
180
  return (
148
181
  <button
@@ -163,8 +196,10 @@ export function ScrollableQuiz({
163
196
  );
164
197
  })}
165
198
  </div>
199
+ </CardContent>
166
200
  </Card>
167
201
  )}
168
202
  </div>
203
+ </SectionShell>
169
204
  );
170
205
  }
@@ -33,6 +33,12 @@ export interface ScrollableQuizProps {
33
33
  isSubmitting?: boolean;
34
34
  /** When true, all inputs are disabled */
35
35
  readOnly?: boolean;
36
+ /** Render skeleton placeholders instead of content */
37
+ isLoading?: boolean;
38
+ /** Error message — renders an error state with optional retry */
39
+ error?: string | null;
40
+ /** Called when the user clicks retry in the error state */
41
+ onRetry?: () => void;
36
42
  /** CSS class name for the root element */
37
43
  className?: string;
38
44
  /** Inline styles for the root element */