@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,28 +1,16 @@
1
- import { useState, useRef, useEffect } from "react";
2
- import {
3
- ShieldCheck,
4
- RotateCcw,
5
- Clock,
6
- HelpCircle,
7
- CheckCircle2,
8
- XCircle,
9
- Trophy,
10
- Play,
11
- } from "lucide-react";
1
+ import { useState, useRef, useEffect, useCallback } from "react";
2
+ import { ShieldCheck } from "lucide-react";
12
3
  import { ExamSession } from "../../sections/ExamSession/ExamSession";
13
- import { AssessmentReview } from "../../sections/AssessmentReview/AssessmentReview";
14
- import { ProgressRing } from "../../progress/progress-ring";
15
- import { StatCard } from "../../progress/stat-card";
16
- import { Button } from "../../ui/button";
17
4
  import { Badge } from "../../ui/badge";
18
- import { Card, CardContent } from "../../ui/card";
19
5
  import { Alert, AlertDescription } from "../../ui/alert";
20
- import { Separator } from "../../ui/separator";
21
- import { formatDuration } from "../../utils/format-duration";
22
6
  import { cn } from "../../lib/utils";
23
7
  import type { SessionAnswer } from "../../questions/types";
24
8
  import type { ExamSubmitMetadata } from "../../sections/ExamSession/types";
25
9
  import { scoreAssessment } from "../../questions/scoring";
10
+ import { useTimer } from "../_shared/use-timer";
11
+ import { AssessmentIntro } from "../_shared/assessment-intro";
12
+ import { AssessmentResults } from "../_shared/assessment-results";
13
+ import { withProGate } from "../../license/withProGate";
26
14
  import type { ExamModuleProps, ExamModuleResult } from "./types";
27
15
 
28
16
  type InternalStep =
@@ -36,11 +24,11 @@ type InternalStep =
36
24
  * Steps: Intro (rules/instructions) → Exam (timed ExamSession) → Results (score + review).
37
25
  * Manages an external timer that feeds elapsed time to ExamSession.
38
26
  */
