@hydralms/components 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/StudentProfile-BPsZBaJj.cjs +1 -0
  2. package/dist/StudentProfile-Cw2p-RZn.js +3273 -0
  3. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  4. package/dist/assessment-toolbar/timer-display.d.ts +1 -1
  5. package/dist/common/index.d.ts +2 -1
  6. package/dist/common/pagination.d.ts +26 -0
  7. package/dist/common/types.d.ts +1 -0
  8. package/dist/components.css +1 -1
  9. package/dist/content/audio-player.d.ts +22 -0
  10. package/dist/content/code-block.d.ts +30 -0
  11. package/dist/content/embed-block.d.ts +28 -0
  12. package/dist/content/index.d.ts +6 -0
  13. package/dist/content/types.d.ts +24 -0
  14. package/dist/curriculum/course-card.d.ts +51 -0
  15. package/dist/curriculum/index.d.ts +2 -0
  16. package/dist/curriculum/types.d.ts +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +495 -439
  20. package/dist/license/HydraContext.d.ts +16 -0
  21. package/dist/license/ProBadge.d.ts +6 -0
  22. package/dist/license/index.d.ts +7 -0
  23. package/dist/license/tiers.d.ts +6 -0
  24. package/dist/license/useHydraLicense.d.ts +6 -0
  25. package/dist/license/validate.d.ts +13 -0
  26. package/dist/license/withProGate.d.ts +6 -0
  27. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +4 -7
  28. package/dist/modules/AssignmentModule/types.d.ts +5 -1
  29. package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
  30. package/dist/modules/CertificateModule/types.d.ts +6 -4
  31. package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
  32. package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
  33. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
  34. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +4 -7
  35. package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
  36. package/dist/modules/ExamModule/types.d.ts +5 -14
  37. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
  38. package/dist/modules/FlashcardLab/types.d.ts +2 -0
  39. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +4 -8
  40. package/dist/modules/GradeCenterModule/types.d.ts +2 -0
  41. package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
  42. package/dist/modules/QuizModule/types.d.ts +5 -14
  43. package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
  44. package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
  45. package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
  46. package/dist/modules/StudentProfileModule/types.d.ts +43 -0
  47. package/dist/modules/SurveyModule/SurveyModule.d.ts +4 -6
  48. package/dist/modules/SurveyModule/types.d.ts +2 -0
  49. package/dist/modules/_shared/assessment-intro.d.ts +16 -0
  50. package/dist/modules/_shared/assessment-results.d.ts +23 -0
  51. package/dist/modules/_shared/types.d.ts +10 -0
  52. package/dist/modules/_shared/use-timer.d.ts +9 -0
  53. package/dist/modules/index.d.ts +6 -0
  54. package/dist/modules.cjs +1 -1
  55. package/dist/modules.js +1267 -854
  56. package/dist/progress/types.d.ts +2 -0
  57. package/dist/provider/HydraProvider.d.ts +5 -1
  58. package/dist/questions/choice.d.ts +1 -1
  59. package/dist/questions/confidence-indicator.d.ts +37 -0
  60. package/dist/questions/essay.d.ts +1 -1
  61. package/dist/questions/fill-in-the-blank.d.ts +1 -1
  62. package/dist/questions/hotspot.d.ts +1 -1
  63. package/dist/questions/index.d.ts +2 -0
  64. package/dist/questions/inline-choice.d.ts +1 -1
  65. package/dist/questions/matching.d.ts +1 -1
  66. package/dist/questions/multiple-choice.d.ts +1 -1
  67. package/dist/questions/numeric.d.ts +1 -1
  68. package/dist/questions/ordering.d.ts +1 -1
  69. package/dist/questions/question-renderer.d.ts +1 -1
  70. package/dist/questions/scenario.d.ts +1 -1
  71. package/dist/questions/spreadsheet.d.ts +1 -1
  72. package/dist/questions/true-false.d.ts +1 -1
  73. package/dist/sections/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
  74. package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
  75. package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
  76. package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
  77. package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
  78. package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -0
  79. package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
  80. package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
  81. package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
  82. package/dist/sections/AssessmentReview/types.d.ts +6 -0
  83. package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
  84. package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
  85. package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
  86. package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
  87. package/dist/sections/CertificateViewer/types.d.ts +6 -0
  88. package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
  89. package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
  90. package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
  91. package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -0
  92. package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
  93. package/dist/sections/CourseCatalog/types.d.ts +80 -0
  94. package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
  95. package/dist/sections/CourseOutline/types.d.ts +6 -0
  96. package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
  97. package/dist/sections/DiscussionThread/types.d.ts +6 -0
  98. package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
  99. package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
  100. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  101. package/dist/sections/ExamSession/types.d.ts +6 -0
  102. package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
  103. package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
  104. package/dist/sections/ForumBoard/ForumBoard.d.ts +1 -1
  105. package/dist/sections/ForumBoard/types.d.ts +14 -0
  106. package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
  107. package/dist/sections/GradebookTable/types.d.ts +14 -0
  108. package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
  109. package/dist/sections/LecturePlayer/types.d.ts +8 -0
  110. package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
  111. package/dist/sections/LessonPage/types.d.ts +6 -0
  112. package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
  113. package/dist/sections/PracticeQuiz/types.d.ts +6 -0
  114. package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
  115. package/dist/sections/ProgressDashboard/types.d.ts +6 -0
  116. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  117. package/dist/sections/QuizSession/types.d.ts +6 -0
  118. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
  119. package/dist/sections/RequirementsChecklist/types.d.ts +6 -0
  120. package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
  121. package/dist/sections/ResourceLibrary/types.d.ts +15 -1
  122. package/dist/sections/RubricView/RubricView.d.ts +1 -1
  123. package/dist/sections/RubricView/types.d.ts +6 -0
  124. package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
  125. package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
  126. package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
  127. package/dist/sections/StudentProfile/types.d.ts +98 -0
  128. package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
  129. package/dist/sections/SurveyForm/types.d.ts +6 -0
  130. package/dist/sections/_shared/merge-answers.d.ts +9 -0
  131. package/dist/sections/_shared/section-shell.d.ts +20 -0
  132. package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
  133. package/dist/sections/index.d.ts +10 -0
  134. package/dist/sections.cjs +1 -1
  135. package/dist/sections.js +1361 -307
  136. package/dist/ui/badge.d.ts +1 -1
  137. package/dist/ui/index.d.ts +2 -0
  138. package/dist/ui/progress.d.ts +1 -1
  139. package/dist/ui/rich-text-editor.d.ts +3 -1
  140. package/dist/ui/toast.d.ts +43 -0
  141. package/dist/utils/debounce.d.ts +5 -1
  142. package/dist/utils/pick-palette-color.d.ts +19 -0
  143. package/dist/video/types.d.ts +15 -0
  144. package/dist/video/video-player.d.ts +1 -1
  145. package/dist/withProGate-BJdu1T9Y.cjs +2 -0
  146. package/dist/withProGate-BvFc7Jwy.js +4975 -0
  147. package/package.json +57 -226
  148. package/src/assessment-toolbar/question-navigator.tsx +10 -5
  149. package/src/assessment-toolbar/timer-display.tsx +4 -3
  150. package/src/assessment-toolbar/use-countdown.ts +1 -1
  151. package/src/common/empty-state.tsx +1 -0
  152. package/src/common/index.ts +2 -0
  153. package/src/common/pagination.tsx +135 -0
  154. package/src/common/search-input.tsx +2 -1
  155. package/src/common/types.ts +2 -0
  156. package/src/content/attachment-list.tsx +2 -0
  157. package/src/content/audio-player.tsx +196 -0
  158. package/src/content/code-block.tsx +113 -0
  159. package/src/content/content-block.tsx +64 -0
  160. package/src/content/embed-block.tsx +78 -0
  161. package/src/content/file-upload-zone.tsx +10 -0
  162. package/src/content/index.ts +6 -0
  163. package/src/content/types.ts +5 -0
  164. package/src/curriculum/course-card.tsx +199 -0
  165. package/src/curriculum/curriculum-item.tsx +3 -3
  166. package/src/curriculum/curriculum-tree.tsx +20 -13
  167. package/src/curriculum/index.ts +2 -0
  168. package/src/curriculum/types.ts +2 -2
  169. package/src/flashcards/flashcard.tsx +28 -8
  170. package/src/index.ts +3 -0
  171. package/src/license/HydraContext.tsx +62 -0
  172. package/src/license/ProBadge.tsx +43 -0
  173. package/src/license/index.ts +7 -0
  174. package/src/license/tiers.ts +34 -0
  175. package/src/license/useHydraLicense.ts +10 -0
  176. package/src/license/validate.ts +90 -0
  177. package/src/license/withProGate.tsx +21 -0
  178. package/src/modules/AssignmentModule/AssignmentModule.tsx +17 -8
  179. package/src/modules/AssignmentModule/types.ts +5 -1
  180. package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
  181. package/src/modules/CertificateModule/types.ts +6 -4
  182. package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
  183. package/src/modules/CourseCatalogModule/types.ts +47 -0
  184. package/src/modules/CoursePlayer/CoursePlayer.tsx +39 -22
  185. package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
  186. package/src/modules/ExamModule/ExamModule.tsx +64 -198
  187. package/src/modules/ExamModule/types.ts +5 -14
  188. package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
  189. package/src/modules/FlashcardLab/types.ts +2 -0
  190. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
  191. package/src/modules/GradeCenterModule/types.ts +2 -0
  192. package/src/modules/QuizModule/QuizModule.tsx +49 -169
  193. package/src/modules/QuizModule/types.ts +5 -15
  194. package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
  195. package/src/modules/StudentDashboardModule/types.ts +56 -0
  196. package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
  197. package/src/modules/StudentProfileModule/types.ts +45 -0
  198. package/src/modules/SurveyModule/SurveyModule.tsx +9 -4
  199. package/src/modules/SurveyModule/types.ts +2 -0
  200. package/src/modules/_shared/assessment-intro.tsx +75 -0
  201. package/src/modules/_shared/assessment-results.tsx +133 -0
  202. package/src/modules/_shared/types.ts +11 -0
  203. package/src/modules/_shared/use-timer.ts +49 -0
  204. package/src/modules/index.ts +9 -0
  205. package/src/progress/achievement-badge.tsx +3 -3
  206. package/src/progress/grade-indicator.tsx +9 -1
  207. package/src/progress/progress-ring.tsx +2 -1
  208. package/src/progress/stat-card.tsx +14 -2
  209. package/src/progress/types.ts +2 -0
  210. package/src/provider/HydraProvider.tsx +15 -6
  211. package/src/questions/choice.tsx +13 -6
  212. package/src/questions/confidence-indicator.tsx +107 -0
  213. package/src/questions/essay.tsx +6 -4
  214. package/src/questions/fill-in-the-blank.tsx +8 -4
  215. package/src/questions/hotspot.tsx +4 -4
  216. package/src/questions/index.ts +2 -0
  217. package/src/questions/inline-choice.tsx +5 -4
  218. package/src/questions/matching.tsx +5 -4
  219. package/src/questions/multiple-choice.tsx +13 -6
  220. package/src/questions/numeric.tsx +8 -4
  221. package/src/questions/ordering.tsx +12 -4
  222. package/src/questions/question-renderer.tsx +3 -2
  223. package/src/questions/scenario.tsx +4 -4
  224. package/src/questions/spreadsheet.tsx +5 -4
  225. package/src/questions/true-false.tsx +13 -6
  226. package/src/sections/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
  227. package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
  228. package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
  229. package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
  230. package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
  231. package/src/sections/AdaptiveLearningPath/types.ts +159 -0
  232. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
  233. package/src/sections/AnnouncementFeed/types.ts +15 -1
  234. package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
  235. package/src/sections/AssessmentReview/types.ts +6 -0
  236. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
  237. package/src/sections/AssignmentSubmission/types.ts +6 -0
  238. package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
  239. package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
  240. package/src/sections/CertificateViewer/types.ts +6 -0
  241. package/src/sections/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
  242. package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
  243. package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
  244. package/src/sections/ContentAuthoringStudio/types.ts +67 -0
  245. package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
  246. package/src/sections/CourseCatalog/types.ts +76 -0
  247. package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
  248. package/src/sections/CourseOutline/types.ts +6 -0
  249. package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
  250. package/src/sections/DiscussionThread/types.ts +6 -0
  251. package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
  252. package/src/sections/EnrollmentWizard/types.ts +65 -0
  253. package/src/sections/ExamSession/ExamSession.tsx +100 -94
  254. package/src/sections/ExamSession/types.ts +6 -0
  255. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
  256. package/src/sections/FlashcardStudySession/types.ts +6 -0
  257. package/src/sections/ForumBoard/ForumBoard.tsx +67 -7
  258. package/src/sections/ForumBoard/types.ts +14 -0
  259. package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
  260. package/src/sections/GradebookTable/types.ts +14 -0
  261. package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
  262. package/src/sections/LecturePlayer/types.ts +8 -0
  263. package/src/sections/LessonPage/LessonPage.tsx +34 -6
  264. package/src/sections/LessonPage/types.ts +6 -0
  265. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
  266. package/src/sections/PracticeQuiz/types.ts +6 -0
  267. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
  268. package/src/sections/ProgressDashboard/types.ts +6 -0
  269. package/src/sections/QuizSession/QuizSession.tsx +71 -82
  270. package/src/sections/QuizSession/types.ts +6 -0
  271. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
  272. package/src/sections/RequirementsChecklist/types.ts +6 -0
  273. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
  274. package/src/sections/ResourceLibrary/types.ts +15 -1
  275. package/src/sections/RubricView/RubricView.tsx +37 -1
  276. package/src/sections/RubricView/types.ts +6 -0
  277. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
  278. package/src/sections/ScrollableQuiz/types.ts +6 -0
  279. package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
  280. package/src/sections/StudentProfile/types.ts +99 -0
  281. package/src/sections/SurveyForm/SurveyForm.tsx +32 -5
  282. package/src/sections/SurveyForm/types.ts +6 -0
  283. package/src/sections/_shared/merge-answers.ts +22 -0
  284. package/src/sections/_shared/section-shell.tsx +64 -0
  285. package/src/sections/_shared/use-assessment-session.ts +125 -0
  286. package/src/sections/index.ts +40 -0
  287. package/src/social/user-avatar.tsx +9 -5
  288. package/src/styles/globals.css +39 -41
  289. package/src/ui/badge.tsx +8 -0
  290. package/src/ui/index.ts +2 -0
  291. package/src/ui/progress.tsx +4 -0
  292. package/src/ui/rich-text-editor.tsx +10 -0
  293. package/src/ui/rich-text-toolbar.tsx +2 -1
  294. package/src/ui/toast.tsx +170 -0
  295. package/src/utils/debounce.ts +8 -2
  296. package/src/utils/pick-palette-color.ts +33 -0
  297. package/src/video/types.ts +16 -0
  298. package/src/video/video-player.tsx +27 -6
  299. package/dist/ForumBoard-CHXU3mjC.js +0 -2207
  300. package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
  301. package/dist/tabs-DRM2Iq_J.cjs +0 -172
  302. package/dist/tabs-Wf3h_Cx3.js +0 -21580
