@hydralms/components 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
  2. package/dist/StudentProfile-DeMxdrL3.js +3275 -0
  3. package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
  4. package/dist/assessment-toolbar/index.d.ts +5 -1
  5. package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
  6. package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
  7. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  8. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  9. package/dist/assessment-toolbar/types.d.ts +52 -4
  10. package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
  11. package/dist/common/index.d.ts +3 -1
  12. package/dist/common/pagination.d.ts +26 -0
  13. package/dist/common/stepper.d.ts +6 -0
  14. package/dist/common/types.d.ts +38 -0
  15. package/dist/components.css +1 -1
  16. package/dist/content/attachment-list.d.ts +6 -0
  17. package/dist/content/audio-player.d.ts +22 -0
  18. package/dist/content/code-block.d.ts +30 -0
  19. package/dist/content/content-block.d.ts +1 -1
  20. package/dist/content/embed-block.d.ts +28 -0
  21. package/dist/content/index.d.ts +8 -1
  22. package/dist/content/types.d.ts +63 -0
  23. package/dist/curriculum/course-card.d.ts +51 -0
  24. package/dist/curriculum/curriculum-item.d.ts +1 -1
  25. package/dist/curriculum/index.d.ts +2 -0
  26. package/dist/curriculum/types.d.ts +2 -2
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +597 -308
  30. package/dist/license/HydraContext.d.ts +16 -0
  31. package/dist/license/ProBadge.d.ts +6 -0
  32. package/dist/license/index.d.ts +7 -0
  33. package/dist/license/tiers.d.ts +3 -0
  34. package/dist/license/useHydraLicense.d.ts +6 -0
  35. package/dist/license/validate.d.ts +13 -0
  36. package/dist/license/withProGate.d.ts +6 -0
  37. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
  38. package/dist/modules/AssignmentModule/types.d.ts +69 -0
  39. package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
  40. package/dist/modules/CertificateModule/types.d.ts +51 -0
  41. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  42. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  43. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  44. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
  45. package/dist/modules/DiscussionModule/types.d.ts +47 -0
  46. package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
  47. package/dist/modules/ExamModule/types.d.ts +55 -0
  48. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  49. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  50. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
  51. package/dist/modules/GradeCenterModule/types.d.ts +56 -0
  52. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  53. package/dist/modules/QuizModule/types.d.ts +10 -14
  54. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  55. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  56. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  57. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  58. package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
  59. package/dist/modules/SurveyModule/types.d.ts +51 -0
  60. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  61. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  62. package/dist/modules/_shared/types.d.ts +10 -0
  63. package/dist/modules/_shared/use-timer.d.ts +9 -0
  64. package/dist/modules/index.d.ts +18 -0
  65. package/dist/modules.cjs +1 -0
  66. package/dist/modules.js +1834 -0
  67. package/dist/progress/achievement-badge.d.ts +6 -0
  68. package/dist/progress/activity-timeline.d.ts +6 -0
  69. package/dist/progress/index.d.ts +4 -1
  70. package/dist/progress/stat-card.d.ts +1 -1
  71. package/dist/progress/streak-badge.d.ts +6 -0
  72. package/dist/progress/types.d.ts +99 -0
  73. package/dist/provider/HydraProvider.d.ts +5 -1
  74. package/dist/questions/choice.d.ts +1 -1
  75. package/dist/questions/confidence-indicator.d.ts +37 -0
  76. package/dist/questions/essay.d.ts +2 -2
  77. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  78. package/dist/questions/hotspot.d.ts +21 -0
  79. package/dist/questions/index.d.ts +11 -1
  80. package/dist/questions/inline-choice.d.ts +21 -0
  81. package/dist/questions/matching.d.ts +22 -0
  82. package/dist/questions/multiple-choice.d.ts +1 -1
  83. package/dist/questions/numeric.d.ts +11 -0
  84. package/dist/questions/ordering.d.ts +12 -0
  85. package/dist/questions/question-renderer.d.ts +1 -1
  86. package/dist/questions/scenario.d.ts +23 -0
  87. package/dist/questions/scoring.d.ts +22 -0
  88. package/dist/questions/spreadsheet.d.ts +29 -0
  89. package/dist/questions/true-false.d.ts +1 -1
  90. package/dist/questions/types.d.ts +106 -1
  91. package/dist/questions/use-drag-reorder.d.ts +17 -0
  92. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  93. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  94. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  95. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  96. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  97. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  98. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  99. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  100. package/dist/sections/CertificateViewer/types.d.ts +13 -5
  101. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  102. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  103. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  104. package/dist/sections/CourseOutline/types.d.ts +6 -0
  105. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  106. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  107. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  108. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  109. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  110. package/dist/sections/ExamSession/types.d.ts +12 -1
  111. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  112. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  113. package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
  114. package/dist/sections/ForumBoard/types.d.ts +78 -0
  115. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  116. package/dist/sections/GradebookTable/types.d.ts +14 -0
  117. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  118. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  119. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  120. package/dist/sections/LessonPage/types.d.ts +6 -0
  121. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  122. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  123. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  124. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  125. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  126. package/dist/sections/QuizSession/types.d.ts +12 -1
  127. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
  128. package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
  129. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  130. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  131. package/dist/sections/RubricView/RubricView.d.ts +9 -0
  132. package/dist/sections/RubricView/types.d.ts +56 -0
  133. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  134. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  135. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  136. package/dist/sections/StudentProfile/types.d.ts +98 -0
  137. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  138. package/dist/sections/SurveyForm/types.d.ts +6 -0
  139. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  140. package/dist/sections/_shared/section-shell.d.ts +20 -0
  141. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  142. package/dist/sections/index.d.ts +13 -1
  143. package/dist/sections.cjs +1 -1
  144. package/dist/sections.js +282 -1786
  145. package/dist/social/post-card.d.ts +1 -1
  146. package/dist/tabs-BsfVo2Bl.cjs +173 -0
  147. package/dist/tabs-BuY1iNJE.js +22305 -0
  148. package/dist/ui/alert.d.ts +1 -1
  149. package/dist/ui/badge.d.ts +1 -1
  150. package/dist/ui/button.d.ts +1 -1
  151. package/dist/ui/drawer.d.ts +84 -0
  152. package/dist/ui/index.d.ts +5 -0
  153. package/dist/ui/progress.d.ts +1 -1
  154. package/dist/ui/rich-text-editor.d.ts +32 -0
  155. package/dist/ui/rich-text-toolbar.d.ts +8 -0
  156. package/dist/ui/toast.d.ts +43 -0
  157. package/dist/utils/array-utils.d.ts +4 -0
  158. package/dist/utils/debounce.d.ts +5 -1
  159. package/dist/utils/flatten-leaves.d.ts +6 -0
  160. package/dist/utils/format-file-size.d.ts +1 -0
  161. package/dist/utils/format-timestamp.d.ts +1 -0
  162. package/dist/utils/is-empty-html.d.ts +5 -0
  163. package/dist/utils/pick-palette-color.d.ts +19 -0
  164. package/dist/utils/shuffle.d.ts +1 -0
  165. package/dist/utils/string-utils.d.ts +12 -0
  166. package/dist/video/types.d.ts +15 -0
  167. package/dist/video/video-bookmark.d.ts +1 -1
  168. package/dist/video/video-player.d.ts +1 -1
  169. package/dist/video/video-playlist-item.d.ts +1 -1
  170. package/dist/withProGate-BWqcKdPM.js +137 -0
  171. package/dist/withProGate-DX6XqKLp.cjs +1 -0
  172. package/package.json +40 -137
  173. package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
  174. package/src/assessment-toolbar/index.ts +6 -0
  175. package/src/assessment-toolbar/question-header-bar.tsx +61 -0
  176. package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
  177. package/src/assessment-toolbar/question-navigator.tsx +13 -36
  178. package/src/assessment-toolbar/timer-display.tsx +6 -5
  179. package/src/assessment-toolbar/types.ts +54 -4
  180. package/src/assessment-toolbar/use-countdown.ts +153 -0
  181. package/src/common/empty-state.tsx +1 -0
  182. package/src/common/index.ts +5 -0
  183. package/src/common/pagination.tsx +135 -0
  184. package/src/common/search-input.tsx +8 -6
  185. package/src/common/stepper.tsx +100 -0
  186. package/src/common/types.ts +41 -0
  187. package/src/content/attachment-list.tsx +92 -0
  188. package/src/content/audio-player.tsx +196 -0
  189. package/src/content/code-block.tsx +113 -0
  190. package/src/content/content-block.tsx +68 -2
  191. package/src/content/embed-block.tsx +78 -0
  192. package/src/content/file-upload-zone.tsx +11 -6
  193. package/src/content/index.ts +9 -0
  194. package/src/content/types.ts +46 -0
  195. package/src/curriculum/course-card.tsx +199 -0
  196. package/src/curriculum/curriculum-item.tsx +9 -5
  197. package/src/curriculum/curriculum-tree.tsx +20 -13
  198. package/src/curriculum/index.ts +2 -0
  199. package/src/curriculum/types.ts +2 -2
  200. package/src/feedback/feedback-banner.tsx +12 -14
  201. package/src/flashcards/flashcard-deck.tsx +1 -9
  202. package/src/flashcards/flashcard.tsx +29 -9
  203. package/src/index.ts +3 -0
  204. package/src/license/HydraContext.tsx +62 -0
  205. package/src/license/ProBadge.tsx +43 -0
  206. package/src/license/index.ts +7 -0
  207. package/src/license/tiers.ts +24 -0
  208. package/src/license/useHydraLicense.ts +10 -0
  209. package/src/license/validate.ts +90 -0
  210. package/src/license/withProGate.tsx +21 -0
  211. package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
  212. package/src/modules/AssignmentModule/types.ts +77 -0
  213. package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
  214. package/src/modules/CertificateModule/types.ts +49 -0
  215. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  216. package/src/modules/CourseCatalogModule/types.ts +47 -0
  217. package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
  218. package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
  219. package/src/modules/DiscussionModule/types.ts +54 -0
  220. package/src/modules/ExamModule/ExamModule.tsx +151 -0
  221. package/src/modules/ExamModule/types.ts +57 -0
  222. package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
  223. package/src/modules/FlashcardLab/types.ts +2 -0
  224. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
  225. package/src/modules/GradeCenterModule/types.ts +65 -0
  226. package/src/modules/QuizModule/QuizModule.tsx +58 -178
  227. package/src/modules/QuizModule/types.ts +10 -15
  228. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  229. package/src/modules/StudentDashboardModule/types.ts +56 -0
  230. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  231. package/src/modules/StudentProfileModule/types.ts +45 -0
  232. package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
  233. package/src/modules/SurveyModule/types.ts +53 -0
  234. package/src/modules/_shared/assessment-intro.tsx +75 -0
  235. package/src/modules/_shared/assessment-results.tsx +133 -0
  236. package/src/modules/_shared/types.ts +11 -0
  237. package/src/modules/_shared/use-timer.ts +49 -0
  238. package/src/modules/index.ts +33 -0
  239. package/src/progress/achievement-badge.tsx +52 -0
  240. package/src/progress/activity-timeline.tsx +84 -0
  241. package/src/progress/grade-indicator.tsx +9 -1
  242. package/src/progress/index.ts +7 -0
  243. package/src/progress/progress-ring.tsx +2 -1
  244. package/src/progress/stat-card.tsx +37 -18
  245. package/src/progress/streak-badge.tsx +35 -0
  246. package/src/progress/types.ts +103 -0
  247. package/src/provider/HydraProvider.tsx +15 -6
  248. package/src/questions/choice.tsx +19 -14
  249. package/src/questions/confidence-indicator.tsx +107 -0
  250. package/src/questions/essay.tsx +28 -28
  251. package/src/questions/fill-in-the-blank.tsx +20 -19
  252. package/src/questions/hotspot.tsx +154 -0
  253. package/src/questions/index.ts +18 -0
  254. package/src/questions/inline-choice.tsx +152 -0
  255. package/src/questions/matching.tsx +229 -0
  256. package/src/questions/multiple-choice.tsx +19 -14
  257. package/src/questions/numeric.tsx +106 -0
  258. package/src/questions/ordering.tsx +167 -0
  259. package/src/questions/question-renderer.tsx +24 -2
  260. package/src/questions/scenario.tsx +140 -0
  261. package/src/questions/scoring.ts +201 -0
  262. package/src/questions/spreadsheet.tsx +260 -0
  263. package/src/questions/true-false.tsx +19 -14
  264. package/src/questions/types.ts +123 -1
  265. package/src/questions/use-drag-reorder.ts +80 -0
  266. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
  267. package/src/sections/AnnouncementFeed/types.ts +15 -1
  268. package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
  269. package/src/sections/AssessmentReview/types.ts +6 -0
  270. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
  271. package/src/sections/AssignmentSubmission/types.ts +6 -0
  272. package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
  273. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  274. package/src/sections/CertificateViewer/types.ts +19 -5
  275. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  276. package/src/sections/CourseCatalog/types.ts +76 -0
  277. package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
  278. package/src/sections/CourseOutline/types.ts +6 -0
  279. package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
  280. package/src/sections/DiscussionThread/types.ts +6 -0
  281. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  282. package/src/sections/EnrollmentWizard/types.ts +65 -0
  283. package/src/sections/ExamSession/ExamSession.tsx +125 -82
  284. package/src/sections/ExamSession/types.ts +12 -1
  285. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  286. package/src/sections/FlashcardStudySession/types.ts +6 -0
  287. package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
  288. package/src/sections/ForumBoard/types.ts +81 -0
  289. package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
  290. package/src/sections/GradebookTable/types.ts +14 -0
  291. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  292. package/src/sections/LecturePlayer/types.ts +8 -0
  293. package/src/sections/LessonPage/LessonPage.tsx +40 -13
  294. package/src/sections/LessonPage/types.ts +6 -0
  295. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
  296. package/src/sections/PracticeQuiz/types.ts +6 -0
  297. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
  298. package/src/sections/ProgressDashboard/types.ts +6 -0
  299. package/src/sections/QuizSession/QuizSession.tsx +115 -67
  300. package/src/sections/QuizSession/types.ts +12 -1
  301. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
  302. package/src/sections/RequirementsChecklist/types.ts +44 -0
  303. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
  304. package/src/sections/ResourceLibrary/types.ts +15 -1
  305. package/src/sections/RubricView/RubricView.tsx +174 -0
  306. package/src/sections/RubricView/types.ts +58 -0
  307. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
  308. package/src/sections/ScrollableQuiz/types.ts +6 -0
  309. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  310. package/src/sections/StudentProfile/types.ts +99 -0
  311. package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
  312. package/src/sections/SurveyForm/types.ts +6 -0
  313. package/src/sections/_shared/merge-answers.ts +22 -0
  314. package/src/sections/_shared/section-shell.tsx +64 -0
  315. package/src/sections/_shared/use-assessment-session.ts +125 -0
  316. package/src/sections/index.ts +42 -1
  317. package/src/social/post-card.tsx +8 -19
  318. package/src/social/user-avatar.tsx +10 -5
  319. package/src/styles/globals.css +52 -41
  320. package/src/ui/badge.tsx +8 -0
  321. package/src/ui/drawer.tsx +600 -0
  322. package/src/ui/index.ts +21 -0
  323. package/src/ui/progress.tsx +4 -0
  324. package/src/ui/rich-text-editor.tsx +119 -0
  325. package/src/ui/rich-text-toolbar.tsx +157 -0
  326. package/src/ui/toast.tsx +170 -0
  327. package/src/utils/array-utils.ts +17 -0
  328. package/src/utils/debounce.ts +8 -2
  329. package/src/utils/flatten-leaves.ts +17 -0
  330. package/src/utils/format-file-size.ts +5 -0
  331. package/src/utils/format-timestamp.ts +13 -0
  332. package/src/utils/is-empty-html.ts +7 -0
  333. package/src/utils/pick-palette-color.ts +33 -0
  334. package/src/utils/shuffle.ts +8 -0
  335. package/src/utils/string-utils.ts +30 -0
  336. package/src/video/types.ts +16 -0
  337. package/src/video/video-bookmark.tsx +4 -3
  338. package/src/video/video-chapter-list.tsx +9 -4
  339. package/src/video/video-player.tsx +24 -5
  340. package/src/video/video-playlist-item.tsx +8 -3
  341. package/src/video/video-thumbnail-card.tsx +4 -0
  342. package/src/video/video-transcript.tsx +8 -5
  343. package/dist/table-BrS5cDQu.js +0 -2510
  344. package/dist/table-D6AkBBEo.cjs +0 -1
