@hydralms/components 0.2.0 → 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 (283) hide show
  1. package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
  2. package/dist/StudentProfile-DeMxdrL3.js +3275 -0
  3. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  4. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  5. package/dist/common/index.d.ts +2 -1
  6. package/dist/common/pagination.d.ts +26 -0
  7. package/dist/common/types.d.ts +1 -0
  8. package/dist/components.css +1 -1
  9. package/dist/content/audio-player.d.ts +22 -0
  10. package/dist/content/code-block.d.ts +30 -0
  11. package/dist/content/embed-block.d.ts +28 -0
  12. package/dist/content/index.d.ts +6 -0
  13. package/dist/content/types.d.ts +24 -0
  14. package/dist/curriculum/course-card.d.ts +51 -0
  15. package/dist/curriculum/index.d.ts +2 -0
  16. package/dist/curriculum/types.d.ts +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +494 -444
  20. package/dist/license/HydraContext.d.ts +16 -0
  21. package/dist/license/ProBadge.d.ts +6 -0
  22. package/dist/license/index.d.ts +7 -0
  23. package/dist/license/tiers.d.ts +3 -0
  24. package/dist/license/useHydraLicense.d.ts +6 -0
  25. package/dist/license/validate.d.ts +13 -0
  26. package/dist/license/withProGate.d.ts +6 -0
  27. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +4 -7
  28. package/dist/modules/AssignmentModule/types.d.ts +5 -1
  29. package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
  30. package/dist/modules/CertificateModule/types.d.ts +6 -4
  31. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  32. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  33. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  34. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +4 -7
  35. package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
  36. package/dist/modules/ExamModule/types.d.ts +5 -14
  37. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  38. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  39. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +4 -8
  40. package/dist/modules/GradeCenterModule/types.d.ts +2 -0
  41. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  42. package/dist/modules/QuizModule/types.d.ts +5 -14
  43. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  44. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  45. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  46. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  47. package/dist/modules/SurveyModule/SurveyModule.d.ts +4 -6
  48. package/dist/modules/SurveyModule/types.d.ts +2 -0
  49. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  50. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  51. package/dist/modules/_shared/types.d.ts +10 -0
  52. package/dist/modules/_shared/use-timer.d.ts +9 -0
  53. package/dist/modules/index.d.ts +6 -0
  54. package/dist/modules.cjs +1 -1
  55. package/dist/modules.js +1266 -854
  56. package/dist/progress/types.d.ts +2 -0
  57. package/dist/provider/HydraProvider.d.ts +5 -1
  58. package/dist/questions/choice.d.ts +1 -1
  59. package/dist/questions/confidence-indicator.d.ts +37 -0
  60. package/dist/questions/essay.d.ts +1 -1
  61. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  62. package/dist/questions/hotspot.d.ts +1 -1
  63. package/dist/questions/index.d.ts +2 -0
  64. package/dist/questions/inline-choice.d.ts +1 -1
  65. package/dist/questions/matching.d.ts +1 -1
  66. package/dist/questions/multiple-choice.d.ts +1 -1
  67. package/dist/questions/numeric.d.ts +1 -1
  68. package/dist/questions/ordering.d.ts +1 -1
  69. package/dist/questions/question-renderer.d.ts +1 -1
  70. package/dist/questions/scenario.d.ts +1 -1
  71. package/dist/questions/spreadsheet.d.ts +1 -1
  72. package/dist/questions/true-false.d.ts +1 -1
  73. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  74. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  75. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  76. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  77. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  78. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  79. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  80. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  81. package/dist/sections/CertificateViewer/types.d.ts +6 -0
  82. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  83. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  84. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  85. package/dist/sections/CourseOutline/types.d.ts +6 -0
  86. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  87. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  88. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  89. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  90. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  91. package/dist/sections/ExamSession/types.d.ts +6 -0
  92. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  93. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  94. package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
  95. package/dist/sections/ForumBoard/types.d.ts +14 -0
  96. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  97. package/dist/sections/GradebookTable/types.d.ts +14 -0
  98. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  99. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  100. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  101. package/dist/sections/LessonPage/types.d.ts +6 -0
  102. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  103. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  104. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  105. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  106. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  107. package/dist/sections/QuizSession/types.d.ts +6 -0
  108. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
  109. package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
  110. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  111. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  112. package/dist/sections/RubricView/RubricView.d.ts +1 -1
  113. package/dist/sections/RubricView/types.d.ts +6 -0
  114. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  115. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  116. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  117. package/dist/sections/StudentProfile/types.d.ts +98 -0
  118. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  119. package/dist/sections/SurveyForm/types.d.ts +6 -0
  120. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  121. package/dist/sections/_shared/section-shell.d.ts +20 -0
  122. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  123. package/dist/sections/index.d.ts +6 -0
  124. package/dist/sections.cjs +1 -1
  125. package/dist/sections.js +268 -307
  126. package/dist/tabs-BsfVo2Bl.cjs +173 -0
  127. package/dist/{tabs-Wf3h_Cx3.js → tabs-BuY1iNJE.js} +7532 -6807
  128. package/dist/ui/badge.d.ts +1 -1
  129. package/dist/ui/index.d.ts +2 -0
  130. package/dist/ui/progress.d.ts +1 -1
  131. package/dist/ui/rich-text-editor.d.ts +3 -1
  132. package/dist/ui/toast.d.ts +43 -0
  133. package/dist/utils/debounce.d.ts +5 -1
  134. package/dist/utils/pick-palette-color.d.ts +19 -0
  135. package/dist/video/types.d.ts +15 -0
  136. package/dist/video/video-player.d.ts +1 -1
  137. package/dist/withProGate-BWqcKdPM.js +137 -0
  138. package/dist/withProGate-DX6XqKLp.cjs +1 -0
  139. package/package.json +34 -220
  140. package/src/assessment-toolbar/question-navigator.tsx +10 -5
  141. package/src/assessment-toolbar/timer-display.tsx +4 -3
  142. package/src/assessment-toolbar/use-countdown.ts +1 -1
  143. package/src/common/empty-state.tsx +1 -0
  144. package/src/common/index.ts +2 -0
  145. package/src/common/pagination.tsx +135 -0
  146. package/src/common/search-input.tsx +2 -1
  147. package/src/common/types.ts +2 -0
  148. package/src/content/attachment-list.tsx +2 -0
  149. package/src/content/audio-player.tsx +196 -0
  150. package/src/content/code-block.tsx +113 -0
  151. package/src/content/content-block.tsx +64 -0
  152. package/src/content/embed-block.tsx +78 -0
  153. package/src/content/file-upload-zone.tsx +10 -0
  154. package/src/content/index.ts +6 -0
  155. package/src/content/types.ts +5 -0
  156. package/src/curriculum/course-card.tsx +199 -0
  157. package/src/curriculum/curriculum-item.tsx +3 -3
  158. package/src/curriculum/curriculum-tree.tsx +20 -13
  159. package/src/curriculum/index.ts +2 -0
  160. package/src/curriculum/types.ts +2 -2
  161. package/src/flashcards/flashcard.tsx +28 -8
  162. package/src/index.ts +3 -0
  163. package/src/license/HydraContext.tsx +62 -0
  164. package/src/license/ProBadge.tsx +43 -0
  165. package/src/license/index.ts +7 -0
  166. package/src/license/tiers.ts +24 -0
  167. package/src/license/useHydraLicense.ts +10 -0
  168. package/src/license/validate.ts +90 -0
  169. package/src/license/withProGate.tsx +21 -0
  170. package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
  171. package/src/modules/AssignmentModule/types.ts +5 -1
  172. package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
  173. package/src/modules/CertificateModule/types.ts +6 -4
  174. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  175. package/src/modules/CourseCatalogModule/types.ts +47 -0
  176. package/src/modules/CoursePlayer/CoursePlayer.tsx +37 -22
  177. package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
  178. package/src/modules/ExamModule/ExamModule.tsx +64 -198
  179. package/src/modules/ExamModule/types.ts +5 -14
  180. package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
  181. package/src/modules/FlashcardLab/types.ts +2 -0
  182. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
  183. package/src/modules/GradeCenterModule/types.ts +2 -0
  184. package/src/modules/QuizModule/QuizModule.tsx +49 -169
  185. package/src/modules/QuizModule/types.ts +5 -15
  186. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  187. package/src/modules/StudentDashboardModule/types.ts +56 -0
  188. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  189. package/src/modules/StudentProfileModule/types.ts +45 -0
  190. package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
  191. package/src/modules/SurveyModule/types.ts +2 -0
  192. package/src/modules/_shared/assessment-intro.tsx +75 -0
  193. package/src/modules/_shared/assessment-results.tsx +133 -0
  194. package/src/modules/_shared/types.ts +11 -0
  195. package/src/modules/_shared/use-timer.ts +49 -0
  196. package/src/modules/index.ts +9 -0
  197. package/src/progress/achievement-badge.tsx +3 -3
  198. package/src/progress/grade-indicator.tsx +9 -1
  199. package/src/progress/progress-ring.tsx +2 -1
  200. package/src/progress/stat-card.tsx +8 -1
  201. package/src/progress/types.ts +2 -0
  202. package/src/provider/HydraProvider.tsx +15 -6
  203. package/src/questions/choice.tsx +13 -6
  204. package/src/questions/confidence-indicator.tsx +107 -0
  205. package/src/questions/essay.tsx +6 -4
  206. package/src/questions/fill-in-the-blank.tsx +8 -4
  207. package/src/questions/hotspot.tsx +4 -4
  208. package/src/questions/index.ts +2 -0
  209. package/src/questions/inline-choice.tsx +5 -4
  210. package/src/questions/matching.tsx +5 -4
  211. package/src/questions/multiple-choice.tsx +13 -6
  212. package/src/questions/numeric.tsx +8 -4
  213. package/src/questions/ordering.tsx +12 -4
  214. package/src/questions/question-renderer.tsx +3 -2
  215. package/src/questions/scenario.tsx +4 -4
  216. package/src/questions/spreadsheet.tsx +5 -4
  217. package/src/questions/true-false.tsx +13 -6
  218. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
  219. package/src/sections/AnnouncementFeed/types.ts +15 -1
  220. package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
  221. package/src/sections/AssessmentReview/types.ts +6 -0
  222. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
  223. package/src/sections/AssignmentSubmission/types.ts +6 -0
  224. package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
  225. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  226. package/src/sections/CertificateViewer/types.ts +6 -0
  227. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  228. package/src/sections/CourseCatalog/types.ts +76 -0
  229. package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
  230. package/src/sections/CourseOutline/types.ts +6 -0
  231. package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
  232. package/src/sections/DiscussionThread/types.ts +6 -0
  233. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  234. package/src/sections/EnrollmentWizard/types.ts +65 -0
  235. package/src/sections/ExamSession/ExamSession.tsx +100 -94
  236. package/src/sections/ExamSession/types.ts +6 -0
  237. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  238. package/src/sections/FlashcardStudySession/types.ts +6 -0
  239. package/src/sections/ForumBoard/ForumBoard.tsx +59 -1
  240. package/src/sections/ForumBoard/types.ts +14 -0
  241. package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
  242. package/src/sections/GradebookTable/types.ts +14 -0
  243. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  244. package/src/sections/LecturePlayer/types.ts +8 -0
  245. package/src/sections/LessonPage/LessonPage.tsx +36 -5
  246. package/src/sections/LessonPage/types.ts +6 -0
  247. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
  248. package/src/sections/PracticeQuiz/types.ts +6 -0
  249. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
  250. package/src/sections/ProgressDashboard/types.ts +6 -0
  251. package/src/sections/QuizSession/QuizSession.tsx +71 -82
  252. package/src/sections/QuizSession/types.ts +6 -0
  253. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
  254. package/src/sections/RequirementsChecklist/types.ts +6 -0
  255. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
  256. package/src/sections/ResourceLibrary/types.ts +15 -1
  257. package/src/sections/RubricView/RubricView.tsx +37 -1
  258. package/src/sections/RubricView/types.ts +6 -0
  259. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
  260. package/src/sections/ScrollableQuiz/types.ts +6 -0
  261. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  262. package/src/sections/StudentProfile/types.ts +99 -0
  263. package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
  264. package/src/sections/SurveyForm/types.ts +6 -0
  265. package/src/sections/_shared/merge-answers.ts +22 -0
  266. package/src/sections/_shared/section-shell.tsx +64 -0
  267. package/src/sections/_shared/use-assessment-session.ts +125 -0
  268. package/src/sections/index.ts +22 -0
  269. package/src/social/user-avatar.tsx +9 -5
  270. package/src/styles/globals.css +39 -41
  271. package/src/ui/badge.tsx +8 -0
  272. package/src/ui/index.ts +2 -0
  273. package/src/ui/progress.tsx +4 -0
  274. package/src/ui/rich-text-editor.tsx +10 -0
  275. package/src/ui/rich-text-toolbar.tsx +2 -1
  276. package/src/ui/toast.tsx +170 -0
  277. package/src/utils/debounce.ts +8 -2
  278. package/src/utils/pick-palette-color.ts +33 -0
  279. package/src/video/types.ts +16 -0
  280. package/src/video/video-player.tsx +13 -1
  281. package/dist/ForumBoard-CHXU3mjC.js +0 -2207
  282. package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
  283. package/dist/tabs-DRM2Iq_J.cjs +0 -172