@@ -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, isProSection, PRO_SECTIONS } from './tiers';
5
+ export type { ProModuleName, ProSectionName } from './tiers';
6
+ export { ProBadge } from './ProBadge';
7
+ export { withProGate } from './withProGate';
@@ -0,0 +1,34 @@
1
+ // ─── HydraLMS Pro Tier Definitions ──────────────────────────────
2
+ // All 12 modules and premium sections are Pro tier.
3
+ // UI primitives, base components, and standard sections are free.
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
+ }
25
+
26
+ export const PRO_SECTIONS = new Set([
27
+ 'ContentAuthoringStudio',
28
+ ] as const);
29
+
30
+ export type ProSectionName = typeof PRO_SECTIONS extends Set<infer T> ? T : never;
31
+
32
+ export function isProSection(name: string): boolean {
33
+ return PRO_SECTIONS.has(name as any);
34
+ }
@@ -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
+ }
@@ -0,0 +1,90 @@
1
+ // ─── Shared License Validation ──────────────────────────────
2
+
3
+ const CACHE_KEY = 'hydra_key_v';
4
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
5
+
6
+ export type KeyResult = 'valid-pro' | 'valid-free' | 'invalid' | 'no-key' | 'skip';
7
+
8
+ export interface ValidationResponse {
9
+ valid: boolean;
10
+ plan: 'pro' | 'enterprise' | null;
11
+ }
12
+
13
+ /** Read a cached validation result from sessionStorage. */
14
+ function readCache(rawKey: string): KeyResult | null {
15
+ try {
16
+ const cached = sessionStorage.getItem(CACHE_KEY);
17
+ if (cached) {
18
+ const { result, ts, key } = JSON.parse(cached) as { result: KeyResult; ts: number; key: string };
19
+ if (key === rawKey && Date.now() - ts < CACHE_TTL) return result;
20
+ }
21
+ } catch { /* sessionStorage unavailable */ }
22
+ return null;
23
+ }
24
+
25
+ /** Write a validation result to sessionStorage cache. */
26
+ function writeCache(rawKey: string, result: KeyResult): void {
27
+ try {
28
+ sessionStorage.setItem(CACHE_KEY, JSON.stringify({ result, ts: Date.now(), key: rawKey }));
29
+ } catch { /* full or unavailable */ }
30
+ }
31
+
32
+ /** On network failure, trust a previously cached valid-pro result. */
33
+ function fallbackFromCache(rawKey: string): KeyResult {
34
+ try {
35
+ const cached = sessionStorage.getItem(CACHE_KEY);
36
+ if (cached) {
37
+ const { result, key } = JSON.parse(cached) as { result: KeyResult; key: string };
38
+ if (key === rawKey && result === 'valid-pro') return 'valid-pro';
39
+ }
40
+ } catch { /* ignore */ }
41
+ return 'invalid';
42
+ }
43
+
44
+ /**
45
+ * Validate a license key against the validation endpoint.
46
+ * @param rawKey - The license key string, or null if not provided.
47
+ * @param validateUrl - The validation endpoint URL. Empty string skips validation (dev mode).
48
+ */
49
+ export async function validateKey(rawKey: string | null, validateUrl: string): Promise<KeyResult> {
50
+ // No validation URL configured (local dev) — skip entirely
51
+ if (!validateUrl) return 'skip';
52
+
53
+ // No key provided
54
+ if (!rawKey) return 'no-key';
55
+
56
+ // Check sessionStorage cache
57
+ const cached = readCache(rawKey);
58
+ if (cached) return cached;
59
+
60
+ // Network validation
61
+ try {
62
+ const res = await fetch(`${validateUrl}?key=${encodeURIComponent(rawKey)}`, {
63
+ method: 'GET',
64
+ signal: AbortSignal.timeout(4000),
65
+ });
66
+
67
+ if (!res.ok) return fallbackFromCache(rawKey);
68
+
69
+ const data = await res.json() as ValidationResponse;
70
+ let result: KeyResult;
71
+
72
+ if (!data.valid) {
73
+ result = 'invalid';
74
+ } else if (data.plan === 'pro' || data.plan === 'enterprise') {
75
+ result = 'valid-pro';
76
+ } else {
77
+ result = 'valid-free';
78
+ }
79
+
80
+ writeCache(rawKey, result);
81
+ return result;
82
+ } catch {
83
+ return fallbackFromCache(rawKey);
84
+ }
85
+ }
86
+
87
+ /** Whether a watermark should be shown for the given key result. */
88
+ export function shouldShowWatermark(result: KeyResult): boolean {
89
+ return result === 'no-key' || result === 'invalid' || result === 'valid-free';
90
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { useHydraLicense } from './useHydraLicense';
3
+ import { ProBadge } from './ProBadge';
4
+
5
+ /** Higher-order component that wraps a pro-tier module with ProBadge when unlicensed. */
6
+ export function withProGate<P extends object>(
7
+ Component: React.ComponentType<P>,
8
+ moduleName: string,
9
+ ) {
10
+ const Gated = (props: P) => {
11
+ const { isPro } = useHydraLicense();
12
+ if (isPro) return <Component {...props} />;
13
+ return (
14
+ <ProBadge feature={moduleName}>
15
+ <Component {...props} />
16
+ </ProBadge>
17
+ );
18
+ };
19
+ Gated.displayName = `ProGated(${moduleName})`;
20
+ return Gated;
21
+ }