@@ -0,0 +1,196 @@
1
+ import { memo, useCallback, useRef, useState } from "react";
2
+ import { Play, Pause, Volume2, VolumeX } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+ import { Button } from "../ui/button";
5
+ import { formatTimer } from "../utils/format-duration";
6
+
7
+ /**
8
+ * AudioPlayer provides an HTML5 audio player with custom controls
9
+ * including play/pause, seek, speed adjustment, and mute toggle.
10
+ *
11
+ * @example
12
+ * <AudioPlayer src="/audio/lecture-1.mp3" title="Lecture 1: Introduction" />
13
+ */
14
+ export interface AudioPlayerProps {
15
+ /** Audio source URL */
16
+ src: string;
17
+ /** Optional title displayed above the player */
18
+ title?: string;
19
+ /** Called when playback ends */
20
+ onEnded?: () => void;
21
+ /** Called on time update with current time and duration */
22
+ onTimeUpdate?: (currentTime: number, duration: number) => void;
23
+ /** CSS class name for the root element */
24
+ className?: string;
25
+ /** Inline styles for the root element */
26
+ style?: React.CSSProperties;
27
+ }
28
+
29
+ const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
30
+
31
+ export const AudioPlayer = memo(function AudioPlayer({
32
+ src,
33
+ title,
34
+ onEnded,
35
+ onTimeUpdate,
36
+ className,
37
+ style,
38
+ }: AudioPlayerProps) {
39
+ const audioRef = useRef<HTMLAudioElement>(null);
40
+ const [isPlaying, setIsPlaying] = useState(false);
41
+ const [isMuted, setIsMuted] = useState(false);
42
+ const [currentTime, setCurrentTime] = useState(0);
43
+ const [duration, setDuration] = useState(0);
44
+ const [speed, setSpeed] = useState(1);
45
+ const lastReportedTime = useRef(-1);
46
+ const onTimeUpdateRef = useRef(onTimeUpdate);
47
+ onTimeUpdateRef.current = onTimeUpdate;
48
+
49
+ const togglePlay = useCallback(() => {
50
+ const audio = audioRef.current;
51
+ if (!audio) return;
52
+ if (audio.paused) {
53
+ audio.play();
54
+ } else {
55
+ audio.pause();
56
+ }
57
+ }, []);
58
+
59
+ const toggleMute = useCallback(() => {
60
+ const audio = audioRef.current;
61
+ if (!audio) return;
62
+ audio.muted = !audio.muted;
63
+ setIsMuted((prev) => !prev);
64
+ }, []);
65
+
66
+ const cycleSpeed = useCallback(() => {
67
+ const audio = audioRef.current;
68
+ if (!audio) return;
69
+ setSpeed((prev) => {
70
+ const idx = SPEEDS.indexOf(prev);
71
+ const next = SPEEDS[(idx + 1) % SPEEDS.length];
72
+ audio.playbackRate = next;
73
+ return next;
74
+ });
75
+ }, []);
76
+
77
+ const handleSeek = useCallback(
78
+ (e: React.ChangeEvent<HTMLInputElement>) => {
79
+ const audio = audioRef.current;
80
+ if (!audio) return;
81
+ const time = Number(e.target.value);
82
+ audio.currentTime = time;
83
+ lastReportedTime.current = Math.floor(time * 4) / 4;
84
+ setCurrentTime(time);
85
+ },
86
+ [],
87
+ );
88
+
89
+ const handleTimeUpdate = useCallback(() => {
90
+ const audio = audioRef.current;
91
+ if (!audio) return;
92
+ const rounded = Math.floor(audio.currentTime * 4) / 4;
93
+ if (rounded !== lastReportedTime.current) {
94
+ lastReportedTime.current = rounded;
95
+ setCurrentTime(audio.currentTime);
96
+ onTimeUpdateRef.current?.(audio.currentTime, audio.duration);
97
+ }
98
+ }, []);
99
+
100
+ const handleLoadedMetadata = useCallback(() => {
101
+ const audio = audioRef.current;
102
+ if (audio && !isNaN(audio.duration)) setDuration(audio.duration);
103
+ }, []);
104
+
105
+ const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
106
+
107
+ return (
108
+ <div
109
+ data-slot="audio-player"
110
+ className={cn("rounded-lg border bg-card p-4 space-y-3", className)}
111
+ style={style}
112
+ >
113
+ {title && (
114
+ <p className="text-sm font-medium text-foreground">{title}</p>
115
+ )}
116
+ <audio
117
+ ref={audioRef}
118
+ src={src}
119
+ onPlay={() => setIsPlaying(true)}
120
+ onPause={() => setIsPlaying(false)}
121
+ onEnded={() => {
122
+ setIsPlaying(false);
123
+ onEnded?.();
124
+ }}
125
+ onTimeUpdate={handleTimeUpdate}
126
+ onLoadedMetadata={handleLoadedMetadata}
127
+ preload="metadata"
128
+ />
129
+ <div className="flex items-center gap-3">
130
+ <Button
131
+ variant="ghost"
132
+ size="icon-sm"
133
+ onClick={togglePlay}
134
+ aria-label={isPlaying ? "Pause" : "Play"}
135
+ >
136
+ {isPlaying ? (
137
+ <Pause className="size-4" />
138
+ ) : (
139
+ <Play className="size-4" />
140
+ )}
141
+ </Button>
142
+
143
+ <span className="text-xs tabular-nums text-muted-foreground w-12 shrink-0">
144
+ {formatTimer(Math.floor(currentTime))}
145
+ </span>
146
+
147
+ <div className="relative flex-1 h-5 flex items-center">
148
+ <div className="absolute inset-y-0 left-0 flex items-center w-full">
149
+ <div className="w-full h-1 rounded-full bg-muted overflow-hidden">
150
+ <div
151
+ className="h-full bg-primary rounded-full transition-[width] duration-100"
152
+ style={{ width: `${progress}%` }}
153
+ />
154
+ </div>
155
+ </div>
156
+ <input
157
+ type="range"
158
+ min={0}
159
+ max={duration || 0}
160
+ step={0.1}
161
+ value={currentTime}
162
+ onChange={handleSeek}
163
+ className="absolute inset-0 w-full opacity-0 cursor-pointer"
164
+ aria-label="Seek"
165
+ />
166
+ </div>
167
+
168
+ <span className="text-xs tabular-nums text-muted-foreground w-12 shrink-0 text-right">
169
+ {formatTimer(Math.floor(duration))}
170
+ </span>
171
+
172
+ <Button
173
+ variant="ghost"
174
+ size="icon-xs"
175
+ onClick={toggleMute}
176
+ aria-label={isMuted ? "Unmute" : "Mute"}
177
+ >
178
+ {isMuted ? (
179
+ <VolumeX className="size-3.5" />
180
+ ) : (
181
+ <Volume2 className="size-3.5" />
182
+ )}
183
+ </Button>
184
+
185
+ <button
186
+ type="button"
187
+ onClick={cycleSpeed}
188
+ className="text-xs font-medium text-muted-foreground hover:text-foreground transition-colors tabular-nums w-8 text-center"
189
+ aria-label={`Playback speed ${speed}x`}
190
+ >
191
+ {speed}x
192
+ </button>
193
+ </div>
194
+ </div>
195
+ );
196
+ });
@@ -0,0 +1,113 @@
1
+ import { memo, useCallback, useEffect, useRef, useState } from "react";
2
+ import { Copy, Check, FileCode } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+ import { Button } from "../ui/button";
5
+
6
+ /**
7
+ * CodeBlock renders source code with optional line numbers, a filename header,
8
+ * and a copy-to-clipboard button. Does not bundle a syntax highlighter —
9
+ * consumers can apply their own (Prism, Shiki, etc.) via the `language-*` class on the `<code>` element.
10
+ *
11
+ * @example
12
+ * <CodeBlock
13
+ * code="console.log('hello');"
14
+ * language="javascript"
15
+ * filename="example.js"
16
+ * showLineNumbers
17
+ * />
18
+ */
19
+ export interface CodeBlockProps {
20
+ /** The code content to display */
21
+ code: string;
22
+ /** Programming language identifier (applied as `language-*` class on `<code>`) */
23
+ language?: string;
24
+ /** Optional filename shown in the header bar */
25
+ filename?: string;
26
+ /** Whether to display line numbers */
27
+ showLineNumbers?: boolean;
28
+ /** Called after code is copied to clipboard */
29
+ onCopy?: () => void;
30
+ /** CSS class name for the root element */
31
+ className?: string;
32
+ /** Inline styles for the root element */
33
+ style?: React.CSSProperties;
34
+ }
35
+
36
+ export const CodeBlock = memo(function CodeBlock({
37
+ code,
38
+ language,
39
+ filename,
40
+ showLineNumbers = false,
41
+ onCopy,
42
+ className,
43
+ style,
44
+ }: CodeBlockProps) {
45
+ const [copied, setCopied] = useState(false);
46
+ const hasHeader = !!(filename || language);
47
+ const copyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
48
+ useEffect(() => () => clearTimeout(copyTimerRef.current), []);
49
+
50
+ const handleCopy = useCallback(async () => {
51
+ try {
52
+ await navigator.clipboard.writeText(code);
53
+ setCopied(true);
54
+ onCopy?.();
55
+ clearTimeout(copyTimerRef.current);
56
+ copyTimerRef.current = setTimeout(() => setCopied(false), 2000);
57
+ } catch {
58
+ // Clipboard API not available
59
+ }
60
+ }, [code, onCopy]);
61
+
62
+ const lines = code.split("\n");
63
+
64
+ const copyButton = (
65
+ <Button
66
+ variant="ghost"
67
+ size="icon-xs"
68
+ onClick={handleCopy}
69
+ aria-label={copied ? "Copied" : "Copy code"}
70
+ >
71
+ {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
72
+ </Button>
73
+ );
74
+
75
+ return (
76
+ <div
77
+ data-slot="code-block"
78
+ className={cn(
79
+ "relative rounded-lg border bg-card overflow-hidden text-sm",
80
+ className,
81
+ )}
82
+ style={style}
83
+ >
84
+ {hasHeader ? (
85
+ <div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50">
86
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
87
+ <FileCode className="size-3.5" />
88
+ <span>{filename ?? language}</span>
89
+ </div>
90
+ {copyButton}
91
+ </div>
92
+ ) : (
93
+ <div className="absolute top-2 right-2 z-10">{copyButton}</div>
94
+ )}
95
+ <div className="overflow-x-auto">
96
+ <pre className="p-4 m-0">
97
+ <code className={language ? `language-${language}` : undefined}>
98
+ {showLineNumbers
99
+ ? lines.map((line, i) => (
100
+ <div key={i} className="flex">
101
+ <span className="select-none text-muted-foreground/50 w-8 shrink-0 text-right pr-4 tabular-nums">
102
+ {i + 1}
103
+ </span>
104
+ <span>{line}</span>
105
+ </div>
106
+ ))
107
+ : code}
108
+ </code>
109
+ </pre>
110
+ </div>
111
+ </div>
112
+ );
113
+ });
@@ -1,9 +1,23 @@
1
+ import { memo } from "react";
1
2
  import { Info, AlertTriangle, Lightbulb } from "lucide-react";
2
3
  import { VideoPlayer } from "../video";
3
4
  import { QuestionRenderer } from "../questions";
4
5
  import { FlashcardDeck } from "../flashcards";
5
6
  import { Alert, AlertDescription } from "../ui/alert";
6
7
  import { Separator } from "../ui/separator";
8
+ import {
9
+ Table,
10
+ TableHeader,
11
+ TableBody,
12
+ TableRow,
13
+ TableHead,
14
+ TableCell,
15
+ TableCaption,
16
+ } from "../ui/table";
17
+ import { AudioPlayer } from "./audio-player";
18
+ import { CodeBlock } from "./code-block";
19
+ import { EmbedBlock } from "./embed-block";
20
+ import { AttachmentList } from "./attachment-list";
7
21
  import type { ContentBlockProps } from "./types";
8
22
  import { cn } from "../lib/utils";
9
23
 
@@ -13,7 +27,7 @@ const CALLOUT_CONFIG = {
13
27
  tip: { variant: "success" as const, Icon: Lightbulb },
14
28
  };
15
29
 
16
- export function ContentBlock({
30
+ export const ContentBlock = memo(function ContentBlock({
17
31
  block,
18
32
  onQuestionAnswer,
19
33
  readOnly = false,
@@ -57,6 +71,7 @@ export function ContentBlock({
57
71
  <img
58
72
  src={block.src}
59
73
  alt={block.alt ?? ""}
74
+ loading="lazy"
60
75
  className="max-w-full rounded-sm"
61
76
  />
62
77
  {block.caption && (
@@ -104,6 +119,57 @@ export function ContentBlock({
104
119
  />
105
120
  );
106
121
 
122
+ case "audio":
123
+ return wrapper(
124
+ <AudioPlayer src={block.src} title={block.title} />
125
+ );
126
+
127
+ case "code":
128
+ return wrapper(
129
+ <CodeBlock
130
+ code={block.code}
131
+ language={block.language}
132
+ filename={block.filename}
133
+ showLineNumbers={block.showLineNumbers}
134
+ />
135
+ );
136
+
137
+ case "embed":
138
+ return wrapper(
139
+ <EmbedBlock
140
+ src={block.src}
141
+ title={block.title}
142
+ aspectRatio={block.aspectRatio}
143
+ allowFullscreen={block.allowFullscreen}
144
+ />
145
+ );
146
+
147
+ case "table":
148
+ return wrapper(
149
+ <Table>
150
+ {block.caption && <TableCaption>{block.caption}</TableCaption>}
151
+ <TableHeader>
152
+ <TableRow>
153
+ {block.headers.map((header, i) => (
154
+ <TableHead key={i}>{header}</TableHead>
155
+ ))}
156
+ </TableRow>
157
+ </TableHeader>
158
+ <TableBody>
159
+ {block.rows.map((row, i) => (
160
+ <TableRow key={i}>
161
+ {row.map((cell, j) => (
162
+ <TableCell key={j}>{cell}</TableCell>
163
+ ))}
164
+ </TableRow>
165
+ ))}
166
+ </TableBody>
167
+ </Table>
168
+ );
169
+
170
+ case "file":
171
+ return wrapper(<AttachmentList files={block.files} readOnly />);
172
+
107
173
  case "divider":
108
174
  return wrapper(<Separator />);
109
175
 
@@ -113,4 +179,4 @@ export function ContentBlock({
113
179
  default:
114
180
  return null;
115
181
  }
116
- }
182
+ });
@@ -0,0 +1,78 @@
1
+ import { memo, useState } from "react";
2
+ import { ExternalLink } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+ import { Skeleton } from "../ui/skeleton";
5
+
6
+ export type EmbedAspectRatio = "16/9" | "4/3" | "1/1";
7
+
8
+ /**
9
+ * EmbedBlock renders a responsive iframe wrapper with an optional title bar
10
+ * and loading skeleton. Ideal for embedding YouTube videos, SCORM objects,
11
+ * Google Slides, and other external content.
12
+ *
13
+ * @example
14
+ * <EmbedBlock
15
+ * src="https://www.youtube.com/embed/dQw4w9WgXcQ"
16
+ * title="Introduction Video"
17
+ * aspectRatio="16/9"
18
+ * />
19
+ */
20
+ export interface EmbedBlockProps {
21
+ /** iframe source URL */
22
+ src: string;
23
+ /** Optional title for the embed */
24
+ title?: string;
25
+ /** Aspect ratio of the embed container */
26
+ aspectRatio?: EmbedAspectRatio;
27
+ /** Whether to allow fullscreen */
28
+ allowFullscreen?: boolean;
29
+ /** CSS class name for the root element */
30
+ className?: string;
31
+ /** Inline styles for the root element */
32
+ style?: React.CSSProperties;
33
+ }
34
+
35
+ const ASPECT_CLASSES: Record<EmbedAspectRatio, string> = {
36
+ "16/9": "aspect-video",
37
+ "4/3": "aspect-[4/3]",
38
+ "1/1": "aspect-square",
39
+ };
40
+
41
+ export const EmbedBlock = memo(function EmbedBlock({
42
+ src,
43
+ title,
44
+ aspectRatio = "16/9",
45
+ allowFullscreen = true,
46
+ className,
47
+ style,
48
+ }: EmbedBlockProps) {
49
+ const [isLoaded, setIsLoaded] = useState(false);
50
+
51
+ return (
52
+ <div
53
+ data-slot="embed-block"
54
+ className={cn("rounded-lg border bg-card overflow-hidden", className)}
55
+ style={style}
56
+ >
57
+ {title && (
58
+ <div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50">
59
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
60
+ <ExternalLink className="size-3.5" />
61
+ <span>{title}</span>
62
+ </div>
63
+ </div>
64
+ )}
65
+ <div className={cn("relative w-full", ASPECT_CLASSES[aspectRatio])}>
66
+ {!isLoaded && <Skeleton className="absolute inset-0 rounded-none" />}
67
+ <iframe
68
+ src={src}
69
+ title={title ?? "Embedded content"}
70
+ allowFullScreen={allowFullscreen}
71
+ onLoad={() => setIsLoaded(true)}
72
+ className="absolute inset-0 w-full h-full border-0"
73
+ sandbox="allow-scripts allow-same-origin allow-popups allow-presentation"
74
+ />
75
+ </div>
76
+ </div>
77
+ );
78
+ });
@@ -3,12 +3,7 @@ import { Upload, File, X } from "lucide-react";
3
3
  import { Button } from "../ui/button";
4
4
  import type { FileUploadZoneProps } from "./types";
5
5
  import { cn } from "../lib/utils";
6
-
7
- function formatFileSize(bytes: number): string {
8
- if (bytes < 1024) return `${bytes} B`;
9
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
10
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
11
- }
6
+ import { formatFileSize } from "../utils/format-file-size";
12
7
 
13
8
  export function FileUploadZone({
14
9
  files,
@@ -44,7 +39,17 @@ export function FileUploadZone({
44
39
  isDragging && "border-primary bg-muted",
45
40
  disabled && "cursor-default opacity-50",
46
41
  atLimit && "cursor-default",
42
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
47
43
  )}
44
+ role="button"
45
+ tabIndex={disabled || atLimit ? -1 : 0}
46
+ aria-label={atLimit ? `Maximum ${maxFiles} files reached` : label}
47
+ onKeyDown={(e) => {
48
+ if ((e.key === "Enter" || e.key === " ") && !disabled && !atLimit) {
49
+ e.preventDefault();
50
+ inputRef.current?.click();
51
+ }
52
+ }}
48
53
  onDragOver={(e) => {
49
54
  e.preventDefault();
50
55
  if (!disabled && !atLimit) setIsDragging(true);
@@ -1,7 +1,16 @@
1
1
  export { ContentBlock } from "./content-block";
2
2
  export { FileUploadZone } from "./file-upload-zone";
3
+ export { AttachmentList } from "./attachment-list";
3
4
  export type {
4
5
  LessonBlock,
5
6
  ContentBlockProps,
6
7
  FileUploadZoneProps,
8
+ AttachmentListProps,
9
+ AttachmentFile,
7
10
  } from "./types";
11
+ export { AudioPlayer } from "./audio-player";
12
+ export type { AudioPlayerProps } from "./audio-player";
13
+ export { CodeBlock } from "./code-block";
14
+ export type { CodeBlockProps } from "./code-block";
15
+ export { EmbedBlock } from "./embed-block";
16
+ export type { EmbedBlockProps, EmbedAspectRatio } from "./embed-block";
@@ -14,6 +14,11 @@ export type LessonBlock =
14
14
  | { type: "callout"; content: string; variant?: "info" | "warning" | "tip" }
15
15
  | { type: "question"; question: QuestionData }
16
16
  | { type: "flashcards"; cards: FlashcardData[]; deckName?: string }
17
+ | { type: "audio"; src: string; title?: string }
18
+ | { type: "code"; code: string; language?: string; filename?: string; showLineNumbers?: boolean }
19
+ | { type: "embed"; src: string; title?: string; aspectRatio?: "16/9" | "4/3" | "1/1"; allowFullscreen?: boolean }
20
+ | { type: "table"; headers: string[]; rows: string[][]; caption?: string }
21
+ | { type: "file"; files: AttachmentFile[] }
17
22
  | { type: "divider" }
18
23
  | { type: "custom"; render: ReactNode };
19
24
 
@@ -74,3 +79,44 @@ export interface FileUploadZoneProps {
74
79
  /** Inline styles for the root element */
75
80
  style?: React.CSSProperties;
76
81
  }
82
+
83
+ /**
84
+ * A single file attachment for display in AttachmentList.
85
+ */
86
+ export interface AttachmentFile {
87
+ /** File name with extension */
88
+ name: string;
89
+ /** File size in bytes */
90
+ size: number;
91
+ /** MIME type (e.g. "application/pdf", "image/png") */
92
+ type: string;
93
+ /** Optional download URL */
94
+ url?: string;
95
+ }
96
+
97
+ /**
98
+ * AttachmentList displays a read-only list of file attachments
99
+ * with type icons, formatted sizes, and optional download/remove actions.
100
+ *
101
+ * @example
102
+ * <AttachmentList
103
+ * files={[
104
+ * { name: "syllabus.pdf", size: 245000, type: "application/pdf", url: "/files/syllabus.pdf" },
105
+ * ]}
106
+ * onDownload={(file) => window.open(file.url)}
107
+ * />
108
+ */
109
+ export interface AttachmentListProps {
110
+ /** List of file attachments to display */
111
+ files: AttachmentFile[];
112
+ /** Called when the user clicks a file's download action */
113
+ onDownload?: (file: AttachmentFile) => void;
114
+ /** Called when the user clicks a file's remove button (only shown when not readOnly) */
115
+ onRemove?: (file: AttachmentFile) => void;
116
+ /** When true, hides the remove button. @default true */
117
+ readOnly?: boolean;
118
+ /** CSS class name for the root element */
119
+ className?: string;
120
+ /** Inline styles for the root element */
121
+ style?: React.CSSProperties;
122
+ }