@hydralms/components 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/StudentProfile-BPsZBaJj.cjs +1 -0
  2. package/dist/StudentProfile-Cw2p-RZn.js +3273 -0
  3. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  4. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  5. package/dist/common/index.d.ts +2 -1
  6. package/dist/common/pagination.d.ts +26 -0
  7. package/dist/common/types.d.ts +1 -0
  8. package/dist/components.css +1 -1
  9. package/dist/content/audio-player.d.ts +22 -0
  10. package/dist/content/code-block.d.ts +30 -0
  11. package/dist/content/embed-block.d.ts +28 -0
  12. package/dist/content/index.d.ts +6 -0
  13. package/dist/content/types.d.ts +24 -0
  14. package/dist/curriculum/course-card.d.ts +51 -0
  15. package/dist/curriculum/index.d.ts +2 -0
  16. package/dist/curriculum/types.d.ts +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +495 -439
  20. package/dist/license/HydraContext.d.ts +16 -0
  21. package/dist/license/ProBadge.d.ts +6 -0
  22. package/dist/license/index.d.ts +7 -0
  23. package/dist/license/tiers.d.ts +6 -0
  24. package/dist/license/useHydraLicense.d.ts +6 -0
  25. package/dist/license/validate.d.ts +13 -0
  26. package/dist/license/withProGate.d.ts +6 -0
  27. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +4 -7
  28. package/dist/modules/AssignmentModule/types.d.ts +5 -1
  29. package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
  30. package/dist/modules/CertificateModule/types.d.ts +6 -4
  31. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  32. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  33. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  34. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +4 -7
  35. package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
  36. package/dist/modules/ExamModule/types.d.ts +5 -14
  37. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  38. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  39. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +4 -8
  40. package/dist/modules/GradeCenterModule/types.d.ts +2 -0
  41. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  42. package/dist/modules/QuizModule/types.d.ts +5 -14
  43. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  44. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  45. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  46. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  47. package/dist/modules/SurveyModule/SurveyModule.d.ts +4 -6
  48. package/dist/modules/SurveyModule/types.d.ts +2 -0
  49. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  50. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  51. package/dist/modules/_shared/types.d.ts +10 -0
  52. package/dist/modules/_shared/use-timer.d.ts +9 -0
  53. package/dist/modules/index.d.ts +6 -0
  54. package/dist/modules.cjs +1 -1
  55. package/dist/modules.js +1267 -854
  56. package/dist/progress/types.d.ts +2 -0
  57. package/dist/provider/HydraProvider.d.ts +5 -1
  58. package/dist/questions/choice.d.ts +1 -1
  59. package/dist/questions/confidence-indicator.d.ts +37 -0
  60. package/dist/questions/essay.d.ts +1 -1
  61. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  62. package/dist/questions/hotspot.d.ts +1 -1
  63. package/dist/questions/index.d.ts +2 -0
  64. package/dist/questions/inline-choice.d.ts +1 -1
  65. package/dist/questions/matching.d.ts +1 -1
  66. package/dist/questions/multiple-choice.d.ts +1 -1
  67. package/dist/questions/numeric.d.ts +1 -1
  68. package/dist/questions/ordering.d.ts +1 -1
  69. package/dist/questions/question-renderer.d.ts +1 -1
  70. package/dist/questions/scenario.d.ts +1 -1
  71. package/dist/questions/spreadsheet.d.ts +1 -1
  72. package/dist/questions/true-false.d.ts +1 -1
  73. package/dist/sections/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
  74. package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
  75. package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
  76. package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
  77. package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
  78. package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -0
  79. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  80. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  81. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  82. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  83. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  84. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  85. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  86. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  87. package/dist/sections/CertificateViewer/types.d.ts +6 -0
  88. package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
  89. package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
  90. package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
  91. package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -0
  92. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  93. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  94. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  95. package/dist/sections/CourseOutline/types.d.ts +6 -0
  96. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  97. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  98. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  99. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  100. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  101. package/dist/sections/ExamSession/types.d.ts +6 -0
  102. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  103. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  104. package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
  105. package/dist/sections/ForumBoard/types.d.ts +14 -0
  106. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  107. package/dist/sections/GradebookTable/types.d.ts +14 -0
  108. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  109. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  110. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  111. package/dist/sections/LessonPage/types.d.ts +6 -0
  112. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  113. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  114. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  115. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  116. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  117. package/dist/sections/QuizSession/types.d.ts +6 -0
  118. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
  119. package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
  120. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  121. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  122. package/dist/sections/RubricView/RubricView.d.ts +1 -1
  123. package/dist/sections/RubricView/types.d.ts +6 -0
  124. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  125. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  126. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  127. package/dist/sections/StudentProfile/types.d.ts +98 -0
  128. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  129. package/dist/sections/SurveyForm/types.d.ts +6 -0
  130. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  131. package/dist/sections/_shared/section-shell.d.ts +20 -0
  132. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  133. package/dist/sections/index.d.ts +10 -0
  134. package/dist/sections.cjs +1 -1
  135. package/dist/sections.js +1361 -307
  136. package/dist/ui/badge.d.ts +1 -1
  137. package/dist/ui/index.d.ts +2 -0
  138. package/dist/ui/progress.d.ts +1 -1
  139. package/dist/ui/rich-text-editor.d.ts +3 -1
  140. package/dist/ui/toast.d.ts +43 -0
  141. package/dist/utils/debounce.d.ts +5 -1
  142. package/dist/utils/pick-palette-color.d.ts +19 -0
  143. package/dist/video/types.d.ts +15 -0
  144. package/dist/video/video-player.d.ts +1 -1
  145. package/dist/withProGate-BJdu1T9Y.cjs +2 -0
  146. package/dist/withProGate-BvFc7Jwy.js +4975 -0
  147. package/package.json +57 -226
  148. package/src/assessment-toolbar/question-navigator.tsx +10 -5
  149. package/src/assessment-toolbar/timer-display.tsx +4 -3
  150. package/src/assessment-toolbar/use-countdown.ts +1 -1
  151. package/src/common/empty-state.tsx +1 -0
  152. package/src/common/index.ts +2 -0
  153. package/src/common/pagination.tsx +135 -0
  154. package/src/common/search-input.tsx +2 -1
  155. package/src/common/types.ts +2 -0
  156. package/src/content/attachment-list.tsx +2 -0
  157. package/src/content/audio-player.tsx +196 -0
  158. package/src/content/code-block.tsx +113 -0
  159. package/src/content/content-block.tsx +64 -0
  160. package/src/content/embed-block.tsx +78 -0
  161. package/src/content/file-upload-zone.tsx +10 -0
  162. package/src/content/index.ts +6 -0
  163. package/src/content/types.ts +5 -0
  164. package/src/curriculum/course-card.tsx +199 -0
  165. package/src/curriculum/curriculum-item.tsx +3 -3
  166. package/src/curriculum/curriculum-tree.tsx +20 -13
  167. package/src/curriculum/index.ts +2 -0
  168. package/src/curriculum/types.ts +2 -2
  169. package/src/flashcards/flashcard.tsx +28 -8
  170. package/src/index.ts +3 -0
  171. package/src/license/HydraContext.tsx +62 -0
  172. package/src/license/ProBadge.tsx +43 -0
  173. package/src/license/index.ts +7 -0
  174. package/src/license/tiers.ts +34 -0
  175. package/src/license/useHydraLicense.ts +10 -0
  176. package/src/license/validate.ts +90 -0
  177. package/src/license/withProGate.tsx +21 -0
  178. package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
  179. package/src/modules/AssignmentModule/types.ts +5 -1
  180. package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
  181. package/src/modules/CertificateModule/types.ts +6 -4
  182. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  183. package/src/modules/CourseCatalogModule/types.ts +47 -0
  184. package/src/modules/CoursePlayer/CoursePlayer.tsx +39 -22
  185. package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
  186. package/src/modules/ExamModule/ExamModule.tsx +64 -198
  187. package/src/modules/ExamModule/types.ts +5 -14
  188. package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
  189. package/src/modules/FlashcardLab/types.ts +2 -0
  190. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
  191. package/src/modules/GradeCenterModule/types.ts +2 -0
  192. package/src/modules/QuizModule/QuizModule.tsx +49 -169
  193. package/src/modules/QuizModule/types.ts +5 -15
  194. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  195. package/src/modules/StudentDashboardModule/types.ts +56 -0
  196. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  197. package/src/modules/StudentProfileModule/types.ts +45 -0
  198. package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
  199. package/src/modules/SurveyModule/types.ts +2 -0
  200. package/src/modules/_shared/assessment-intro.tsx +75 -0
  201. package/src/modules/_shared/assessment-results.tsx +133 -0
  202. package/src/modules/_shared/types.ts +11 -0
  203. package/src/modules/_shared/use-timer.ts +49 -0
  204. package/src/modules/index.ts +9 -0
  205. package/src/progress/achievement-badge.tsx +3 -3
  206. package/src/progress/grade-indicator.tsx +9 -1
  207. package/src/progress/progress-ring.tsx +2 -1
  208. package/src/progress/stat-card.tsx +14 -2
  209. package/src/progress/types.ts +2 -0
  210. package/src/provider/HydraProvider.tsx +15 -6
  211. package/src/questions/choice.tsx +13 -6
  212. package/src/questions/confidence-indicator.tsx +107 -0
  213. package/src/questions/essay.tsx +6 -4
  214. package/src/questions/fill-in-the-blank.tsx +8 -4
  215. package/src/questions/hotspot.tsx +4 -4
  216. package/src/questions/index.ts +2 -0
  217. package/src/questions/inline-choice.tsx +5 -4
  218. package/src/questions/matching.tsx +5 -4
  219. package/src/questions/multiple-choice.tsx +13 -6
  220. package/src/questions/numeric.tsx +8 -4
  221. package/src/questions/ordering.tsx +12 -4
  222. package/src/questions/question-renderer.tsx +3 -2
  223. package/src/questions/scenario.tsx +4 -4
  224. package/src/questions/spreadsheet.tsx +5 -4
  225. package/src/questions/true-false.tsx +13 -6
  226. package/src/sections/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
  227. package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
  228. package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
  229. package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
  230. package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
  231. package/src/sections/AdaptiveLearningPath/types.ts +159 -0
  232. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
  233. package/src/sections/AnnouncementFeed/types.ts +15 -1
  234. package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
  235. package/src/sections/AssessmentReview/types.ts +6 -0
  236. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
  237. package/src/sections/AssignmentSubmission/types.ts +6 -0
  238. package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
  239. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  240. package/src/sections/CertificateViewer/types.ts +6 -0
  241. package/src/sections/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
  242. package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
  243. package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
  244. package/src/sections/ContentAuthoringStudio/types.ts +67 -0
  245. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  246. package/src/sections/CourseCatalog/types.ts +76 -0
  247. package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
  248. package/src/sections/CourseOutline/types.ts +6 -0
  249. package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
  250. package/src/sections/DiscussionThread/types.ts +6 -0
  251. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  252. package/src/sections/EnrollmentWizard/types.ts +65 -0
  253. package/src/sections/ExamSession/ExamSession.tsx +100 -94
  254. package/src/sections/ExamSession/types.ts +6 -0
  255. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  256. package/src/sections/FlashcardStudySession/types.ts +6 -0
  257. package/src/sections/ForumBoard/ForumBoard.tsx +67 -7
  258. package/src/sections/ForumBoard/types.ts +14 -0
  259. package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
  260. package/src/sections/GradebookTable/types.ts +14 -0
  261. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  262. package/src/sections/LecturePlayer/types.ts +8 -0
  263. package/src/sections/LessonPage/LessonPage.tsx +34 -6
  264. package/src/sections/LessonPage/types.ts +6 -0
  265. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
  266. package/src/sections/PracticeQuiz/types.ts +6 -0
  267. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
  268. package/src/sections/ProgressDashboard/types.ts +6 -0
  269. package/src/sections/QuizSession/QuizSession.tsx +71 -82
  270. package/src/sections/QuizSession/types.ts +6 -0
  271. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
  272. package/src/sections/RequirementsChecklist/types.ts +6 -0
  273. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
  274. package/src/sections/ResourceLibrary/types.ts +15 -1
  275. package/src/sections/RubricView/RubricView.tsx +37 -1
  276. package/src/sections/RubricView/types.ts +6 -0
  277. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
  278. package/src/sections/ScrollableQuiz/types.ts +6 -0
  279. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  280. package/src/sections/StudentProfile/types.ts +99 -0
  281. package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
  282. package/src/sections/SurveyForm/types.ts +6 -0
  283. package/src/sections/_shared/merge-answers.ts +22 -0
  284. package/src/sections/_shared/section-shell.tsx +64 -0
  285. package/src/sections/_shared/use-assessment-session.ts +125 -0
  286. package/src/sections/index.ts +40 -0
  287. package/src/social/user-avatar.tsx +9 -5
  288. package/src/styles/globals.css +39 -41
  289. package/src/ui/badge.tsx +8 -0
  290. package/src/ui/index.ts +2 -0
  291. package/src/ui/progress.tsx +4 -0
  292. package/src/ui/rich-text-editor.tsx +10 -0
  293. package/src/ui/rich-text-toolbar.tsx +2 -1
  294. package/src/ui/toast.tsx +170 -0
  295. package/src/utils/debounce.ts +8 -2
  296. package/src/utils/pick-palette-color.ts +33 -0
  297. package/src/video/types.ts +16 -0
  298. package/src/video/video-player.tsx +27 -6
  299. package/dist/ForumBoard-CHXU3mjC.js +0 -2207
  300. package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
  301. package/dist/tabs-DRM2Iq_J.cjs +0 -172
  302. package/dist/tabs-Wf3h_Cx3.js +0 -21580
