@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
@@ -37,8 +37,6 @@ export interface AssessmentToolbarProps {
37
37
  questions?: QuestionNavigatorItem[];
38
38
  /** Called when the user navigates to a specific question via the navigator */
39
39
  onNavigateToQuestion?: (questionUid: string) => void;
40
- /** Called when the user flags or unflags a question */
41
- onToggleFlag?: (questionUid: string) => void;
42
40
  /** UID of the currently active question */
43
41
  currentQuestionUid?: string;
44
42
  /** Whether the assessment has been completed/submitted */
@@ -72,14 +70,12 @@ export interface TimerDisplayProps {
72
70
  * questions={questions}
73
71
  * currentQuestionUid={currentUid}
74
72
  * onNavigate={handleNavigate}
75
- * onToggleFlag={handleFlag}
76
73
  * />
77
74
  */
78
75
  export interface QuestionNavigatorProps {
79
76
  questions: QuestionNavigatorItem[];
80
77
  currentQuestionUid?: string;
81
78
  onNavigate?: (questionUid: string) => void;
82
- onToggleFlag?: (questionUid: string) => void;
83
79
  readOnly?: boolean;
84
80
  }
85
81
 
@@ -90,3 +86,57 @@ export interface QuestionNavigatorItem {
90
86
  isAnswered: boolean;
91
87
  isSkipped: boolean;
92
88
  }
89
+
90
+ /**
91
+ * QuestionHeaderBar displays question number, flag toggle, and optional
92
+ * materials button inside the question Card above the QuestionRenderer.
93
+ *
94
+ * @example
95
+ * <QuestionHeaderBar
96
+ * questionNumber={3}
97
+ * totalQuestions={10}
98
+ * isFlagged={false}
99
+ * onToggleFlag={() => toggleFlag(questionUid)}
100
+ * hasMaterials
101
+ * onOpenMaterials={() => setDrawerOpen(true)}
102
+ * />
103
+ */
104
+ export interface QuestionHeaderBarProps {
105
+ /** 1-based question number */
106
+ questionNumber: number;
107
+ /** Total number of questions */
108
+ totalQuestions: number;
109
+ /** Whether this question is currently flagged */
110
+ isFlagged: boolean;
111
+ /** Called when the user toggles the flag */
112
+ onToggleFlag?: () => void;
113
+ /** Whether the current question has related materials available */
114
+ hasMaterials?: boolean;
115
+ /** Called when the user clicks the materials button */
116
+ onOpenMaterials?: () => void;
117
+ /** When true, hides interactive elements */
118
+ readOnly?: boolean;
119
+ }
120
+
121
+ /**
122
+ * QuestionMaterialsDrawer renders a slide-in panel with content blocks
123
+ * linked to the current question for open-book reference.
124
+ *
125
+ * @example
126
+ * <QuestionMaterialsDrawer
127
+ * open={open}
128
+ * onOpenChange={setOpen}
129
+ * materials={currentMaterials}
130
+ * questionNumber={3}
131
+ * />
132
+ */
133
+ export interface QuestionMaterialsDrawerProps {
134
+ /** Whether the drawer is open */
135
+ open: boolean;
136
+ /** Called when the drawer should open or close */
137
+ onOpenChange: (open: boolean) => void;
138
+ /** One or more material groups to display in the drawer */
139
+ materials: import("../questions/types").QuestionMaterial[];
140
+ /** Question number for the title display */
141
+ questionNumber?: number;
142
+ }
@@ -0,0 +1,153 @@
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+
3
+ export interface UseCountdownOptions {
4
+ /** Total countdown duration in seconds */
5
+ durationSeconds: number;
6
+ /** Seconds remaining at which to trigger warning state. @default 60 */
7
+ warningThresholdSeconds?: number;
8
+ /** Called once when the countdown reaches zero */
9
+ onExpire?: () => void;
10
+ /** Called once when the countdown enters warning territory */
11
+ onWarning?: () => void;
12
+ /** Whether to start the countdown immediately. @default false */
13
+ autoStart?: boolean;
14
+ }
15
+
16
+ export interface UseCountdownReturn {
17
+ /** Seconds remaining in the countdown */
18
+ timeRemaining: number;
19
+ /** Whether the countdown is actively running */
20
+ isRunning: boolean;
21
+ /** Whether the countdown has been paused */
22
+ isPaused: boolean;
23
+ /** Whether the countdown has reached zero */
24
+ isExpired: boolean;
25
+ /** Whether the countdown is within the warning threshold */
26
+ isWarning: boolean;
27
+ /** Start or restart the countdown */
28
+ start: () => void;
29
+ /** Pause the countdown */
30
+ pause: () => void;
31
+ /** Resume after pausing */
32
+ resume: () => void;
33
+ /** Reset to the original duration, stopped */
34
+ reset: () => void;
35
+ }
36
+
37
+ type TimerState = "idle" | "running" | "paused" | "expired";
38
+
39
+ /**
40
+ * useCountdown manages a countdown timer lifecycle with warning and expiry callbacks.
41
+ *
42
+ * @example
43
+ * const { timeRemaining, isWarning, start } = useCountdown({
44
+ * durationSeconds: 300,
45
+ * warningThresholdSeconds: 60,
46
+ * onExpire: () => handleAutoSubmit(),
47
+ * });
48
+ */
49
+ export function useCountdown({
50
+ durationSeconds,
51
+ warningThresholdSeconds = 60,
52
+ onExpire,
53
+ onWarning,
54
+ autoStart = false,
55
+ }: UseCountdownOptions): UseCountdownReturn {
56
+ const [timeRemaining, setTimeRemaining] = useState(durationSeconds);
57
+ const stateRef = useRef<TimerState>(autoStart ? "running" : "idle");
58
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
59
+ const onExpireRef = useRef(onExpire);
60
+ const onWarningRef = useRef(onWarning);
61
+ const warningFiredRef = useRef(false);
62
+
63
+ // Keep callback refs current without recreating intervals
64
+ onExpireRef.current = onExpire;
65
+ onWarningRef.current = onWarning;
66
+
67
+ const clearTimer = useCallback(() => {
68
+ if (intervalRef.current != null) {
69
+ clearInterval(intervalRef.current);
70
+ intervalRef.current = null;
71
+ }
72
+ }, []);
73
+
74
+ const startInterval = useCallback(() => {
75
+ clearTimer();
76
+ intervalRef.current = setInterval(() => {
77
+ setTimeRemaining((prev) => {
78
+ const next = prev - 1;
79
+
80
+ // Warning check
81
+ if (
82
+ !warningFiredRef.current &&
83
+ next <= warningThresholdSeconds &&
84
+ next > 0
85
+ ) {
86
+ warningFiredRef.current = true;
87
+ onWarningRef.current?.();
88
+ }
89
+
90
+ // Expiry check
91
+ if (next <= 0) {
92
+ clearTimer();
93
+ stateRef.current = "expired";
94
+ onExpireRef.current?.();
95
+ return 0;
96
+ }
97
+
98
+ return next;
99
+ });
100
+ }, 1000);
101
+ }, [clearTimer, warningThresholdSeconds]);
102
+
103
+ const start = useCallback(() => {
104
+ setTimeRemaining(durationSeconds);
105
+ warningFiredRef.current = false;
106
+ stateRef.current = "running";
107
+ startInterval();
108
+ }, [durationSeconds, startInterval]);
109
+
110
+ const pause = useCallback(() => {
111
+ if (stateRef.current !== "running") return;
112
+ clearTimer();
113
+ stateRef.current = "paused";
114
+ // Force re-render so isPaused updates
115
+ setTimeRemaining((prev) => prev);
116
+ }, [clearTimer]);
117
+
118
+ const resume = useCallback(() => {
119
+ if (stateRef.current !== "paused") return;
120
+ stateRef.current = "running";
121
+ startInterval();
122
+ }, [startInterval]);
123
+
124
+ const reset = useCallback(() => {
125
+ clearTimer();
126
+ stateRef.current = "idle";
127
+ warningFiredRef.current = false;
128
+ setTimeRemaining(durationSeconds);
129
+ }, [clearTimer, durationSeconds]);
130
+
131
+ // Auto-start on mount if requested
132
+ useEffect(() => {
133
+ if (autoStart) {
134
+ startInterval();
135
+ }
136
+ return clearTimer;
137
+ }, [autoStart, startInterval, clearTimer]);
138
+
139
+ const state = stateRef.current;
140
+
141
+ return {
142
+ timeRemaining,
143
+ isRunning: state === "running",
144
+ isPaused: state === "paused",
145
+ isExpired: state === "expired",
146
+ isWarning:
147
+ timeRemaining <= warningThresholdSeconds && timeRemaining > 0,
148
+ start,
149
+ pause,
150
+ resume,
151
+ reset,
152
+ };
153
+ }
@@ -6,6 +6,7 @@ export function EmptyState({ icon, title, description, action, className, style
6
6
  <div
7
7
  className={cn("flex flex-col items-center justify-center text-center px-3 py-6", className)}
8
8
  style={style}
9
+ role="status"
9
10
  >
10
11
  {icon && (
11
12
  <div className="mb-2 text-muted-foreground [&>svg]:size-12">
@@ -3,10 +3,15 @@ export { ConfirmDialog } from "./confirm-dialog";
3
3
  export { SearchInput } from "./search-input";
4
4
  export { StatusBadge } from "./status-badge";
5
5
  export { DueDateDisplay } from "./due-date-display";
6
+ export { Stepper } from "./stepper";
7
+ export { Pagination } from "./pagination";
6
8
  export type {
7
9
  EmptyStateProps,
8
10
  ConfirmDialogProps,
9
11
  SearchInputProps,
10
12
  StatusBadgeProps,
11
13
  DueDateDisplayProps,
14
+ StepperProps,
15
+ StepDefinition,
16
+ PaginationProps,
12
17
  } from "./types";
@@ -0,0 +1,135 @@
1
+ import { memo, useMemo } from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+ import { Button } from "../ui/button";
5
+
6
+ /**
7
+ * Pagination provides page navigation controls with page numbers,
8
+ * previous/next buttons, and ellipsis for large page ranges.
9
+ *
10
+ * @example
11
+ * <Pagination
12
+ * currentPage={3}
13
+ * totalPages={10}
14
+ * onPageChange={(page) => setPage(page)}
15
+ * />
16
+ */
17
+ export interface PaginationProps {
18
+ /** Current active page (1-indexed) */
19
+ currentPage: number;
20
+ /** Total number of pages */
21
+ totalPages: number;
22
+ /** Called when the user navigates to a page */
23
+ onPageChange: (page: number) => void;
24
+ /** Number of page buttons shown on each side of the current page (default 1) */
25
+ siblingCount?: number;
26
+ /** CSS class name for the root element */
27
+ className?: string;
28
+ /** Inline styles for the root element */
29
+ style?: React.CSSProperties;
30
+ }
31
+
32
+ function getPageRange(
33
+ current: number,
34
+ total: number,
35
+ siblings: number,
36
+ ): (number | "ellipsis")[] {
37
+ const totalSlots = siblings * 2 + 5;
38
+ if (total <= totalSlots) {
39
+ return Array.from({ length: total }, (_, i) => i + 1);
40
+ }
41
+
42
+ const leftSibling = Math.max(current - siblings, 1);
43
+ const rightSibling = Math.min(current + siblings, total);
44
+ const showLeftEllipsis = leftSibling > 2;
45
+ const showRightEllipsis = rightSibling < total - 1;
46
+
47
+ if (!showLeftEllipsis && showRightEllipsis) {
48
+ const leftCount = siblings * 2 + 3;
49
+ const leftRange = Array.from({ length: leftCount }, (_, i) => i + 1);
50
+ return [...leftRange, "ellipsis", total];
51
+ }
52
+
53
+ if (showLeftEllipsis && !showRightEllipsis) {
54
+ const rightCount = siblings * 2 + 3;
55
+ const rightRange = Array.from(
56
+ { length: rightCount },
57
+ (_, i) => total - rightCount + i + 1,
58
+ );
59
+ return [1, "ellipsis", ...rightRange];
60
+ }
61
+
62
+ const middleRange = Array.from(
63
+ { length: rightSibling - leftSibling + 1 },
64
+ (_, i) => leftSibling + i,
65
+ );
66
+ return [1, "ellipsis", ...middleRange, "ellipsis", total];
67
+ }
68
+
69
+ export const Pagination = memo(function Pagination({
70
+ currentPage,
71
+ totalPages,
72
+ onPageChange,
73
+ siblingCount = 1,
74
+ className,
75
+ style,
76
+ }: PaginationProps) {
77
+ const pages = useMemo(
78
+ () => getPageRange(currentPage, totalPages, siblingCount),
79
+ [currentPage, totalPages, siblingCount],
80
+ );
81
+
82
+ if (totalPages <= 1) return null;
83
+
84
+ return (
85
+ <nav
86
+ data-slot="pagination"
87
+ aria-label="Pagination"
88
+ className={cn("flex items-center justify-center gap-1", className)}
89
+ style={style}
90
+ >
91
+ <Button
92
+ variant="ghost"
93
+ size="icon-sm"
94
+ onClick={() => onPageChange(currentPage - 1)}
95
+ disabled={currentPage <= 1}
96
+ aria-label="Previous page"
97
+ >
98
+ <ChevronLeft className="size-4" />
99
+ </Button>
100
+
101
+ {pages.map((page, i) =>
102
+ page === "ellipsis" ? (
103
+ <span
104
+ key={`ellipsis-${i}`}
105
+ className="flex items-center justify-center size-8 text-xs text-muted-foreground"
106
+ >
107
+ <span aria-hidden="true">...</span>
108
+ <span className="sr-only">More pages</span>
109
+ </span>
110
+ ) : (
111
+ <Button
112
+ key={page}
113
+ variant={page === currentPage ? "default" : "ghost"}
114
+ size="icon-sm"
115
+ onClick={() => onPageChange(page)}
116
+ aria-label={`Page ${page}`}
117
+ aria-current={page === currentPage ? "page" : undefined}
118
+ >
119
+ <span className="text-xs">{page}</span>
120
+ </Button>
121
+ ),
122
+ )}
123
+
124
+ <Button
125
+ variant="ghost"
126
+ size="icon-sm"
127
+ onClick={() => onPageChange(currentPage + 1)}
128
+ disabled={currentPage >= totalPages}
129
+ aria-label="Next page"
130
+ >
131
+ <ChevronRight className="size-4" />
132
+ </Button>
133
+ </nav>
134
+ );
135
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback } from "react";
1
+ import { useState, useMemo, useRef, useEffect } from "react";
2
2
  import { Search, X } from "lucide-react";
3
3
  import { debounce } from "../utils/debounce";
4
4
  import type { SearchInputProps } from "./types";
@@ -16,12 +16,14 @@ export function SearchInput({
16
16
  }: SearchInputProps) {
17
17
  const [localValue, setLocalValue] = useState(value);
18
18
 
19
- useEffect(() => {
20
- setLocalValue(value);
21
- }, [value]);
19
+ const onChangeRef = useRef(onChange);
20
+ onChangeRef.current = onChange;
22
21
 
23
- // eslint-disable-next-line react-hooks/exhaustive-deps
24
- const debouncedOnChange = useCallback(debounce(onChange, debounceMs), [onChange, debounceMs]);
22
+ const debouncedOnChange = useMemo(
23
+ () => debounce((val: string) => onChangeRef.current(val), debounceMs),
24
+ [debounceMs],
25
+ );
26
+ useEffect(() => () => debouncedOnChange.cancel(), [debouncedOnChange]);
25
27
 
26
28
  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
27
29
  const next = e.target.value;
@@ -0,0 +1,100 @@
1
+ import { Check } from "lucide-react";
2
+ import { cn } from "../lib/utils";
3
+ import type { StepperProps } from "./types";
4
+
5
+ /**
6
+ * Stepper renders a visual step indicator for multi-step flows,
7
+ * showing completed, current, and upcoming steps with labels.
8
+ */
9
+ export function Stepper({
10
+ steps,
11
+ currentStep,
12
+ orientation = "horizontal",
13
+ variant = "default",
14
+ className,
15
+ style,
16
+ }: StepperProps) {
17
+ const isHorizontal = orientation === "horizontal";
18
+ const isCompact = variant === "compact";
19
+
20
+ const circleSize = isCompact ? "w-6 h-6 text-xs" : "w-8 h-8 text-sm";
21
+ const iconSize = isCompact ? 12 : 16;
22
+
23
+ return (
24
+ <div
25
+ className={cn(
26
+ "flex",
27
+ isHorizontal ? "flex-row items-start" : "flex-col",
28
+ className,
29
+ )}
30
+ style={style}
31
+ >
32
+ {steps.map((step, index) => {
33
+ const isCompleted = index < currentStep;
34
+ const isCurrent = index === currentStep;
35
+
36
+ return (
37
+ <div key={index} className={cn("flex", isHorizontal ? "flex-1 flex-row items-start" : "flex-row items-start")}>
38
+ {/* Step indicator */}
39
+ <div className={cn("flex", isHorizontal ? "flex-col items-center" : "flex-row items-start gap-3")}>
40
+ {/* Circle */}
41
+ <div
42
+ className={cn(
43
+ "rounded-full flex items-center justify-center shrink-0 font-semibold transition-colors",
44
+ circleSize,
45
+ isCompleted && "bg-primary text-primary-foreground",
46
+ isCurrent && "border-2 border-primary bg-primary/10 text-primary",
47
+ !isCompleted && !isCurrent && "border-2 border-border bg-muted text-muted-foreground",
48
+ )}
49
+ >
50
+ {isCompleted ? (
51
+ <Check size={iconSize} />
52
+ ) : (
53
+ <span>{index + 1}</span>
54
+ )}
55
+ </div>
56
+
57
+ {/* Label */}
58
+ <div className={cn(isHorizontal ? "mt-1.5 text-center" : "pt-0.5")}>
59
+ <p
60
+ className={cn(
61
+ "text-sm font-medium leading-tight",
62
+ isCurrent ? "text-foreground" : "text-muted-foreground",
63
+ )}
64
+ >
65
+ {step.label}
66
+ </p>
67
+ {!isCompact && step.description && (
68
+ <p className="text-xs text-muted-foreground mt-0.5">
69
+ {step.description}
70
+ </p>
71
+ )}
72
+ </div>
73
+ </div>
74
+
75
+ {/* Connector line */}
76
+ {index < steps.length - 1 && (
77
+ isHorizontal ? (
78
+ <div
79
+ className={cn(
80
+ "flex-1 h-0.5 self-center mt-0 mx-2",
81
+ isCompact ? "mt-3" : "mt-4",
82
+ isCompleted ? "bg-primary" : "bg-border",
83
+ )}
84
+ />
85
+ ) : (
86
+ <div
87
+ className={cn(
88
+ "w-0.5 h-6 ml-3.5",
89
+ isCompact && "ml-2.5",
90
+ isCompleted ? "bg-primary" : "bg-border",
91
+ )}
92
+ />
93
+ )
94
+ )}
95
+ </div>
96
+ );
97
+ })}
98
+ </div>
99
+ );
100
+ }
@@ -127,3 +127,44 @@ export interface DueDateDisplayProps {
127
127
  /** Inline styles for the root element */
128
128
  style?: React.CSSProperties;
129
129
  }
130
+
131
+ /**
132
+ * A single step definition used by the Stepper component.
133
+ */
134
+ export interface StepDefinition {
135
+ /** Step label text */
136
+ label: string;
137
+ /** Optional secondary description */
138
+ description?: string;
139
+ }
140
+
141
+ /**
142
+ * Stepper renders a visual step indicator for multi-step flows,
143
+ * showing completed, current, and upcoming steps with labels.
144
+ *
145
+ * @example
146
+ * <Stepper
147
+ * steps={[
148
+ * { label: "Setup" },
149
+ * { label: "Questions" },
150
+ * { label: "Results" },
151
+ * ]}
152
+ * currentStep={1}
153
+ * />
154
+ */
155
+ export interface StepperProps {
156
+ /** Ordered list of steps to display */
157
+ steps: StepDefinition[];
158
+ /** Zero-based index of the currently active step */
159
+ currentStep: number;
160
+ /** Layout orientation. @default 'horizontal' */
161
+ orientation?: "horizontal" | "vertical";
162
+ /** Visual density variant. @default 'default' */
163
+ variant?: "default" | "compact";
164
+ /** CSS class name for the root element */
165
+ className?: string;
166
+ /** Inline styles for the root element */
167
+ style?: React.CSSProperties;
168
+ }
169
+
170
+ export type { PaginationProps } from "./pagination";
@@ -0,0 +1,92 @@
1
+ import {
2
+ File,
3
+ FileText,
4
+ Image,
5
+ Film,
6
+ Music,
7
+ Archive,
8
+ Download,
9
+ X,
10
+ } from "lucide-react";
11
+ import { cn } from "../lib/utils";
12
+ import { formatFileSize } from "../utils/format-file-size";
13
+ import { Button } from "../ui/button";
14
+ import type { AttachmentListProps } from "./types";
15
+
16
+ function getFileIcon(mimeType: string) {
17
+ if (mimeType.startsWith("image/")) return Image;
18
+ if (mimeType.startsWith("video/")) return Film;
19
+ if (mimeType.startsWith("audio/")) return Music;
20
+ if (mimeType === "application/pdf") return FileText;
21
+ if (
22
+ mimeType === "application/zip" ||
23
+ mimeType === "application/x-rar-compressed" ||
24
+ mimeType === "application/gzip"
25
+ )
26
+ return Archive;
27
+ return File;
28
+ }
29
+
30
+ /**
31
+ * AttachmentList displays a read-only list of file attachments
32
+ * with type icons, formatted sizes, and optional download/remove actions.
33
+ */
34
+ export function AttachmentList({
35
+ files,
36
+ onDownload,
37
+ onRemove,
38
+ readOnly = true,
39
+ className,
40
+ style,
41
+ }: AttachmentListProps) {
42
+ if (files.length === 0) return null;
43
+
44
+ return (
45
+ <div className={cn("flex flex-col gap-1", className)} style={style}>
46
+ {files.map((file, index) => {
47
+ const Icon = getFileIcon(file.type);
48
+
49
+ return (
50
+ <div
51
+ key={`${file.name}-${index}`}
52
+ className="flex items-center gap-3 rounded-md px-2 py-1.5 hover:bg-muted/50 transition-colors"
53
+ >
54
+ <span className="shrink-0 text-muted-foreground">
55
+ <Icon size={18} />
56
+ </span>
57
+ <div className="flex-1 min-w-0">
58
+ <span className="text-sm text-foreground block truncate">
59
+ {file.name}
60
+ </span>
61
+ <span className="text-xs text-muted-foreground">
62
+ {formatFileSize(file.size)}
63
+ </span>
64
+ </div>
65
+ {onDownload && file.url && (
66
+ <Button
67
+ variant="ghost"
68
+ size="sm"
69
+ className="shrink-0 h-7 w-7 p-0"
70
+ onClick={() => onDownload(file)}
71
+ aria-label={`Download ${file.name}`}
72
+ >
73
+ <Download size={14} />
74
+ </Button>
75
+ )}
76
+ {!readOnly && onRemove && (
77
+ <Button
78
+ variant="ghost"
79
+ size="sm"
80
+ className="shrink-0 h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
81
+ onClick={() => onRemove(file)}
82
+ aria-label={`Remove ${file.name}`}
83
+ >
84
+ <X size={14} />
85
+ </Button>
86
+ )}
87
+ </div>
88
+ );
89
+ })}
90
+ </div>
91
+ );
92
+ }