39
- export function ExamModule({
27
+ function ExamModuleBase({
40
28
  title,
41
29
  description,
42
30
  instructions,
43
- questions,
31
+ questions = [],
44
32
  timeLimitSeconds,
45
33
  passingScore,
46
34
  allowBackNavigation = true,
@@ -49,45 +37,24 @@ export function ExamModule({
49
37
  allowRetake = false,
50
38
  showReview = true,
51
39
  onComplete,
40
+ readOnly = false,
52
41
  className,
53
42
  style,
54
43
  }: ExamModuleProps) {
55
44
  const [step, setStep] = useState<InternalStep>({ tag: "intro" });
56
- const [timeElapsed, setTimeElapsed] = useState(0);
57
- const startTimeRef = useRef<number | null>(null);
58
- const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
59
45
  const contentRef = useRef<HTMLDivElement>(null);
46
+ const { timeElapsed, reset: resetTimer } = useTimer(step.tag === "exam");
60
47
 
61
- useEffect(() => {
62
- contentRef.current?.focus({ preventScroll: true });
63
- }, [step.tag]);
48
+ const onCompleteRef = useRef(onComplete);
49
+ onCompleteRef.current = onComplete;
64
50
 
65
- // Timer for exam step
66
51
  useEffect(() => {
67
- if (step.tag === "exam") {
68
- startTimeRef.current = Date.now();
69
- intervalRef.current = setInterval(() => {
70
- if (startTimeRef.current) {
71
- setTimeElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000));
72
- }
73
- }, 1000);
74
- } else {
75
- if (intervalRef.current) {
76
- clearInterval(intervalRef.current);
77
- intervalRef.current = null;
78
- }
79
- }
80
- return () => {
81
- if (intervalRef.current) clearInterval(intervalRef.current);
82
- };
52
+ contentRef.current?.focus({ preventScroll: true });
83
53
  }, [step.tag]);
84
54
 
85
- function scoreAnswers(
86
- answers: SessionAnswer[],
87
- metadata: ExamSubmitMetadata
88
- ): ExamModuleResult {
55
+ const handleSubmit = useCallback((answers: SessionAnswer[], metadata: ExamSubmitMetadata) => {
89
56
  const { correct, total, percentage } = scoreAssessment(questions, answers);
90
- return {
57
+ const result: ExamModuleResult = {
91
58
  answers,
92
59
  correct,
93
60
  total,
@@ -96,72 +63,36 @@ export function ExamModule({
96
63
  timeElapsedSeconds: metadata.timeElapsedSeconds,
97
64
  wasAutoSubmitted: metadata.wasAutoSubmitted,
98
65
  };
99
- }
100
-
101
- function handleSubmit(answers: SessionAnswer[], metadata: ExamSubmitMetadata) {
102
- const result = scoreAnswers(answers, metadata);
103
66
  setStep({ tag: "results", result });
104
- onComplete?.(result);
105
- }
67
+ onCompleteRef.current?.(result);
68
+ }, [questions, passingScore]);
106
69
 
107
- function handleRetake() {
108
- setTimeElapsed(0);
109
- startTimeRef.current = null;
70
+ const handleRetake = useCallback(() => {
71
+ resetTimer();
110
72
  setStep({ tag: "intro" });
111
- }
73
+ }, [resetTimer]);
112
74
 
113
75
  // ─── Intro Screen ───
114
76
  if (step.tag === "intro") {
115
77
  return (
116
- <div
117
- ref={contentRef}
118
- tabIndex={-1}
119
- className={cn("max-w-2xl mx-auto outline-none", className)}
120
- style={style}
121
- >
122
- <Card>
123
- <CardContent className="pt-8 pb-8 text-center">
124
- <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
125
- <ShieldCheck className="size-7 text-primary" />
126
- </div>
127
- <h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
128
- {description && (
129
- <p className="text-muted-foreground mb-6 max-w-md mx-auto">
130
- {description}
131
- </p>
132
- )}
133
-
134
- {/* Instructions */}
135
- {instructions && (
136
- <Alert className="text-left mb-6">
137
- <AlertDescription>{instructions}</AlertDescription>
138
- </Alert>
139
- )}
140
-
141
- {/* Metadata chips */}
142
- <div className="flex flex-wrap justify-center gap-2 mb-8">
143
- <Badge variant="outline" className="gap-1.5">
144
- <HelpCircle className="size-3.5" />
145
- {questions.length} questions
146
- </Badge>
147
- <Badge variant="outline" className="gap-1.5">
148
- <Clock className="size-3.5" />
149
- {formatDuration(timeLimitSeconds)} time limit
150
- </Badge>
151
- {passingScore !== undefined && (
152
- <Badge variant="outline" className="gap-1.5">
153
- <CheckCircle2 className="size-3.5" />
154
- {passingScore}% to pass
155
- </Badge>
156
- )}
157
- </div>
158
-
159
- <Button size="lg" onClick={() => setStep({ tag: "exam" })}>
160
- <Play className="size-4 mr-2" />
161
- Begin Exam
162
- </Button>
163
- </CardContent>
164
- </Card>
78
+ <div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
79
+ <AssessmentIntro
80
+ icon={<ShieldCheck className="size-7 text-primary" />}
81
+ title={title}
82
+ description={description}
83
+ questionCount={questions.length}
84
+ timeLimitSeconds={timeLimitSeconds}
85
+ passingScore={passingScore}
86
+ startLabel="Begin Exam"
87
+ onStart={() => setStep({ tag: "exam" })}
88
+ readOnly={readOnly}
89
+ >
90
+ {instructions && (
91
+ <Alert className="text-left mb-6">
92
+ <AlertDescription>{instructions}</AlertDescription>
93
+ </Alert>
94
+ )}
95
+ </AssessmentIntro>
165
96
  </div>
166
97
  );
167
98
  }
@@ -169,12 +100,7 @@ export function ExamModule({
169
100
  // ─── Exam Screen ───
170
101
  if (step.tag === "exam") {
171
102
  return (
172
- <div
173
- ref={contentRef}
174
- tabIndex={-1}
175
- className={cn("outline-none", className)}
176
- style={style}
177
- >
103
+ <div ref={contentRef} tabIndex={-1} className={cn("outline-none", className)} style={style}>
178
104
  <ExamSession
179
105
  questions={questions}
180
106
  timeLimitSeconds={timeLimitSeconds}
@@ -185,6 +111,7 @@ export function ExamModule({
185
111
  confirmBeforeSubmit
186
112
  examTitle={title}
187
113
  onSubmit={handleSubmit}
114
+ readOnly={readOnly}
188
115
  />
189
116
  </div>
190
117
  );
@@ -192,94 +119,33 @@ export function ExamModule({
192
119
 
193
120
  // ─── Results Screen ───
194
121
  const { result } = step;
195
- const passed = result.passed;
196
122
 
197
123
  return (
198
- <div
199
- ref={contentRef}
200
- tabIndex={-1}
201
- className={cn("max-w-2xl mx-auto outline-none", className)}
202
- style={style}
203
- >
204
- <Card>
205
- <CardContent className="pt-8 pb-8">
206
- {/* Score summary */}
207
- <div className="text-center mb-8">
208
- <ProgressRing
209
- value={result.percentage}
210
- size={140}
211
- strokeWidth={10}
212
- color={passed ? "var(--success)" : "var(--destructive)"}
213
- className="mx-auto mb-4 text-foreground"
214
- />
215
- <Badge
216
- variant={passed ? "success" : "destructive"}
217
- className="text-sm px-3 py-1 mb-2"
218
- >
219
- {passed ? "Passed" : "Failed"}
124
+ <div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
125
+ <AssessmentResults
126
+ title={title}
127
+ percentage={result.percentage}
128
+ passed={result.passed}
129
+ correct={result.correct}
130
+ total={result.total}
131
+ timeElapsedSeconds={result.timeElapsedSeconds}
132
+ answers={result.answers}
133
+ questions={questions}
134
+ allowRetake={allowRetake}
135
+ onRetake={handleRetake}
136
+ retakeLabel="Retake Exam"
137
+ showReview={showReview}
138
+ readOnly={readOnly}
139
+ extraBadges={
140
+ result.wasAutoSubmitted ? (
141
+ <Badge variant="outline" className="text-sm px-3 py-1 mb-2 ml-2">
142
+ Auto-submitted
220
143
  </Badge>
221
- {result.wasAutoSubmitted && (
222
- <Badge variant="outline" className="text-sm px-3 py-1 mb-2 ml-2">
223
- Auto-submitted
224
- </Badge>
225
- )}
226
- <h2 className="text-xl font-bold text-foreground">{title}</h2>
227
- </div>
228
-
229
- {/* Stats grid */}
230
- <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
231
- <StatCard
232
- icon={<CheckCircle2 />}
233
- label="Correct"
234
- description="Questions answered right"
235
- value={`${result.correct}/${result.total}`}
236
- />
237
- <StatCard
238
- icon={<XCircle />}
239
- label="Incorrect"
240
- description="Questions to review"
241
- value={`${result.total - result.correct}/${result.total}`}
242
- />
243
- <StatCard
244
- icon={<Trophy />}
245
- label="Score"
246
- description="Overall percentage"
247
- value={`${result.percentage}%`}
248
- />
249
- <StatCard
250
- icon={<Clock />}
251
- label="Time"
252
- description="Total elapsed"
253
- value={formatDuration(result.timeElapsedSeconds)}
254
- />
255
- </div>
256
-
257
- {/* Actions */}
258
- {allowRetake && (
259
- <div className="flex justify-center mb-8">
260
- <Button variant="outline" onClick={handleRetake}>
261
- <RotateCcw className="size-4 mr-2" />
262
- Retake Exam
263
- </Button>
264
- </div>
265
- )}
266
- </CardContent>
267
- </Card>
268
-
269
- {/* Per-question review */}
270
- {showReview && (
271
- <>
272
- <Separator className="my-6" />
273
- <h3 className="text-lg font-semibold text-foreground mb-4">
274
- Question Review
275
- </h3>
276
- <AssessmentReview
277
- questions={questions}
278
- sessionAnswers={result.answers}
279
- showCorrectAnswers
280
- />
281
- </>
282
- )}
144
+ ) : null
145
+ }
146
+ />
283
147
  </div>
284
148
  );
285
149
  }
150
+
151
+ export const ExamModule = withProGate(ExamModuleBase, "ExamModule");
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
- import type { QuestionData, SessionAnswer } from "../../questions/types";
2
+ import type { QuestionData } from "../../questions/types";
3
+ import type { AssessmentResult } from "../_shared/types";
3
4
 
4
5
  /**
5
6
  * ExamModule — a formal timed exam experience with intro, exam, and results steps.
@@ -42,25 +43,15 @@ export interface ExamModuleProps {
42
43
  showReview?: boolean;
43
44
  /** Called when the exam is completed (submitted) */
44
45
  onComplete?: (result: ExamModuleResult) => void;
46
+ /** When true, disables interactions for preview/demo mode. @default false */
47
+ readOnly?: boolean;
45
48
  /** CSS class name for the root element */
46
49
  className?: string;
47
50
  /** Inline styles for the root element */
48
51
  style?: React.CSSProperties;
49
52
  }
50
53
 
51
- export interface ExamModuleResult {
52
- /** The user's submitted answers */
53
- answers: SessionAnswer[];
54
- /** Number of correct answers */
55
- correct: number;
56
- /** Total number of gradable questions */
57
- total: number;
58
- /** Score as a percentage (0-100) */
59
- percentage: number;
60
- /** Whether the user passed (only meaningful when passingScore is set) */
61
- passed: boolean;
62
- /** Total time taken in seconds */
63
- timeElapsedSeconds: number;
54
+ export interface ExamModuleResult extends AssessmentResult {
64
55
  /** Whether the submission was triggered by timeout */
65
56
  wasAutoSubmitted: boolean;
66
57
  }
@@ -17,6 +17,7 @@ import { Card, CardContent } from "../../ui/card";
17
17
  import { formatDuration } from "../../utils/format-duration";
18
18
  import { cn } from "../../lib/utils";
19
19
  import type { FlashcardData } from "../../flashcards/types";
20
+ import { withProGate } from "../../license/withProGate";
20
21
  import type {
21
22
  FlashcardLabProps,
22
23
  FlashcardLabResult,
@@ -27,12 +28,13 @@ type InternalStep =
27
28
  | { tag: "study"; cards: FlashcardData[]; deckUids: string[]; shuffled: boolean }
28
29
  | { tag: "completion"; result: FlashcardLabResult };
29
30
 
30
- export function FlashcardLab({
31
- decks,
31
+ function FlashcardLabBase({
32
+ decks = [],
32
33
  showShuffleToggle = true,
33
34
  defaultShuffled = false,
34
35
  allowMultiSelect = true,
35
36
  onComplete,
37
+ readOnly = false,
36
38
  className,
37
39
  style,
38
40
  }: FlashcardLabProps) {
@@ -187,7 +189,7 @@ export function FlashcardLab({
187
189
  </div>
188
190
  <Button
189
191
  onClick={startStudying}
190
- disabled={selectedUids.size === 0}
192
+ disabled={selectedUids.size === 0 || readOnly}
191
193
  >
192
194
  Start Studying
193
195
  </Button>
@@ -212,6 +214,7 @@ export function FlashcardLab({
212
214
  title={deckNames}
213
215
  shuffled={step.shuffled}
214
216
  onComplete={handleStudyComplete}
217
+ readOnly={readOnly}
215
218
  />
216
219
  </div>
217
220
  );
@@ -272,11 +275,11 @@ export function FlashcardLab({
272
275
  </div>
273
276
 
274
277
  <div className="flex justify-center gap-3">
275
- <Button variant="outline" onClick={handlePickNewDeck}>
278
+ <Button variant="outline" onClick={handlePickNewDeck} disabled={readOnly}>
276
279
  <ArrowLeft className="size-4 mr-2" />
277
280
  Pick New Deck
278
281
  </Button>
279
- <Button onClick={handleStudyAgain}>
282
+ <Button onClick={handleStudyAgain} disabled={readOnly}>
280
283
  <RotateCcw className="size-4 mr-2" />
281
284
  Study Again
282
285
  </Button>
@@ -286,3 +289,5 @@ export function FlashcardLab({
286
289
  </div>
287
290
  );
288
291
  }
292
+
293
+ export const FlashcardLab = withProGate(FlashcardLabBase, "FlashcardLab");
@@ -27,6 +27,8 @@ export interface FlashcardLabProps {
27
27
  allowMultiSelect?: boolean;
28
28
  /** Called when the user completes a study session */
29
29
  onComplete?: (result: FlashcardLabResult) => void;
30
+ /** When true, disables interactions for preview/demo mode. @default false */
31
+ readOnly?: boolean;
30
32
  /** CSS class name for the root element */
31
33
  className?: string;
32
34
  /** Inline styles for the root element */
@@ -10,6 +10,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../ui/tabs";
10
10
  import { Separator } from "../../ui/separator";
11
11
  import { cn } from "../../lib/utils";
12
12
  import type { GradeItem } from "../../sections/GradebookTable/types";
13
+ import { withProGate } from "../../license/withProGate";
13
14
  import type { GradeCenterModuleProps } from "./types";
14
15
 
15
16
  /**
@@ -19,14 +20,15 @@ import type { GradeCenterModuleProps } from "./types";
19
20
  * Uses a panel-based layout (like CoursePlayer) with tabs for Grades and Progress,
20
21
  * and a slide-in detail panel for reviewing individual assessments.
21
22
  */
22
- export function GradeCenterModule({
23
+ function GradeCenterModuleBase({
23
24
  courseTitle,
24
- gradeItems,
25
+ gradeItems = [],
25
26
  categories,
26
27
  overallGrade,
27
28
  showWeights,
28
29
  progressData,
29
30
  reviewData,
31
+ readOnly = false,
30
32
  className,
31
33
  style,
32
34
  }: GradeCenterModuleProps) {
@@ -147,6 +149,7 @@ export function GradeCenterModule({
147
149
  overallGrade={overallGrade}
148
150
  showWeights={showWeights}
149
151
  onItemClick={handleItemClick}
152
+ readOnly={readOnly}
150
153
  />
151
154
  </TabsContent>
152
155
 
@@ -167,3 +170,5 @@ export function GradeCenterModule({
167
170
  </div>
168
171
  );
169
172
  }
173
+
174
+ export const GradeCenterModule = withProGate(GradeCenterModuleBase, "GradeCenterModule");
@@ -56,6 +56,8 @@ export interface GradeCenterModuleProps {
56
56
  score?: AssessmentScore;
57
57
  }
58
58
  >;
59
+ /** When true, disables interactions for preview/demo mode. @default false */
60
+ readOnly?: boolean;
59
61
  /** CSS class name for the root element */
60
62
  className?: string;
61
63
  /** Inline styles for the root element */