@@ -1,4 +1,4 @@
1
- import { useMemo, useRef } from "react";
1
+ import { useMemo, useRef, memo } from "react";
2
2
  import { Alert, AlertDescription } from "../ui/alert";
3
3
  import { Separator } from "../ui/separator";
4
4
  import { cn } from "../lib/utils";
@@ -26,14 +26,14 @@ import type { QuestionProps, SessionAnswer } from "./types";
26
26
  * onAnswer={(answers) => handleAnswer(answers)}
27
27
  * />
28
28
  */
29
- export const Scenario = ({
29
+ export const Scenario = memo(function Scenario({
30
30
  question,
31
31
  sessionAnswers,
32
32
  onAnswer,
33
33
  readOnly = false,
34
34
  showCorrectAnswers = false,
35
35
  disabled = false,
36
- }: QuestionProps) => {
36
+ }: QuestionProps) {
37
37
  const subQuestions = question.scenarioQuestions ?? [];
38
38
 
39
39
  // Track the latest answers from each sub-question in a ref so
@@ -137,4 +137,4 @@ export const Scenario = ({
137
137
  )}
138
138
  </div>
139
139
  );
140
- };
140
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useRef } from "react";
1
+ import { useState, useMemo, useRef, useEffect, memo } from "react";
2
2
  import { debounce } from "../utils/debounce";
