@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,119 @@
1
+ import { useEffect } from "react";
2
+ import { useEditor, EditorContent } from "@tiptap/react";
3
+ import StarterKit from "@tiptap/starter-kit";
4
+ import Placeholder from "@tiptap/extension-placeholder";
5
+ import UnderlineExtension from "@tiptap/extension-underline";
6
+ import LinkExtension from "@tiptap/extension-link";
7
+ import { cn } from "../lib/utils";
8
+ import { RichTextToolbar } from "./rich-text-toolbar";
9
+
10
+ export type RichTextEditorVariant = "default" | "minimal";
11
+
12
+ export interface RichTextEditorProps {
13
+ /** HTML string value */
14
+ value?: string;
15
+ /** Called with the HTML string on every content change */
16
+ onChange?: (html: string) => void;
17
+ /** Placeholder text shown when the editor is empty */
18
+ placeholder?: string;
19
+ /** Makes the editor non-editable (shows content without toolbar) */
20
+ readOnly?: boolean;
21
+ /** Visually disables the editor */
22
+ disabled?: boolean;
23
+ /** `"default"` shows full toolbar; `"minimal"` shows only basic formatting */
24
+ variant?: RichTextEditorVariant;
25
+ /** Accessible label for screen readers (defaults to "Rich text editor") */
26
+ ariaLabel?: string;
27
+ className?: string;
28
+ style?: React.CSSProperties;
29
+ }
30
+
31
+ /**
32
+ * RichTextEditor wraps Tiptap to provide a rich text editing experience
33
+ * styled to match the HydraLMS design system.
34
+ *
35
+ * @example
36
+ * <RichTextEditor
37
+ * value={html}
38
+ * onChange={setHtml}
39
+ * placeholder="Write your response..."
40
+ * variant="default"
41
+ * />
42
+ */
43
+ export function RichTextEditor({
44
+ value = "",
45
+ onChange,
46
+ placeholder,
47
+ readOnly = false,
48
+ disabled = false,
49
+ variant = "default",
50
+ ariaLabel = "Rich text editor",
51
+ className,
52
+ style,
53
+ }: RichTextEditorProps) {
54
+ const editor = useEditor({
55
+ extensions: [
56
+ StarterKit,
57
+ UnderlineExtension,
58
+ LinkExtension.configure({
59
+ openOnClick: false,
60
+ HTMLAttributes: { class: "text-primary underline" },
61
+ }),
62
+ Placeholder.configure({ placeholder }),
63
+ ],
64
+ content: value,
65
+ editable: !readOnly && !disabled,
66
+ editorProps: {
67
+ attributes: {
68
+ "aria-label": ariaLabel,
69
+ role: "textbox",
70
+ "aria-multiline": "true",
71
+ },
72
+ },
73
+ onUpdate: ({ editor: e }) => {
74
+ onChange?.(e.getHTML());
75
+ },
76
+ });
77
+
78
+ // Sync external value changes into the editor
79
+ useEffect(() => {
80
+ if (!editor) return;
81
+ if (editor.getHTML() !== value) {
82
+ editor.commands.setContent(value, { emitUpdate: false });
83
+ }
84
+ }, [editor, value]);
85
+
86
+ // Sync editable state
87
+ useEffect(() => {
88
+ if (!editor) return;
89
+ editor.setEditable(!readOnly && !disabled);
90
+ }, [editor, readOnly, disabled]);
91
+
92
+ if (!editor) return null;
93
+
94
+ return (
95
+ <div
96
+ className={cn(
97
+ "rounded-md border border-input bg-transparent text-sm shadow-xs transition-[color,box-shadow]",
98
+ "focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
99
+ disabled && "pointer-events-none opacity-50",
100
+ className,
101
+ )}
102
+ style={style}
103
+ >
104
+ {!readOnly && !disabled && (
105
+ <RichTextToolbar editor={editor} variant={variant} />
106
+ )}
107
+ <EditorContent
108
+ editor={editor}
109
+ className={cn(
110
+ "px-3 py-2 [&_.tiptap]:min-h-16 [&_.tiptap]:outline-none",
111
+ "[&_.tiptap_p]:mb-1 [&_.tiptap_h2]:text-lg [&_.tiptap_h2]:font-semibold [&_.tiptap_h2]:mb-1",
112
+ "[&_.tiptap_ul]:list-disc [&_.tiptap_ul]:pl-5 [&_.tiptap_ol]:list-decimal [&_.tiptap_ol]:pl-5",
113
+ "[&_.tiptap_blockquote]:border-l-2 [&_.tiptap_blockquote]:border-muted-foreground/30 [&_.tiptap_blockquote]:pl-3 [&_.tiptap_blockquote]:italic",
114
+ "[&_.tiptap_pre]:rounded-md [&_.tiptap_pre]:bg-muted [&_.tiptap_pre]:p-3 [&_.tiptap_pre]:font-mono [&_.tiptap_pre]:text-sm",
115
+ )}
116
+ />
117
+ </div>
118
+ );
119
+ }
@@ -0,0 +1,157 @@
1
+ import type { Editor } from "@tiptap/react";
2
+ import {
3
+ Bold,
4
+ Italic,
5
+ Underline,
6
+ Heading2,
7
+ List,
8
+ ListOrdered,
9
+ Quote,
10
+ Link,
11
+ Code,
12
+ } from "lucide-react";
13
+ import { cn } from "../lib/utils";
14
+ import { Separator } from "./separator";
15
+
16
+ type RichTextToolbarVariant = "default" | "minimal";
17
+
18
+ interface RichTextToolbarProps {
19
+ editor: Editor;
20
+ variant?: RichTextToolbarVariant;
21
+ }
22
+
23
+ function ToolbarButton({
24
+ active,
25
+ disabled,
26
+ onClick,
27
+ title,
28
+ children,
29
+ }: {
30
+ active?: boolean;
31
+ disabled?: boolean;
32
+ onClick: () => void;
33
+ title: string;
34
+ children: React.ReactNode;
35
+ }) {
36
+ return (
37
+ <button
38
+ type="button"
39
+ onClick={onClick}
40
+ disabled={disabled}
41
+ title={title}
42
+ aria-label={title}
43
+ className={cn(
44
+ "inline-flex items-center justify-center rounded-sm p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
45
+ active && "bg-muted text-foreground",
46
+ )}
47
+ >
48
+ {children}
49
+ </button>
50
+ );
51
+ }
52
+
53
+ export function RichTextToolbar({
54
+ editor,
55
+ variant = "default",
56
+ }: RichTextToolbarProps) {
57
+ const iconSize = 16;
58
+
59
+ function handleLink() {
60
+ const previousUrl = editor.getAttributes("link").href as string;
61
+ const url = window.prompt("Enter URL", previousUrl || "https://");
62
+ if (url === null) return;
63
+ if (url === "") {
64
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
65
+ } else {
66
+ editor
67
+ .chain()
68
+ .focus()
69
+ .extendMarkRange("link")
70
+ .setLink({ href: url })
71
+ .run();
72
+ }
73
+ }
74
+
75
+ return (
76
+ <div className="flex items-center gap-0.5 border-b border-border px-2 py-1.5" role="toolbar" aria-label="Text formatting">
77
+ <ToolbarButton
78
+ active={editor.isActive("bold")}
79
+ onClick={() => editor.chain().focus().toggleBold().run()}
80
+ title="Bold"
81
+ >
82
+ <Bold size={iconSize} />
83
+ </ToolbarButton>
84
+ <ToolbarButton
85
+ active={editor.isActive("italic")}
86
+ onClick={() => editor.chain().focus().toggleItalic().run()}
87
+ title="Italic"
88
+ >
89
+ <Italic size={iconSize} />
90
+ </ToolbarButton>
91
+ <ToolbarButton
92
+ active={editor.isActive("underline")}
93
+ onClick={() => editor.chain().focus().toggleUnderline().run()}
94
+ title="Underline"
95
+ >
96
+ <Underline size={iconSize} />
97
+ </ToolbarButton>
98
+
99
+ <Separator orientation="vertical" className="mx-1 h-5" />
100
+
101
+ <ToolbarButton
102
+ active={editor.isActive("bulletList")}
103
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
104
+ title="Bullet list"
105
+ >
106
+ <List size={iconSize} />
107
+ </ToolbarButton>
108
+ <ToolbarButton
109
+ active={editor.isActive("orderedList")}
110
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
111
+ title="Ordered list"
112
+ >
113
+ <ListOrdered size={iconSize} />
114
+ </ToolbarButton>
115
+
116
+ <Separator orientation="vertical" className="mx-1 h-5" />
117
+
118
+ <ToolbarButton
119
+ active={editor.isActive("link")}
120
+ onClick={handleLink}
121
+ title="Link"
122
+ >
123
+ <Link size={iconSize} />
124
+ </ToolbarButton>
125
+
126
+ {variant === "default" && (
127
+ <>
128
+ <Separator orientation="vertical" className="mx-1 h-5" />
129
+
130
+ <ToolbarButton
131
+ active={editor.isActive("heading", { level: 2 })}
132
+ onClick={() =>
133
+ editor.chain().focus().toggleHeading({ level: 2 }).run()
134
+ }
135
+ title="Heading"
136
+ >
137
+ <Heading2 size={iconSize} />
138
+ </ToolbarButton>
139
+ <ToolbarButton
140
+ active={editor.isActive("blockquote")}
141
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
142
+ title="Blockquote"
143
+ >
144
+ <Quote size={iconSize} />
145
+ </ToolbarButton>
146
+ <ToolbarButton
147
+ active={editor.isActive("codeBlock")}
148
+ onClick={() => editor.chain().focus().toggleCodeBlock().run()}
149
+ title="Code block"
150
+ >
151
+ <Code size={iconSize} />
152
+ </ToolbarButton>
153
+ </>
154
+ )}
155
+ </div>
156
+ );
157
+ }
@@ -0,0 +1,170 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ type ReactNode,
9
+ } from "react";
10
+ import { createPortal } from "react-dom";
11
+ import { cva } from "class-variance-authority";
12
+ import { X, CheckCircle2, AlertCircle, AlertTriangle, Info } from "lucide-react";
13
+ import { cn } from "../lib/utils";
14
+
15
+ const toastVariants = cva(
16
+ "pointer-events-auto relative flex w-full items-start gap-3 rounded-lg border p-4 pr-8 shadow-lg text-sm transition-all",
17
+ {
18
+ variants: {
19
+ variant: {
20
+ success:
21
+ "border-success/50 bg-success/5 text-success [&>svg]:text-success",
22
+ error:
23
+ "border-destructive/50 bg-destructive/5 text-destructive [&>svg]:text-destructive",
24
+ warning:
25
+ "border-warning/50 bg-warning/5 text-warning [&>svg]:text-warning",
26
+ info: "border-info/50 bg-info/5 text-info [&>svg]:text-info",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "info",
31
+ },
32
+ },
33
+ );
34
+
35
+ type ToastVariant = "success" | "error" | "warning" | "info";
36
+
37
+ /** Options for creating a toast notification. */
38
+ interface ToastOptions {
39
+ /** Toast message */
40
+ message: string;
41
+ /** Optional title */
42
+ title?: string;
43
+ /** Visual variant */
44
+ variant?: ToastVariant;
45
+ /** Auto-dismiss duration in ms (default 5000, 0 = no auto-dismiss) */
46
+ duration?: number;
47
+ }
48
+
49
+ interface ToastItem extends ToastOptions {
50
+ id: string;
51
+ }
52
+
53
+ interface ToastContextValue {
54
+ toast: (options: ToastOptions) => void;
55
+ }
56
+
57
+ const ToastContext = createContext<ToastContextValue | null>(null);
58
+
59
+ const VARIANT_ICONS: Record<ToastVariant, React.ElementType> = {
60
+ success: CheckCircle2,
61
+ error: AlertCircle,
62
+ warning: AlertTriangle,
63
+ info: Info,
64
+ };
65
+
66
+ function ToastCard({
67
+ item,
68
+ onDismiss,
69
+ }: {
70
+ item: ToastItem;
71
+ onDismiss: (id: string) => void;
72
+ }) {
73
+ const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
74
+ const duration = item.duration ?? 5000;
75
+ const Icon = VARIANT_ICONS[item.variant ?? "info"];
76
+
77
+ useEffect(() => {
78
+ if (duration > 0) {
79
+ timerRef.current = setTimeout(() => onDismiss(item.id), duration);
80
+ }
81
+ return () => clearTimeout(timerRef.current);
82
+ }, [duration, item.id, onDismiss]);
83
+
84
+ return (
85
+ <div
86
+ data-slot="toast"
87
+ role="status"
88
+ aria-live="polite"
89
+ className={cn(toastVariants({ variant: item.variant ?? "info" }))}
90
+ >
91
+ <Icon className="size-4 shrink-0 translate-y-0.5" />
92
+ <div className="flex-1 space-y-1">
93
+ {item.title && (
94
+ <p className="font-medium leading-none">{item.title}</p>
95
+ )}
96
+ <p className="text-sm opacity-90">{item.message}</p>
97
+ </div>
98
+ <button
99
+ type="button"
100
+ onClick={() => onDismiss(item.id)}
101
+ aria-label="Dismiss notification"
102
+ className="absolute right-2 top-2 rounded-md p-1 opacity-70 hover:opacity-100 transition-opacity"
103
+ >
104
+ <X className="size-3.5" />
105
+ </button>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ const MAX_VISIBLE = 3;
111
+ let idCounter = 0;
112
+
113
+ /**
114
+ * ToastProvider wraps your app to enable toast notifications.
115
+ * Renders a fixed-position toast container via portal.
116
+ *
117
+ * @example
118
+ * <ToastProvider>
119
+ * <App />
120
+ * </ToastProvider>
121
+ */
122
+ function ToastProvider({ children }: { children: ReactNode }) {
123
+ const [toasts, setToasts] = useState<ToastItem[]>([]);
124
+
125
+ const toast = useCallback((options: ToastOptions) => {
126
+ const id = `toast-${++idCounter}`;
127
+ setToasts((prev) => [...prev.slice(-(MAX_VISIBLE - 1)), { ...options, id }]);
128
+ }, []);
129
+
130
+ const dismiss = useCallback((id: string) => {
131
+ setToasts((prev) => prev.filter((t) => t.id !== id));
132
+ }, []);
133
+
134
+ return (
135
+ <ToastContext.Provider value={{ toast }}>
136
+ {children}
137
+ {typeof document !== "undefined" &&
138
+ createPortal(
139
+ <div
140
+ data-slot="toaster"
141
+ className="fixed top-4 right-4 z-50 flex flex-col gap-2 w-80 pointer-events-none"
142
+ >
143
+ {toasts.map((item) => (
144
+ <ToastCard key={item.id} item={item} onDismiss={dismiss} />
145
+ ))}
146
+ </div>,
147
+ document.body,
148
+ )}
149
+ </ToastContext.Provider>
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Hook to trigger toast notifications. Must be used within a ToastProvider.
155
+ *
156
+ * @example
157
+ * const { toast } = useToast();
158
+ * toast({ message: "Saved!", variant: "success" });
159
+ */
160
+ function useToast(): ToastContextValue {
161
+ const ctx = useContext(ToastContext);
162
+ if (!ctx) throw new Error("useToast must be used within a ToastProvider");
163
+ return ctx;
164
+ }
165
+
166
+ /** Convenience alias for ToastProvider. */
167
+ const Toaster = ToastProvider;
168
+
169
+ export { ToastProvider, Toaster, useToast, toastVariants };
170
+ export type { ToastOptions, ToastVariant };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Group an array of items by a key derived from each item.
3
+ */
4
+ export function groupBy<T>(
5
+ arr: T[],
6
+ keyFn: (item: T) => string,
7
+ ): Record<string, T[]> {
8
+ const result: Record<string, T[]> = {};
9
+ for (const item of arr) {
10
+ const key = keyFn(item);
11
+ if (!result[key]) {
12
+ result[key] = [];
13
+ }
14
+ result[key].push(item);
15
+ }
16
+ return result;
17
+ }
@@ -1,10 +1,16 @@
1
+ type Cancellable<T extends (...args: Parameters<T>) => void> = ((
2
+ ...args: Parameters<T>
3
+ ) => void) & { cancel: () => void };
4
+
1
5
  export function debounce<T extends (...args: Parameters<T>) => void>(
2
6
  fn: T,
3
7
  delay: number,
4
- ): (...args: Parameters<T>) => void {
8
+ ): Cancellable<T> {
5
9
  let timer: ReturnType<typeof setTimeout>;
6
- return (...args: Parameters<T>) => {
10
+ const debounced = (...args: Parameters<T>) => {
7
11
  clearTimeout(timer);
8
12
  timer = setTimeout(() => fn(...args), delay);
9
13
  };
14
+ debounced.cancel = () => clearTimeout(timer);
15
+ return debounced;
10
16
  }
@@ -0,0 +1,17 @@
1
+ import type { CurriculumItem } from "../curriculum/types";
2
+
3
+ /**
4
+ * Recursively collects the UIDs of all leaf nodes (items with no children)
5
+ * in a curriculum tree.
6
+ */
7
+ export function flattenLeaves(items: CurriculumItem[]): string[] {
8
+ const leaves: string[] = [];
9
+ for (const item of items) {
10
+ if (!item.children || item.children.length === 0) {
11
+ leaves.push(item.uid);
12
+ } else {
13
+ leaves.push(...flattenLeaves(item.children));
14
+ }
15
+ }
16
+ return leaves;
17
+ }
@@ -0,0 +1,5 @@
1
+ export function formatFileSize(bytes: number): string {
2
+ if (bytes < 1024) return `${bytes} B`;
3
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
4
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5
+ }
@@ -0,0 +1,13 @@
1
+ export function formatTimestamp(iso: string): string {
2
+ const date = new Date(iso);
3
+ const now = new Date();
4
+ const diffMs = now.getTime() - date.getTime();
5
+ const diffMins = Math.floor(diffMs / 60000);
6
+ if (diffMins < 1) return "Just now";
7
+ if (diffMins < 60) return `${diffMins}m ago`;
8
+ const diffHours = Math.floor(diffMins / 60);
9
+ if (diffHours < 24) return `${diffHours}h ago`;
10
+ const diffDays = Math.floor(diffHours / 24);
11
+ if (diffDays < 7) return `${diffDays}d ago`;
12
+ return date.toLocaleDateString();
13
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns true if an HTML string contains no meaningful text content.
3
+ * Tiptap returns `"<p></p>"` for an empty editor, not an empty string.
4
+ */
5
+ export function isEmptyHtml(html: string): boolean {
6
+ return html.replace(/<[^>]*>/g, "").trim().length === 0;
7
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Palette CSS variable names matching --palette-0 through --palette-3
3
+ * defined in globals.css.
4
+ */
5
+ export const PALETTE_VARS = [
6
+ "var(--palette-0)",
7
+ "var(--palette-1)",
8
+ "var(--palette-2)",
9
+ "var(--palette-3)",
10
+ ] as const;
11
+
12
+ /** Number of palette colors available. */
13
+ export const PALETTE_COUNT = PALETTE_VARS.length;
14
+
15
+ /**
16
+ * Pick a palette CSS variable by cycling through the 4 palette colors.
17
+ * Useful for assigning varied colors to list items by index.
18
+ */
19
+ export function pickPaletteColor(index: number): string {
20
+ return PALETTE_VARS[((index % PALETTE_COUNT) + PALETTE_COUNT) % PALETTE_COUNT];
21
+ }
22
+
23
+ /** Palette variant name for CVA-based components (badge, progress). */
24
+ export type PaletteVariant = "palette0" | "palette1" | "palette2" | "palette3";
25
+
26
+ /**
27
+ * Pick a palette variant name by cycling through the 4 palette variants.
28
+ * Useful for assigning varied CVA variants to list items by index.
29
+ */
30
+ export function pickPaletteVariant(index: number): PaletteVariant {
31
+ const i = ((index % PALETTE_COUNT) + PALETTE_COUNT) % PALETTE_COUNT;
32
+ return `palette${i}` as PaletteVariant;
33
+ }
@@ -0,0 +1,8 @@
1
+ export function shuffle<T>(array: T[]): T[] {
2
+ const shuffled = [...array];
3
+ for (let i = shuffled.length - 1; i > 0; i--) {
4
+ const j = Math.floor(Math.random() * (i + 1));
5
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
6
+ }
7
+ return shuffled;
8
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Truncate text to a maximum length, appending a suffix if truncated.
3
+ */
4
+ export function truncateText(
5
+ text: string,
6
+ maxLength: number,
7
+ suffix = "...",
8
+ ): string {
9
+ if (text.length <= maxLength) return text;
10
+ return text.slice(0, maxLength - suffix.length) + suffix;
11
+ }
12
+
13
+ /**
14
+ * Format a decimal value as a percentage string.
15
+ */
16
+ export function formatPercentage(value: number, decimals = 0): string {
17
+ return `${value.toFixed(decimals)}%`;
18
+ }
19
+
20
+ /**
21
+ * Return the singular or plural form of a word based on count.
22
+ */
23
+ export function pluralize(
24
+ count: number,
25
+ singular: string,
26
+ plural?: string,
27
+ ): string {
28
+ if (count === 1) return singular;
29
+ return plural ?? `${singular}s`;
30
+ }
@@ -9,6 +9,20 @@
9
9
  * onEnded={() => markLessonComplete()}
10
10
  * />
11
11
  */
12
+ /** A text track (captions, subtitles) for the video element. */
13
+ export interface VideoTrack {
14
+ /** URL of the track file (e.g., .vtt) */
15
+ src: string;
16
+ /** Track kind */
17
+ kind: "captions" | "subtitles" | "descriptions" | "chapters" | "metadata";
18
+ /** Human-readable track label (e.g., "English") */
19
+ label: string;
20
+ /** BCP 47 language code (e.g., "en") */
21
+ srcLang: string;
22
+ /** Whether this track should be active by default */
23
+ default?: boolean;
24
+ }
25
+
12
26
  export interface VideoPlayerProps {
13
27
  /** URL of the video source */
14
28
  src?: string;
@@ -30,6 +44,8 @@ export interface VideoPlayerProps {
30
44
  readOnly?: boolean;
31
45
  /** Aspect ratio of the video container */
32
46
  aspectRatio?: "16/9" | "4/3" | "1/1";
47
+ /** Array of text tracks (captions, subtitles) for the video */
48
+ tracks?: VideoTrack[];
33
49
  /** CSS class name for the root element */
34
50
  className?: string;
35
51
  /** Inline styles for the root element */
@@ -1,16 +1,17 @@
1
+ import { memo } from "react";
1
2
  import { Bookmark, Pencil, Trash2 } from "lucide-react";
2
3
  import type { VideoBookmarkProps } from "./types";
3
4
  import { cn } from "../lib/utils";
4
5
  import { formatTimer } from "../utils/format-duration";
5
6
 
6
- export const VideoBookmark = ({
7
+ export const VideoBookmark = memo(function VideoBookmark({
7
8
  bookmark,
8
9
  onSeek,
9
10
  onDelete,
10
11
  onEdit,
11
12
  className,
12
13
  style,
13
- }: VideoBookmarkProps) => {
14
+ }: VideoBookmarkProps) {
14
15
  return (
15
16
  <div
16
17
  className={cn(
@@ -73,4 +74,4 @@ export const VideoBookmark = ({
73
74
  )}
74
75
  </div>
75
76
  );
76
- };
77
+ });