@@ -39,7 +39,17 @@ export function FileUploadZone({
39
39
  isDragging && "border-primary bg-muted",
40
40
  disabled && "cursor-default opacity-50",
41
41
  atLimit && "cursor-default",
42
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
42
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
+ }}
43
53
  onDragOver={(e) => {
44
54
  e.preventDefault();
45
55
  if (!disabled && !atLimit) setIsDragging(true);
@@ -8,3 +8,9 @@ export type {
8
8
  AttachmentListProps,
9
9
  AttachmentFile,
10
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
 
@@ -0,0 +1,199 @@
1
+ import { memo } from "react";
2
+ import { Clock, Lock, Users } from "lucide-react";
3
+ import { Card, CardContent } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { Button } from "../ui/button";
6
+ import { UserAvatar } from "../social/user-avatar";
7
+ import { ProgressRing } from "../progress/progress-ring";
8
+ import { cn } from "../lib/utils";
9
+
10
+ /**
11
+ * CourseCard displays course info in a card — thumbnail, title, instructor,
12
+ * progress, category, enrollment status, and CTA.
13
+ *
14
+ * @example
15
+ * <CourseCard
16
+ * uid="course-1"
17
+ * title="React Fundamentals"
18
+ * description="Learn the basics of React."
19
+ * instructor={{ displayName: "Jane Doe" }}
20
+ * progress={65}
21
+ * enrollmentStatus="enrolled"
22
+ * onClick={() => navigate("/courses/course-1")}
23
+ * />
24
+ */
25
+ export interface CourseCardProps {
26
+ /** Unique identifier */
27
+ uid: string;
28
+ /** Course title */
29
+ title: string;
30
+ /** Course description */
31
+ description?: string;
32
+ /** Thumbnail image URL */
33
+ thumbnailUrl?: string;
34
+ /** Instructor info */
35
+ instructor?: { displayName: string; avatarUrl?: string };
36
+ /** Category label */
37
+ category?: string;
38
+ /** Progress percentage (0-100), shown when enrolled/completed */
39
+ progress?: number;
40
+ /** Enrollment status */
41
+ enrollmentStatus?: "enrolled" | "completed" | "available" | "locked";
42
+ /** Number of enrolled students */
43
+ studentCount?: number;
44
+ /** Estimated duration (e.g. "12 hours") */
45
+ duration?: string;
46
+ /** Called when the card is clicked */
47
+ onClick?: () => void;
48
+ /** Called when the enroll button is clicked */
49
+ onEnroll?: () => void;
50
+ /** Layout orientation */
51
+ layout?: "vertical" | "horizontal";
52
+ /** CSS class name for the root element */
53
+ className?: string;
54
+ /** Inline styles for the root element */
55
+ style?: React.CSSProperties;
56
+ }
57
+
58
+ const STATUS_BADGE: Record<string, { label: string; variant: "success" | "default" | "muted" | "destructive" }> = {
59
+ enrolled: { label: "Enrolled", variant: "default" },
60
+ completed: { label: "Completed", variant: "success" },
61
+ available: { label: "Available", variant: "muted" },
62
+ locked: { label: "Locked", variant: "destructive" },
63
+ };
64
+
65
+ export const CourseCard = memo(function CourseCard({
66
+ title,
67
+ description,
68
+ thumbnailUrl,
69
+ instructor,
70
+ category,
71
+ progress,
72
+ enrollmentStatus = "available",
73
+ studentCount,
74
+ duration,
75
+ onClick,
76
+ onEnroll,
77
+ layout = "vertical",
78
+ className,
79
+ style,
80
+ }: CourseCardProps) {
81
+ const isHorizontal = layout === "horizontal";
82
+ const statusInfo = STATUS_BADGE[enrollmentStatus];
83
+ const showProgress = (enrollmentStatus === "enrolled" || enrollmentStatus === "completed") && progress != null;
84
+ const isLocked = enrollmentStatus === "locked";
85
+
86
+ function handleEnrollClick(e: React.MouseEvent) {
87
+ e.stopPropagation();
88
+ onEnroll?.();
89
+ }
90
+
91
+ return (
92
+ <Card
93
+ data-slot="course-card"
94
+ className={cn(
95
+ "overflow-hidden transition-colors",
96
+ onClick && "cursor-pointer hover:bg-muted/30",
97
+ isLocked && "opacity-70",
98
+ className,
99
+ )}
100
+ style={style}
101
+ onClick={onClick}
102
+ >
103
+ <div className={cn(isHorizontal ? "flex flex-row" : "flex flex-col")}>
104
+ {/* Thumbnail */}
105
+ {thumbnailUrl ? (
106
+ <div
107
+ className={cn(
108
+ "bg-muted bg-cover bg-center shrink-0",
109
+ isHorizontal ? "w-40 min-h-full" : "h-36 w-full",
110
+ )}
111
+ style={{ backgroundImage: `url(${thumbnailUrl})` }}
112
+ />
113
+ ) : (
114
+ <div
115
+ className={cn(
116
+ "bg-muted flex items-center justify-center shrink-0",
117
+ isHorizontal ? "w-40 min-h-full" : "h-36 w-full",
118
+ )}
119
+ >
120
+ <span className="text-3xl text-muted-foreground/40 font-bold">
121
+ {title.charAt(0)}
122
+ </span>
123
+ </div>
124
+ )}
125
+
126
+ {/* Content */}
127
+ <CardContent className={cn("flex-1 min-w-0", isHorizontal ? "py-3 px-4" : "pt-3 pb-4")}>
128
+ <div className="flex items-start justify-between gap-2 mb-1">
129
+ <h3 className="font-semibold text-foreground text-sm leading-tight line-clamp-2">
130
+ {title}
131
+ </h3>
132
+ {showProgress && (
133
+ <ProgressRing
134
+ value={progress}
135
+ size={32}
136
+ strokeWidth={3}
137
+ color={enrollmentStatus === "completed" ? "var(--success)" : "var(--primary)"}
138
+ className="shrink-0"
139
+ />
140
+ )}
141
+ </div>
142
+
143
+ {description && (
144
+ <p className="text-xs text-muted-foreground line-clamp-2 mb-2">
145
+ {description}
146
+ </p>
147
+ )}
148
+
149
+ {/* Meta row */}
150
+ <div className="flex items-center gap-2 flex-wrap mb-2">
151
+ {category && (
152
+ <Badge variant="outline" className="text-[10px] px-1.5 py-0">
153
+ {category}
154
+ </Badge>
155
+ )}
156
+ <Badge variant={statusInfo.variant} className="text-[10px] px-1.5 py-0">
157
+ {isLocked && <Lock className="size-2.5 mr-0.5" />}
158
+ {statusInfo.label}
159
+ </Badge>
160
+ </div>
161
+
162
+ {/* Footer */}
163
+ <div className="flex items-center justify-between gap-2">
164
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
165
+ {instructor && (
166
+ <span className="flex items-center gap-1">
167
+ <UserAvatar
168
+ displayName={instructor.displayName}
169
+ avatarUrl={instructor.avatarUrl}
170
+ size="small"
171
+ />
172
+ <span className="truncate max-w-24">{instructor.displayName}</span>
173
+ </span>
174
+ )}
175
+ {duration && (
176
+ <span className="flex items-center gap-0.5">
177
+ <Clock className="size-3" />
178
+ {duration}
179
+ </span>
180
+ )}
181
+ {studentCount != null && (
182
+ <span className="flex items-center gap-0.5">
183
+ <Users className="size-3" />
184
+ {studentCount}
185
+ </span>
186
+ )}
187
+ </div>
188
+
189
+ {onEnroll && enrollmentStatus === "available" && (
190
+ <Button size="xs" onClick={handleEnrollClick}>
191
+ Enroll
192
+ </Button>
193
+ )}
194
+ </div>
195
+ </CardContent>
196
+ </div>
197
+ </Card>
198
+ );
199
+ });
@@ -30,10 +30,10 @@ export const CurriculumItemRow = memo(function CurriculumItemRow({
30
30
  isActive && "bg-secondary",
31
31
  )}
32
32
  style={{ paddingLeft: `${(level * 20) + 8}px` }}
33
- onClick={onClick}
33
+ onClick={onClick ? () => onClick(item) : undefined}
34
34
  role={onClick ? "button" : undefined}
35
35
  tabIndex={onClick ? 0 : undefined}
36
- onKeyDown={onClick ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } } : undefined}
36
+ onKeyDown={onClick ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(item); } } : undefined}
37
37
  >