3
3
  import { Input } from "../ui/input";
4
4
  import {
@@ -66,14 +66,14 @@ function getCellStatus(
66
66
  * onAnswer={(answers) => handleAnswer(answers)}
67
67
  * />
68
68
  */
69
- export const Spreadsheet = ({
69
+ export const Spreadsheet = memo(function Spreadsheet({
70
70
  question,
71
71
  sessionAnswers,
72
72
  onAnswer,
73
73
  readOnly = false,
74
74
  showCorrectAnswers = false,
75
75
  disabled = false,
76
- }: QuestionProps) => {
76
+ }: QuestionProps) {
77
77
  const columns = question.spreadsheetColumns ?? [];
78
78
  const rows = question.spreadsheetRows ?? [];
79
79
 
@@ -104,6 +104,7 @@ export const Spreadsheet = ({
104
104
  }, 300),
105
105
  [],
106
106
  );
107
+ useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);
107
108
 
108
109
  const [values, setValues] = useState<Map<string, string>>(() => {
109
110
  const map = new Map<string, string>();
@@ -256,4 +257,4 @@ export const Spreadsheet = ({
256
257
  )}
257
258
  </div>
258
259
  );
259
- };
260
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, memo } from "react";
2
2
  import type { QuestionProps } from "./types";
3
3
  import { Alert, AlertDescription } from "../ui/alert";
4
4
  import { cn } from "../lib/utils";
@@ -12,14 +12,14 @@ import { cn } from "../lib/utils";
12
12
  * onAnswer={(answers) => handleAnswer(answers)}
13
13
  * />
14
14
  */
15
- export const TrueFalse = ({
15
+ export const TrueFalse = memo(function TrueFalse({
16
16
  question,
17
17
  sessionAnswers,
18
18
  onAnswer,
19
19
  readOnly = false,
20
20
  showCorrectAnswers = false,
21
21
  disabled = false,
22
- }: QuestionProps) => {
22
+ }: QuestionProps) {
23
23
  const [selectedAnswer, setSelectedAnswer] = useState<string>(
24
24
  () => sessionAnswers?.[0]?.answerUid || "",
25
25
  );
@@ -53,7 +53,8 @@ export const TrueFalse = ({
53
53
  <div className="flex flex-col gap-4">
54
54
  <div dangerouslySetInnerHTML={{ __html: question.content }} />
55
55
 
56
- <div className="flex flex-col gap-2">
56
+ <fieldset className="flex flex-col gap-2 border-0 p-0 m-0">
57
+ <legend className="sr-only">True or False</legend>
57
58
  {sortedAnswers.map((answer) => (
58
59
  <div
59
60
  key={answer.uid}
@@ -70,10 +71,16 @@ export const TrueFalse = ({
70
71
  className="accent-primary m-0 shrink-0"
71
72
  />
72
73
  <span dangerouslySetInnerHTML={{ __html: answer.content }} />
74
+ {showCorrectAnswers && answer.isCorrect && (
75
+ <span className="sr-only">(Correct answer)</span>
76
+ )}
77
+ {showCorrectAnswers && selectedAnswer === answer.uid && !answer.isCorrect && (
78
+ <span className="sr-only">(Your answer — incorrect)</span>
79
+ )}
73
80
  </label>
74
81
  </div>
75
82
  ))}
76
- </div>
83
+ </fieldset>
77
84
 
78
85
  {showCorrectAnswers && question.explanation && (
79
86
  <Alert className="mt-2">
@@ -85,4 +92,4 @@ export const TrueFalse = ({
85
92
  )}
86
93
  </div>
87
94
  );
88
- };
95
+ });
@@ -0,0 +1,251 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ Target,
4
+ BookOpen,
5
+ Clock,
6
+ TrendingUp,
7
+ Flame,
8
+ } from "lucide-react";
9
+ import { StatCard } from "../../progress/stat-card";
10
+ import { ProgressRing } from "../../progress/progress-ring";
11
+ import { Badge } from "../../ui/badge";
12
+ import { Separator } from "../../ui/separator";
13
+ import { Skeleton } from "../../ui/skeleton";
14
+ import { SectionShell } from "../_shared/section-shell";
15
+ import { withProGate } from "../../license/withProGate";
16
+ import { formatDuration } from "../../utils/format-duration";
17
+ import type { AdaptiveLearningPathProps, PathMilestone } from "./types";
18
+ import { PathNodeCard } from "./path-node-card";
19
+ import { PathConnector } from "./path-connector";
20
+ import { PathMilestoneMarker } from "./path-milestone-marker";
21
+ import { PathSkillBar } from "./path-skill-bar";
22
+
23
+ const DIFFICULTY_CONFIG = {
24
+ beginner: { label: "Beginner", variant: "success" as const },
25
+ intermediate: { label: "Intermediate", variant: "warning" as const },
26
+ advanced: { label: "Advanced", variant: "destructive" as const },
27
+ };
28
+
29
+ /**
30
+ * AdaptiveLearningPath section — a premium visualization of a personalized
31
+ * learning sequence that adapts based on learner performance.
32
+ *
33
+ * Renders a vertical path map with nodes, connectors, milestones, skill bars,
34
+ * and summary statistics. Each node represents a learning activity with status,
35
+ * score, and recommended-next indicators.
36
+ *
37
+ * @example
38
+ * <AdaptiveLearningPath
39
+ * title="React Mastery Path"
40
+ * description="A personalized journey for mastering React"
41
+ * nodes={pathNodes}
42
+ * progress={pathProgress}
43
+ * onNodeClick={(uid) => navigate(`/activity/${uid}`)}
44
+ * onNodeStart={(uid) => startActivity(uid)}
45
+ * />
46
+ */
47
+ function AdaptiveLearningPathBase({
48
+ title,
49
+ description,
50
+ difficulty,
51
+ nodes = [],
52
+ progress,
53
+ skills,
54
+ milestones,
55
+ onNodeClick,
56
+ onNodeStart,
57
+ onSkillClick,
58
+ readOnly = false,
59
+ isLoading,
60
+ error,
61
+ onRetry,
62
+ className,
63
+ style,
64
+ }: AdaptiveLearningPathProps) {
65
+ const milestoneMap = useMemo(() => {
66
+ const map = new Map<number, PathMilestone>();
67
+ if (milestones) {
68
+ for (const m of milestones) {
69
+ map.set(m.afterNodeIndex, m);
70
+ }
71
+ }
72
+ return map;
73
+ }, [milestones]);
74
+
75
+ const safeNodes = Array.isArray(nodes) ? nodes : [];
76
+ const safeSkills = Array.isArray(skills) ? skills : [];
77
+
78
+ return (
79
+ <SectionShell
80
+ isLoading={isLoading}
81
+ error={error}
82
+ onRetry={onRetry}
83
+ className={className}
84
+ style={style}
85
+ skeleton={
86
+ <>
87
+ <Skeleton className="h-8 w-64" />
88
+ <Skeleton className="h-4 w-96 mt-2" />
89
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-6">
90
+ {Array.from({ length: 4 }, (_, i) => (
91
+ <Skeleton key={i} className="h-28" />
92
+ ))}
93
+ </div>
94
+ <div className="mt-8 space-y-3">
95
+ {Array.from({ length: 5 }, (_, i) => (
96
+ <Skeleton key={i} className="h-20" />
97
+ ))}
98
+ </div>
99
+ </>
100
+ }
101
+ >
102
+ <div className={className} style={style}>
103
+ {/* Header */}
104
+ <div className="flex items-start justify-between gap-4 flex-wrap">
105
+ <div className="flex items-center gap-4">
106
+ <ProgressRing
107
+ value={progress.completionPercentage}
108
+ size={64}
109
+ strokeWidth={5}
110
+ className="shrink-0 text-primary"
111
+ />
112
+ <div>
113
+ <h2 className="text-xl font-bold text-foreground">{title}</h2>
114
+ {description && (
115
+ <p className="text-sm text-muted-foreground mt-0.5">
116
+ {description}
117
+ </p>
118
+ )}
119
+ </div>
120
+ </div>
121
+ {difficulty && (
122
+ <Badge variant={DIFFICULTY_CONFIG[difficulty].variant}>
123
+ {DIFFICULTY_CONFIG[difficulty].label}
124
+ </Badge>
125
+ )}
126
+ </div>
127
+
128
+ {/* Stats row */}
129
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-6">
130
+ <StatCard
131
+ icon={<Target />}
132
+ label="Progress"
133
+ value={`${Math.round(progress.completionPercentage)}%`}
134
+ subtitle="of path completed"
135
+ accent="var(--primary)"
136
+ />
137
+ <StatCard
138
+ icon={<BookOpen />}
139
+ label="Completed"
140
+ value={`${progress.completedNodes}/${progress.totalNodes}`}
141
+ subtitle="activities"
142
+ accent="var(--success)"
143
+ />
144
+ <StatCard
145
+ icon={<Clock />}
146
+ label="Time Spent"
147
+ value={formatDuration(progress.totalTimeSpent)}
148
+ subtitle="total"
149
+ accent="var(--info)"
150
+ />
151
+ {progress.averageScore != null ? (
152
+ <StatCard
153
+ icon={<TrendingUp />}
154
+ label="Avg Score"
155
+ value={`${Math.round(progress.averageScore)}%`}
156
+ subtitle="across activities"
157
+ accent="var(--warning)"
158
+ />
159
+ ) : progress.currentStreak != null ? (
160
+ <StatCard
161
+ icon={<Flame />}
162
+ label="Streak"
163
+ value={`${progress.currentStreak}`}
164
+ subtitle="days"
165
+ accent="var(--warning)"
166
+ />
167
+ ) : (
168
+ <StatCard
169
+ icon={<TrendingUp />}
170
+ label="Remaining"
171
+ value={`${progress.totalNodes - progress.completedNodes}`}
172
+ subtitle="activities left"
173
+ accent="var(--warning)"
174
+ />
175
+ )}
176
+ </div>
177
+
178
+ {/* Skills */}
179
+ {safeSkills.length > 0 && (
180
+ <>
181
+ <Separator className="my-5" />
182
+ <div>
183
+ <h3 className="text-sm font-semibold text-foreground mb-3">
184
+ Skills
185
+ </h3>
186
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2">
187
+ {safeSkills.map((skill) => (
188
+ <PathSkillBar
189
+ key={skill.uid}
190
+ skill={skill}
191
+ onClick={onSkillClick}
192
+ />
193
+ ))}
194
+ </div>
195
+ </div>
196
+ </>
197
+ )}
198
+
199
+ {/* Path visualization */}
200
+ <Separator className="my-5" />
201
+ <div
202
+ className="relative"
203
+ role="list"
204
+ aria-label={`Learning path: ${progress.completedNodes} of ${progress.totalNodes} completed`}
205
+ >
206
+ {safeNodes.map((node, index) => (
207
+ <div key={node.uid} role="listitem">
208
+ {/* Connector (between nodes) */}
209
+ {index > 0 && (
210
+ <PathConnector fromStatus={safeNodes[index - 1].status} />
211
+ )}
212
+
213
+ {/* Milestone marker (appears before node, after previous connector) */}
214
+ {milestoneMap.has(index - 1) && (
215
+ <PathMilestoneMarker
216
+ milestone={milestoneMap.get(index - 1)!}
217
+ className="my-1"
218
+ />
219
+ )}
220
+
221
+ {/* Node card */}
222
+ <PathNodeCard
223
+ node={node}
224
+ onClick={onNodeClick}
225
+ onStart={onNodeStart}
226
+ readOnly={readOnly}
227
+ />
228
+ </div>
229
+ ))}
230
+
231
+ {/* Final milestone (after last node) */}
232
+ {safeNodes.length > 0 &&
233
+ milestoneMap.has(safeNodes.length - 1) && (
234
+ <>
235
+ <PathConnector fromStatus={safeNodes[safeNodes.length - 1].status} />
236
+ <PathMilestoneMarker
237
+ milestone={milestoneMap.get(safeNodes.length - 1)!}
238
+ className="my-1"
239
+ />
240
+ </>
241
+ )}
242
+ </div>
243
+ </div>
244
+ </SectionShell>
245
+ );
246
+ }
247
+
248
+ export const AdaptiveLearningPath = withProGate(
249
+ AdaptiveLearningPathBase,
250
+ "AdaptiveLearningPath",
251
+ );
@@ -0,0 +1,27 @@
1
+ import { cn } from "../../lib/utils";
2
+ import type { PathNodeStatus } from "./types";
3
+
4
+ interface PathConnectorProps {
5
+ /** Status of the node above this connector */
6
+ fromStatus: PathNodeStatus;
7
+ className?: string;
8
+ }
9
+
10
+ export function PathConnector({ fromStatus, className }: PathConnectorProps) {
11
+ const isDone = fromStatus === "mastered" || fromStatus === "completed";
12
+ const isActive = fromStatus === "in_progress";
13
+
14
+ return (
15
+ <div className={cn("flex justify-center", className)}>
16
+ <div
17
+ className={cn(
18
+ "w-0.5 h-8",
19
+ isDone && "bg-success",
20
+ isActive && "bg-primary",
21
+ !isDone && !isActive && "bg-border",
22
+ fromStatus === "skipped" && "border-l border-dashed border-muted-foreground bg-transparent",
23
+ )}
24
+ />
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,50 @@
1
+ import { Trophy, Lock } from "lucide-react";
2
+ import { cn } from "../../lib/utils";
3
+ import type { PathMilestone } from "./types";
4
+
5
+ const VARIANT_COLORS = {
6
+ default: "text-primary",
7
+ gold: "text-palette-3",
8
+ silver: "text-muted-foreground",
9
+ bronze: "text-palette-3/70",
10
+ } as const;
11
+
12
+ interface PathMilestoneMarkerProps {
13
+ milestone: PathMilestone;
14
+ className?: string;
15
+ }
16
+
17
+ export function PathMilestoneMarker({
18
+ milestone,
19
+ className,
20
+ }: PathMilestoneMarkerProps) {
21
+ const variant = milestone.variant ?? "default";
22
+
23
+ return (
24
+ <div
25
+ className={cn(
26
+ "flex items-center gap-3 py-2 px-4",
27
+ !milestone.reached && "opacity-50",
28
+ className,
29
+ )}
30
+ >
31
+ <div className="flex-1 h-px bg-border" />
32
+ <div className="flex items-center gap-2">
33
+ {milestone.reached ? (
34
+ <Trophy size={16} className={VARIANT_COLORS[variant]} />
35
+ ) : (
36
+ <Lock size={14} className="text-muted-foreground" />
37
+ )}
38
+ <span
39
+ className={cn(
40
+ "text-xs font-semibold whitespace-nowrap",
41
+ milestone.reached ? "text-foreground" : "text-muted-foreground",
42
+ )}
43
+ >
44
+ {milestone.title}
45
+ </span>
46
+ </div>
47
+ <div className="flex-1 h-px bg-border" />
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,166 @@
1
+ import { memo, useCallback } from "react";
2
+ import {
3
+ Clock,
4
+ Lock,
5
+ CheckCircle2,
6
+ ShieldCheck,
7
+ SkipForward,
8
+ Sparkles,
9
+ Play,
10
+ } from "lucide-react";
11
+ import { Card, CardContent } from "../../ui/card";
12
+ import { Badge } from "../../ui/badge";
13
+ import { Button } from "../../ui/button";
14
+ import { LearningObjectIcon } from "../../curriculum/learning-object-icon";
15
+ import { formatDuration } from "../../utils/format-duration";
16
+ import { cn } from "../../lib/utils";
17
+ import type { PathNode, PathNodeStatus } from "./types";
18
+
19
+ interface PathNodeCardProps {
20
+ node: PathNode;
21
+ onClick?: (uid: string) => void;
22
+ onStart?: (uid: string) => void;
23
+ readOnly?: boolean;
24
+ className?: string;
25
+ }
26
+
27
+ const STATUS_CONFIG: Record<
28
+ PathNodeStatus,
29
+ { label: string; variant: "success" | "destructive" | "warning" | "muted" | "info" | "default" | "secondary"; icon?: React.ElementType }
30
+ > = {
31
+ mastered: { label: "Mastered", variant: "success", icon: ShieldCheck },
32
+ completed: { label: "Completed", variant: "success", icon: CheckCircle2 },
33
+ in_progress: { label: "In Progress", variant: "info", icon: Play },
34
+ available: { label: "Available", variant: "secondary" },
35
+ locked: { label: "Locked", variant: "muted", icon: Lock },
36
+ skipped: { label: "Skipped", variant: "muted", icon: SkipForward },
37
+ };
38
+
39
+ export const PathNodeCard = memo(function PathNodeCard({
40
+ node,
41
+ onClick,
42
+ onStart,
43
+ readOnly = false,
44
+ className,
45
+ }: PathNodeCardProps) {
46
+ const config = STATUS_CONFIG[node.status];
47
+ const StatusIcon = config.icon;
48
+ const isInteractive = node.status !== "locked" && !readOnly;
49
+ const showStart =
50
+ !readOnly &&
51
+ (node.status === "available" || node.recommended) &&
52
+ node.status !== "locked" &&
53
+ node.status !== "completed" &&
54
+ node.status !== "mastered" &&
55
+ onStart;
56
+
57
+ const handleClick = useCallback(() => {
58
+ if (isInteractive && onClick) onClick(node.uid);
59
+ }, [isInteractive, onClick, node.uid]);
60
+
61
+ const handleKeyDown = useCallback(
62
+ (e: React.KeyboardEvent) => {
63
+ if (e.key === "Enter" || e.key === " ") {
64
+ e.preventDefault();
65
+ onClick?.(node.uid);
66
+ }
67
+ },
68
+ [onClick, node.uid],
69
+ );
70
+
71
+ const handleStart = useCallback(
72
+ (e: React.MouseEvent) => {
73
+ e.stopPropagation();
74
+ onStart?.(node.uid);
75
+ },
76
+ [onStart, node.uid],
77
+ );
78
+
79
+ return (
80
+ <Card
81
+ className={cn(
82
+ "transition-all",
83
+ isInteractive && onClick && "cursor-pointer hover:border-primary/50 hover:shadow-md",
84
+ node.status === "in_progress" && "border-primary ring-1 ring-primary/20",
85
+ node.status === "locked" && "opacity-60",
86
+ node.status === "skipped" && "opacity-50",
87
+ node.recommended && node.status !== "locked" && "border-primary/40",
88
+ className,
89
+ )}
90
+ onClick={isInteractive && onClick ? handleClick : undefined}
91
+ role={isInteractive && onClick ? "button" : undefined}
92
+ tabIndex={isInteractive && onClick ? 0 : undefined}
93
+ onKeyDown={isInteractive && onClick ? handleKeyDown : undefined}
94
+ >
95
+ <CardContent className="p-4">
96
+ <div className="flex items-start gap-3">
97
+ {/* Icon */}
98
+ <div
99
+ className={cn(
100
+ "shrink-0 w-9 h-9 rounded-lg flex items-center justify-center",
101
+ node.status === "locked"
102
+ ? "bg-muted text-muted-foreground"
103
+ : "bg-primary/10 text-primary",
104
+ )}
105
+ >
106
+ {node.icon ?? <LearningObjectIcon type={node.type} size={18} />}
107
+ </div>
108
+
109
+ {/* Content */}
110
+ <div className="flex-1 min-w-0">
111
+ <div className="flex items-center gap-2 flex-wrap">
112
+ <h4 className="text-sm font-semibold text-foreground truncate">
113
+ {node.title}
114
+ </h4>
115
+ {node.recommended && node.status !== "locked" && (
116
+ <Badge variant="default" className="text-[10px] px-1.5 h-5 gap-0.5">
117
+ <Sparkles size={10} />
118
+ Recommended
119
+ </Badge>
120
+ )}
121
+ </div>
122
+
123
+ {node.description && (
124
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">
125
+ {node.description}
126
+ </p>
127
+ )}
128
+
129
+ {/* Meta row */}
130
+ <div className="flex items-center gap-3 mt-2 flex-wrap">
131
+ <Badge variant={config.variant} className="text-[10px] px-1.5 h-5 gap-0.5">
132
+ {StatusIcon && <StatusIcon size={10} />}
133
+ {config.label}
134
+ </Badge>
135
+
136
+ {node.estimatedDuration != null && node.estimatedDuration > 0 && (
137
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
138
+ <Clock size={12} />
139
+ {formatDuration(node.estimatedDuration)}
140
+ </span>
141
+ )}
142
+
143
+ {node.score != null && (
144
+ <span className="text-xs font-medium tabular-nums text-foreground">
145
+ {node.score}%
146
+ </span>
147
+ )}
148
+ </div>
149
+ </div>
150
+
151
+ {/* Action */}
152
+ {showStart && (
153
+ <Button
154
+ size="sm"
155
+ variant={node.recommended ? "default" : "outline"}
156
+ className="shrink-0"
157
+ onClick={handleStart}
158
+ >
159
+ {node.status === "in_progress" ? "Continue" : "Start"}
160
+ </Button>
161
+ )}
162
+ </div>
163
+ </CardContent>
164
+ </Card>
165
+ );
166
+ });
@@ -0,0 +1,49 @@
1
+ import { memo } from "react";
2
+ import { Progress } from "../../ui/progress";
3
+ import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
4
+ import { cn } from "../../lib/utils";
5
+ import type { PathSkill } from "./types";
6
+
7
+ interface PathSkillBarProps {
8
+ skill: PathSkill;
9
+ onClick?: (uid: string) => void;
10
+ className?: string;
11
+ }
12
+
13
+ export const PathSkillBar = memo(function PathSkillBar({ skill, onClick, className }: PathSkillBarProps) {
14
+ const target = skill.targetProficiency ?? 100;
15
+
16
+ return (
17
+ <Tooltip>
18
+ <TooltipTrigger>
19
+ <button
20
+ type="button"
21
+ onClick={onClick ? () => onClick(skill.uid) : undefined}
22
+ disabled={!onClick}
23
+ className={cn(
24
+ "flex items-center gap-2 min-w-0 text-left",
25
+ onClick && "cursor-pointer hover:opacity-80",
26
+ !onClick && "cursor-default",
27
+ className,
28
+ )}
29
+ >
30
+ <span className="text-xs font-medium text-foreground whitespace-nowrap truncate min-w-16">
31
+ {skill.name}
32
+ </span>
33
+ <Progress
34
+ value={skill.proficiency}
35
+ max={target}
36
+ size="sm"
37
+ className="flex-1 min-w-20"
38
+ />
39
+ <span className="text-xs tabular-nums text-muted-foreground whitespace-nowrap">
40
+ {skill.proficiency}%
41
+ </span>
42
+ </button>
43
+ </TooltipTrigger>
44
+ <TooltipContent>
45
+ {skill.name}: {skill.proficiency}%{target < 100 ? ` / ${target}% target` : ""}
46
+ </TooltipContent>
47
+ </Tooltip>
48
+ );
49
+ });