38
38
  <div className="shrink-0 flex items-center justify-center size-5">
39
39
  {showProgress && isCompleted ? (
@@ -70,7 +70,7 @@ export const CurriculumItemRow = memo(function CurriculumItemRow({
70
70
  aria-label={isExpanded ? "Collapse" : "Expand"}
71
71
  onClick={(e) => {
72
72
  e.stopPropagation();
73
- onToggleExpand?.();
73
+ onToggleExpand?.(item.uid);
74
74
  }}
75
75
  >
76
76
  {isExpanded ? (
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useCallback, useMemo, useState } from "react";
2
2
  import { CurriculumItemRow } from "./curriculum-item";
3
3
  import type { CurriculumTreeProps, CurriculumItem } from "./types";
4
4
 
@@ -14,18 +14,25 @@ export const CurriculumTree = ({
14
14
  }: CurriculumTreeProps) => {
15
15
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
16
16
 
17
- if (!items || items.length === 0) {
18
- return null;
19
- }
17
+ const completedUids = useMemo(() => {
18
+ if (!progress || !showProgress) return new Set<string>();
19
+ return new Set(
20
+ progress.filter((p) => p.isCompleted).map((p) => p.resourceUid),
21
+ );
22
+ }, [progress, showProgress]);
20
23
 
21
- const handleToggleExpand = (uid: string) => {
24
+ const handleToggleExpand = useCallback((uid: string) => {
22
25
  setExpanded((prev) => ({ ...prev, [uid]: !prev[uid] }));
23
- };
26
+ }, []);
24
27
 
25
- const isItemCompleted = (uid: string): boolean => {
26
- if (!progress || !showProgress) return false;
27
- return progress.some((p) => p.resourceUid === uid && p.isCompleted);
28
- };
28
+ const handleItemClick = useCallback(
29
+ (item: CurriculumItem) => { onItemClick?.(item); },
30
+ [onItemClick],
31
+ );
32
+
33
+ if (!items || items.length === 0) {
34
+ return null;
35
+ }
29
36
 
30
37
  const renderItems = (
31
38
  nodeItems: CurriculumItem[],
@@ -43,11 +50,11 @@ export const CurriculumTree = ({
43
50
  item={item}
44
51
  level={level}
45
52
  isActive={activeItemUid === item.uid}
46
- isCompleted={isItemCompleted(item.uid)}
53
+ isCompleted={completedUids.has(item.uid)}
47
54
  isExpanded={isExpanded}
48
55
  hasChildren={hasChildren}
49
- onToggleExpand={() => handleToggleExpand(item.uid)}
50
- onClick={readOnly ? undefined : () => onItemClick?.(item)}
56
+ onToggleExpand={handleToggleExpand}
57
+ onClick={readOnly ? undefined : handleItemClick}
51
58
  showDuration={showDuration}
52
59
  showIcon={showIcons}
53
60
  showProgress={showProgress}
@@ -1,6 +1,7 @@
1
1
  export { CurriculumTree } from "./curriculum-tree";
2
2
  export { CurriculumItemRow } from "./curriculum-item";
3
3
  export { LearningObjectIcon } from "./learning-object-icon";
4
+ export { CourseCard } from "./course-card";
4
5
 
5
6
  export type {
6
7
  CurriculumItem,
@@ -9,3 +10,4 @@ export type {
9
10
  CurriculumItemRowProps,
10
11
  LearningObjectIconProps,
11
12
  } from "./types";
13
+ export type { CourseCardProps } from "./course-card";
@@ -47,8 +47,8 @@ export interface CurriculumItemRowProps {
47
47
  isCompleted?: boolean;
48
48
  isExpanded?: boolean;
49
49
  hasChildren: boolean;
50
- onToggleExpand?: () => void;
51
- onClick?: () => void;
50
+ onToggleExpand?: (uid: string) => void;
51
+ onClick?: (item: CurriculumItem) => void;
52
52
  showDuration?: boolean;
53
53
  showIcon?: boolean;
54
54
  showProgress?: boolean;
@@ -4,12 +4,12 @@ import type { FlashcardProps } from "./types";
4
4
  import { cn } from "../lib/utils";
5
5
 
6
6
  const COLOR_MAP: Record<string, { bg: string; border: string; accent: string }> = {
7
- color1: { bg: "bg-blue-50 dark:bg-blue-950/40", border: "border-blue-200 dark:border-blue-800", accent: "bg-blue-200 dark:bg-blue-800" },
8
- color2: { bg: "bg-violet-50 dark:bg-violet-950/40", border: "border-violet-200 dark:border-violet-800", accent: "bg-violet-200 dark:bg-violet-800" },
9
- color3: { bg: "bg-emerald-50 dark:bg-emerald-950/40", border: "border-emerald-200 dark:border-emerald-800", accent: "bg-emerald-200 dark:bg-emerald-800" },
10
- color4: { bg: "bg-amber-50 dark:bg-amber-950/40", border: "border-amber-200 dark:border-amber-800", accent: "bg-amber-200 dark:bg-amber-800" },
11
- color5: { bg: "bg-rose-50 dark:bg-rose-950/40", border: "border-rose-200 dark:border-rose-800", accent: "bg-rose-200 dark:bg-rose-800" },
12
- color6: { bg: "bg-green-50 dark:bg-green-950/40", border: "border-green-200 dark:border-green-800", accent: "bg-green-200 dark:bg-green-800" },
7
+ color1: { bg: "bg-palette-0/10", border: "border-palette-0/30", accent: "bg-palette-0/30" },
8
+ color2: { bg: "bg-palette-1/10", border: "border-palette-1/30", accent: "bg-palette-1/30" },
9
+ color3: { bg: "bg-palette-2/10", border: "border-palette-2/30", accent: "bg-palette-2/30" },
10
+ color4: { bg: "bg-palette-3/10", border: "border-palette-3/30", accent: "bg-palette-3/30" },
11
+ color5: { bg: "bg-info/10", border: "border-info/30", accent: "bg-info/30" },
12
+ color6: { bg: "bg-success/10", border: "border-success/30", accent: "bg-success/30" },
13
13
  };
14
14
 
15
15
  const SIZE_MAP: Record<string, { width: number; height: number; fontSize: string }> = {
@@ -43,11 +43,21 @@ export const Flashcard = ({
43
43
  const { width, height, fontSize } = SIZE_MAP[size];
44
44
  const colors = COLOR_MAP[card.color] || COLOR_MAP.color1;
45
45
 
46
+ const handleKeyDown = (e: React.KeyboardEvent) => {
47
+ if (e.key === "Enter" || e.key === " ") {
48
+ e.preventDefault();
49
+ handleClick();
50
+ }
51
+ };
52
+
46
53
  return (
47
54
  <div
48
55
  className="perspective-[1000px]"
49
56
  style={{ width: `${width}px`, height: `${height}px` }}
50
57
  >
58
+ <span className="sr-only" aria-live="polite">
59
+ {isFlipped ? "Answer revealed" : "Question shown"}
60
+ </span>
51
61
  <div
52
62
  className={cn(
53
63
  "relative size-full motion-safe:transition-transform motion-safe:duration-500 transform-3d",
@@ -61,8 +71,13 @@ export const Flashcard = ({
61
71
  colors.bg,
62
72
  colors.border,
63
73
  !readOnly && "cursor-pointer",
74
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
64
75
  )}
65
76
  onClick={handleClick}
77
+ onKeyDown={readOnly ? undefined : handleKeyDown}
78
+ role="button"
79
+ tabIndex={readOnly ? -1 : 0}
80
+ aria-label="Flashcard — press Enter to reveal answer"
66
81
  >
67
82
  <div className="flex flex-1 items-center justify-center p-5 overflow-auto">
68
83
  <div
@@ -74,7 +89,7 @@ export const Flashcard = ({
74
89
  {!readOnly && (
75
90
  <div className="flex items-center justify-center gap-1 pb-3 text-muted-foreground">
76
91
  <RotateCcw size={12} />
77
- <span className="text-xs">Tap to flip</span>
92
+ <span className="text-xs">Press Enter to flip</span>
78
93
  </div>
79
94
  )}
80
95
  </div>
@@ -84,8 +99,13 @@ export const Flashcard = ({
84
99
  className={cn(
85
100
  "absolute inset-0 flex flex-col rounded-lg border border-border bg-background backface-hidden transform-[rotateY(180deg)]",
86
101
  !readOnly && "cursor-pointer",
102
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
87
103
  )}
88
104
  onClick={handleClick}
105
+ onKeyDown={readOnly ? undefined : handleKeyDown}
106
+ role="button"
107
+ tabIndex={readOnly ? -1 : 0}
108
+ aria-label="Flashcard answer — press Enter to flip back"
89
109
  >
90
110
  <div className={cn("h-1.5 rounded-t-lg", colors.accent)} />
91
111
  <div className="flex flex-1 items-start justify-center p-5 overflow-auto">
@@ -98,7 +118,7 @@ export const Flashcard = ({
98
118
  {!readOnly && (
99
119
  <div className="flex items-center justify-center gap-1 pb-3 text-muted-foreground">
100
120
  <RotateCcw size={12} />
101
- <span className="text-xs">Tap to flip back</span>
121
+ <span className="text-xs">Press Enter to flip back</span>
102
122
  </div>
103
123
  )}
104
124
  </div>
package/src/index.ts CHANGED
@@ -34,5 +34,8 @@ export * from "./provider";
34
34
  // UI primitives (shadcn/ui + Base UI)
35
35
  export * from "./ui";
36
36
 
37
+ // License
38
+ export * from "./license";
39
+
37
40
  // Utilities
38
41
  export { cn } from "./lib/utils";
@@ -0,0 +1,62 @@
1
+ import { createContext, useState, useEffect, type ReactNode } from 'react';
2
+ import { validateKey, type KeyResult } from './validate';
3
+
4
+ export type HydraPlan = 'pro' | 'free' | 'validating' | 'invalid';
5
+
6
+ export interface HydraLicenseContextValue {
7
+ plan: HydraPlan;
8
+ isPro: boolean;
9
+ }
10
+
11
+ export const HydraLicenseContext = createContext<HydraLicenseContextValue>({
12
+ plan: 'free',
13
+ isPro: false,
14
+ });
15
+
16
+ function keyResultToPlan(result: KeyResult): HydraPlan {
17
+ switch (result) {
18
+ case 'valid-pro':
19
+ case 'skip':
20
+ return 'pro';
21
+ case 'valid-free':
22
+ case 'no-key':
23
+ return 'free';
24
+ case 'invalid':
25
+ return 'invalid';
26
+ }
27
+ }
28
+
29
+ export interface HydraLicenseProviderProps {
30
+ licenseKey?: string;
31
+ validateUrl?: string;
32
+ children: ReactNode;
33
+ }
34
+
35
+ /** Provides license state to all HydraLMS components.
36
+ * When `validateUrl` is omitted or empty, all features are unlocked (dev mode).
37
+ */
38
+ export function HydraLicenseProvider({ licenseKey, validateUrl = '', children }: HydraLicenseProviderProps) {
39
+ const [plan, setPlan] = useState<HydraPlan>(() => {
40
+ if (!validateUrl) return 'pro';
41
+ return licenseKey ? 'validating' : 'free';
42
+ });
43
+
44
+ useEffect(() => {
45
+ if (!validateUrl) {
46
+ setPlan('pro');
47
+ return;
48
+ }
49
+
50
+ let cancelled = false;
51
+ validateKey(licenseKey ?? null, validateUrl).then((result) => {
52
+ if (!cancelled) setPlan(keyResultToPlan(result));
53
+ });
54
+ return () => { cancelled = true; };
55
+ }, [licenseKey, validateUrl]);
56
+
57
+ return (
58
+ <HydraLicenseContext.Provider value={{ plan, isPro: plan === 'pro' }}>
59
+ {children}
60
+ </HydraLicenseContext.Provider>
61
+ );
62
+ }
@@ -0,0 +1,43 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ /** Wraps children with a subtle "HydraLMS Pro" upgrade badge when the module requires a pro license. */
4
+ export function ProBadge({ children, feature }: { children: ReactNode; feature?: string }) {
5
+ return (
6
+ <div style={{ position: 'relative' }}>
7
+ {children}
8
+ <a
9
+ href="https://hydralms.com/pro"
10
+ target="_blank"
11
+ rel="noopener noreferrer"
12
+ style={{
13
+ position: 'absolute',
14
+ bottom: 14,
15
+ right: 16,
16
+ zIndex: 9999,
17
+ display: 'flex',
18
+ alignItems: 'center',
19
+ gap: 5,
20
+ padding: '4px 10px 4px 8px',
21
+ background: 'rgba(0,0,0,0.5)',
22
+ backdropFilter: 'blur(6px)',
23
+ borderRadius: 20,
24
+ fontFamily: 'system-ui, -apple-system, sans-serif',
25
+ fontSize: 11,
26
+ fontWeight: 500,
27
+ color: 'rgba(255,255,255,0.85)',
28
+ letterSpacing: '0.01em',
29
+ whiteSpace: 'nowrap',
30
+ lineHeight: 1,
31
+ pointerEvents: 'auto',
32
+ textDecoration: 'none',
33
+ }}
34
+ title={feature ? `"${feature}" is a HydraLMS Pro module` : 'Upgrade to HydraLMS Pro'}
35
+ >
36
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.8 }}>
37
+ <path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
38
+ </svg>
39
+ HydraLMS Pro
40
+ </a>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,7 @@
1
+ export { HydraLicenseProvider, HydraLicenseContext } from './HydraContext';
2
+ export type { HydraPlan, HydraLicenseContextValue, HydraLicenseProviderProps } from './HydraContext';
3
+ export { useHydraLicense } from './useHydraLicense';
4
+ export { isProModule, PRO_MODULES } from './tiers';
5
+ export type { ProModuleName } from './tiers';
6
+ export { ProBadge } from './ProBadge';
7
+ export { withProGate } from './withProGate';
@@ -0,0 +1,24 @@
1
+ // ─── HydraLMS Pro Tier Definitions ──────────────────────────────
2
+ // All 12 modules are Pro tier. UI primitives, base components, and
3
+ // sections are free and always will be.
4
+
5
+ export const PRO_MODULES = new Set([
6
+ 'QuizModule',
7
+ 'FlashcardLab',
8
+ 'CoursePlayer',
9
+ 'ExamModule',
10
+ 'SurveyModule',
11
+ 'GradeCenterModule',
12
+ 'AssignmentModule',
13
+ 'CertificateModule',
14
+ 'DiscussionModule',
15
+ 'StudentDashboardModule',
16
+ 'CourseCatalogModule',
17
+ 'StudentProfileModule',
18
+ ] as const);
19
+
20
+ export type ProModuleName = typeof PRO_MODULES extends Set<infer T> ? T : never;
21
+
22
+ export function isProModule(name: string): boolean {
23
+ return PRO_MODULES.has(name as any);
24
+ }
@@ -0,0 +1,10 @@
1
+ import { useContext } from 'react';
2
+ import { HydraLicenseContext, type HydraLicenseContextValue } from './HydraContext';
3
+
4
+ /** Returns the current HydraLMS license status. Pro modules render with a
5
+ * watermark badge when `isPro` is false. Wrap your app in `<HydraProvider>`
6
+ * with a valid license key to unlock all features.
7
+ */
8
+ export function useHydraLicense(): HydraLicenseContextValue {
9
+ return useContext(HydraLicenseContext);
10
+ }