@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
@@ -5,9 +5,9 @@ import type { AchievementBadgeProps } from "./types";
5
5
 
6
6
  const VARIANT_STYLES = {
7
7
  default: "text-primary",
8
- gold: "text-yellow-500",
9
- silver: "text-gray-400",
10
- bronze: "text-amber-700",
8
+ gold: "text-palette-3",
9
+ silver: "text-muted-foreground",
10
+ bronze: "text-palette-3/70",
11
11
  } as const;
12
12
 
13
13
  /**
@@ -36,7 +36,15 @@ export function GradeIndicator({
36
36
  if (variant === "linear") {
37
37
  return (
38
38
  <div className={cn("flex flex-row items-center gap-1", className)} style={style}>
39
- <div className="flex-1 bg-muted rounded-full overflow-hidden" style={{ height: TRACK_HEIGHTS[size] }}>
39
+ <div
40
+ className="flex-1 bg-muted rounded-full overflow-hidden"
41
+ style={{ height: TRACK_HEIGHTS[size] }}
42
+ role="progressbar"
43
+ aria-valuenow={Math.round(percentage)}
44
+ aria-valuemin={0}
45
+ aria-valuemax={100}
46
+ aria-label={`Grade: ${letterGrade ?? `${Math.round(percentage)}%`}`}
47
+ >
40
48
  <div
41
49
  className="h-full rounded-full transition-[width] duration-300 ease-in-out"
42
50
  style={{ width: `${percentage}%`, background: color }}
@@ -21,7 +21,7 @@ export function ProgressRing({
21
21
  className={cn("relative inline-flex", className)}
22
22
  style={{ width: `${size}px`, height: `${size}px`, ...style }}
23
23
  >
24
- <svg width={size} height={size}>
24
+ <svg width={size} height={size} role="img" aria-label={label ?? `${Math.round(value)}% progress`}>
25
25
  <circle
26
26
  cx={center}
27
27
  cy={center}
@@ -48,6 +48,7 @@ export function ProgressRing({
48
48
  <span
49
49
  className="absolute inset-0 flex items-center justify-center font-bold text-foreground"
50
50
  style={{ fontSize: `${size * 0.2}px` }}
51
+ aria-hidden="true"
51
52
  >
52
53
  {label ?? `${Math.round(value)}%`}
53
54
  </span>
@@ -18,6 +18,7 @@ export const StatCard = memo(function StatCard({
18
18
  value,
19
19
  subtitle,
20
20
  trend,
21
+ accent,
21
22
  className,
22
23
  style,
23
24
  }: StatCardProps) {
@@ -27,7 +28,13 @@ export const StatCard = memo(function StatCard({
27
28
  <Card className={cn(className)} style={style}>
28
29
  <CardContent className="p-4">
29
30
  {icon && (
30
- <div className="mb-2 w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center text-primary [&>svg]:size-5">
31
+ <div
32
+ className="mb-2 w-9 h-9 rounded-lg flex items-center justify-center [&>svg]:size-5"
33
+ style={{
34
+ backgroundColor: `color-mix(in oklch, ${accent ?? "var(--primary)"} 10%, transparent)`,
35
+ color: accent ?? "var(--primary)",
36
+ }}
37
+ >
31
38
  {icon}
32
39
  </div>
33
40
  )}
@@ -68,6 +68,8 @@ export interface StatCardProps {
68
68
  subtitle?: string;
69
69
  /** Optional trend data */
70
70
  trend?: { value: number; direction: "up" | "down" | "flat" };
71
+ /** Accent color CSS value for the icon background. Defaults to primary. */
72
+ accent?: string;
71
73
  /** CSS class name for the root element */
72
74
  className?: string;
73
75
  /** Inline styles for the root element */
@@ -1,10 +1,15 @@
1
1
  import "../styles/globals.css";
2
2
  import type { ReactNode } from "react";
3
+ import { HydraLicenseProvider } from "../license/HydraContext";
3
4
 
4
5
  export interface HydraProviderProps {
5
6
  children: ReactNode;
6
7
  /** Controls color mode. Defaults to `"dark"`. Set to `"light"` to use the light theme. */
7
8
  colorMode?: "light" | "dark";
9
+ /** HydraLMS Pro license key. Omit for free tier. */
10
+ licenseKey?: string;
11
+ /** Validation endpoint URL. When empty (default), all features are unlocked (dev mode). */
12
+ validateUrl?: string;
8
13
  className?: string;
9
14
  style?: React.CSSProperties;
10
15
  }
@@ -12,15 +17,19 @@ export interface HydraProviderProps {
12
17
  export function HydraProvider({
13
18
  children,
14
19
  colorMode = "dark",
20
+ licenseKey,
21
+ validateUrl,
15
22
  className,
16
23
  style,
17
24
  }: HydraProviderProps) {
18
25
  return (
19
- <div
20
- className={`hydra-root${colorMode === "dark" ? " dark" : ""}${className ? ` ${className}` : ""}`}
21
- style={style}
22
- >
23
- {children}
24
- </div>
26
+ <HydraLicenseProvider licenseKey={licenseKey} validateUrl={validateUrl}>
27
+ <div
28
+ className={`hydra-root${colorMode === "dark" ? " dark" : ""}${className ? ` ${className}` : ""}`}
29
+ style={style}
30
+ >
31
+ {children}
32
+ </div>
33
+ </HydraLicenseProvider>
25
34
  );
26
35
  }
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, memo } from "react";
2
2
  import type { QuestionProps } from "./types";
3
3
  import { Alert, AlertDescription } from "../ui/alert";
4
4
  import { cn } from "../lib/utils";
@@ -12,14 +12,14 @@ import { cn } from "../lib/utils";
12
12
  * onAnswer={(answers) => handleAnswer(answers)}
13
13
  * />
14
14
  */
15
- export const Choice = ({
15
+ export const Choice = memo(function Choice({
16
16
  question,
17
17
  sessionAnswers,
18
18
  onAnswer,
19
19
  readOnly = false,
20
20
  showCorrectAnswers = false,
21
21
  disabled = false,
22
- }: QuestionProps) => {
22
+ }: QuestionProps) {
23
23
  const [selectedAnswer, setSelectedAnswer] = useState<string>(
24
24
  () => sessionAnswers?.[0]?.answerUid || "",
25
25
  );
@@ -53,7 +53,8 @@ export const Choice = ({
53
53
  <div className="flex flex-col gap-4">
54
54
  <div dangerouslySetInnerHTML={{ __html: question.content }} />
55
55
 
56
- <div className="flex flex-col gap-2">
56
+ <fieldset className="flex flex-col gap-2 border-0 p-0 m-0">
57
+ <legend className="sr-only">Answer choices</legend>
57
58
  {sortedAnswers.map((answer) => (
58
59
  <div
59
60
  key={answer.uid}
@@ -70,10 +71,16 @@ export const Choice = ({
70
71
  className="accent-primary m-0 shrink-0"
71
72
  />
72
73
  <span dangerouslySetInnerHTML={{ __html: answer.content }} />
74
+ {showCorrectAnswers && answer.isCorrect && (
75
+ <span className="sr-only">(Correct answer)</span>
76
+ )}
77
+ {showCorrectAnswers && selectedAnswer === answer.uid && !answer.isCorrect && (
78
+ <span className="sr-only">(Your answer — incorrect)</span>
79
+ )}
73
80
  </label>
74
81
  </div>
75
82
  ))}
76
- </div>
83
+ </fieldset>
77
84
 
78
85
  {showCorrectAnswers && question.explanation && (
79
86
  <Alert className="mt-2">
@@ -85,4 +92,4 @@ export const Choice = ({
85
92
  )}
86
93
  </div>
87
94
  );
88
- };
95
+ });
@@ -0,0 +1,107 @@
1
+ import { memo } from "react";
2
+ import type { ReactNode } from "react";
3
+ import { HelpCircle, Brain, Lightbulb } from "lucide-react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ /** A single confidence level option. */
7
+ export interface ConfidenceLevel {
8
+ /** Value stored in SessionAnswer.confidence */
9
+ value: string;
10
+ /** Display label */
11
+ label: string;
12
+ /** Optional icon */
13
+ icon?: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * ConfidenceIndicator lets learners self-report their answer confidence,
18
+ * supporting metacognitive assessment strategies.
19
+ *
20
+ * @example
21
+ * <ConfidenceIndicator
22
+ * value={answer.confidence}
23
+ * onChange={(level) => updateConfidence(level)}
24
+ * />
25
+ */
26
+ export interface ConfidenceIndicatorProps {
27
+ /** Currently selected confidence level value */
28
+ value: string | null;
29
+ /** Called when the user selects a confidence level */
30
+ onChange: (level: string) => void;
31
+ /** Custom confidence levels (defaults to 3 levels) */
32
+ levels?: ConfidenceLevel[];
33
+ /** When true, disables interaction */
34
+ disabled?: boolean;
35
+ /** When true, shows selected state but disables interaction */
36
+ readOnly?: boolean;
37
+ /** CSS class name for the root element */
38
+ className?: string;
39
+ /** Inline styles for the root element */
40
+ style?: React.CSSProperties;
41
+ }
42
+
43
+ const DEFAULT_LEVELS: ConfidenceLevel[] = [
44
+ {
45
+ value: "low",
46
+ label: "Guessing",
47
+ icon: <HelpCircle className="size-3.5" />,
48
+ },
49
+ {
50
+ value: "medium",
51
+ label: "Somewhat sure",
52
+ icon: <Brain className="size-3.5" />,
53
+ },
54
+ {
55
+ value: "high",
56
+ label: "Confident",
57
+ icon: <Lightbulb className="size-3.5" />,
58
+ },
59
+ ];
60
+
61
+ export const ConfidenceIndicator = memo(function ConfidenceIndicator({
62
+ value,
63
+ onChange,
64
+ levels = DEFAULT_LEVELS,
65
+ disabled = false,
66
+ readOnly = false,
67
+ className,
68
+ style,
69
+ }: ConfidenceIndicatorProps) {
70
+ const isDisabled = disabled || readOnly;
71
+
72
+ return (
73
+ <div
74
+ data-slot="confidence-indicator"
75
+ className={cn("flex items-center gap-1", className)}
76
+ style={style}
77
+ role="radiogroup"
78
+ aria-label="Confidence level"
79
+ >
80
+ {levels.map((level) => {
81
+ const isSelected = value === level.value;
82
+ return (
83
+ <button
84
+ key={level.value}
85
+ type="button"
86
+ role="radio"
87
+ aria-checked={isSelected}
88
+ aria-label={level.label}
89
+ disabled={isDisabled}
90
+ onClick={() => onChange(level.value)}
91
+ className={cn(
92
+ "inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-all",
93
+ "border outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
94
+ isSelected
95
+ ? "border-primary bg-primary/10 text-primary"
96
+ : "border-transparent bg-muted text-muted-foreground hover:bg-muted/80",
97
+ isDisabled && "opacity-50 pointer-events-none",
98
+ )}
99
+ >
100
+ {level.icon}
101
+ {level.label}
102
+ </button>
103
+ );
104
+ })}
105
+ </div>
106
+ );
107
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useRef } from "react";
1
+ import { useState, useMemo, useRef, useEffect, memo } from "react";
2
2
  import { debounce } from "../utils/debounce";
3
3
  import { RichTextEditor } from "../ui/rich-text-editor";
4
4
  import type { QuestionProps } from "./types";
@@ -12,13 +12,13 @@ import type { QuestionProps } from "./types";
12
12
  * onAnswer={(answers) => handleAnswer(answers)}
13
13
  * />
14
14
  */
15
- export const Essay = ({
15
+ export const Essay = memo(function Essay({
16
16
  question,
17
17
  sessionAnswers,
18
18
  onAnswer,
19
19
  readOnly = false,
20
20
  disabled = false,
21
- }: QuestionProps) => {
21
+ }: QuestionProps) {
22
22
  const [value, setValue] = useState(() => sessionAnswers?.[0]?.content || "");
23
23
 
24
24
  const onAnswerRef = useRef(onAnswer);
@@ -33,6 +33,7 @@ export const Essay = ({
33
33
  }, 500),
34
34
  [],
35
35
  );
36
+ useEffect(() => () => debouncedAnswer.cancel(), [debouncedAnswer]);
36
37
 
37
38
  const handleChange = (html: string) => {
38
39
  setValue(html);
@@ -51,7 +52,8 @@ export const Essay = ({
51
52
  readOnly={readOnly}
52
53
  disabled={disabled}
53
54
  variant="default"
55
+ ariaLabel="Essay response"
54
56
  />
55
57
  </div>
56
58
  );
57
- };
59
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useRef } from "react";
1
+ import { useState, useMemo, useRef, useId, useEffect, memo } from "react";
2
2
  import { debounce } from "../utils/debounce";
3
3
  import { Input } from "../ui/input";
4
4
  import { Alert, AlertDescription } from "../ui/alert";
@@ -13,14 +13,15 @@ import type { QuestionProps } from "./types";
13
13
  * onAnswer={(answers) => handleAnswer(answers)}
14
14
  * />
15
15
  */
16
- export const FillInTheBlank = ({
16
+ export const FillInTheBlank = memo(function FillInTheBlank({
17
17
  question,
18
18
  sessionAnswers,
19
19
  onAnswer,
20
20
  readOnly = false,
21
21
  showCorrectAnswers = false,
22
22
  disabled = false,
23
- }: QuestionProps) => {
23
+ }: QuestionProps) {
24
+ const inputId = useId();
24
25
  const [value, setValue] = useState(() => sessionAnswers?.[0]?.content || "");
25
26
 
26
27
  const onAnswerRef = useRef(onAnswer);
@@ -35,6 +36,7 @@ export const FillInTheBlank = ({
35
36
  }, 300),
36
37
  [],
37
38
  );
39
+ useEffect(() => () => debouncedAnswer.cancel(), [debouncedAnswer]);
38
40
 
39
41
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
40
42
  const newValue = e.target.value;
@@ -46,7 +48,9 @@ export const FillInTheBlank = ({
46
48
  <div className="flex flex-col gap-2">
47
49
  <div dangerouslySetInnerHTML={{ __html: question.content }} />
48
50
 
51
+ <label htmlFor={inputId} className="sr-only">Your answer</label>
49
52
  <Input
53
+ id={inputId}
50
54
  value={value}
51
55
  onChange={handleChange}
52
56
  placeholder="Type your answer here..."
@@ -63,4 +67,4 @@ export const FillInTheBlank = ({
63
67
  )}
64
68
  </div>
65
69
  );
66
- };
70
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, memo } from "react";
2
2
  import { Alert, AlertDescription } from "../ui/alert";
3
3
  import { cn } from "../lib/utils";
4
4
  import type { QuestionProps, HotspotRegion } from "./types";
@@ -43,14 +43,14 @@ function getRegionStyle(region: HotspotRegion): React.CSSProperties {
43
43
  * onAnswer={(answers) => handleAnswer(answers)}
44
44
  * />
45
45
  */
46
- export const Hotspot = ({
46
+ export const Hotspot = memo(function Hotspot({
47
47
  question,
48
48
  sessionAnswers,
49
49
  onAnswer,
50
50
  readOnly = false,
51
51
  showCorrectAnswers = false,
52
52
  disabled = false,
53
- }: QuestionProps) => {
53
+ }: QuestionProps) {
54
54
  const multiSelect = question.hotspotMultiSelect ?? false;
55
55
 
56
56
  const [selected, setSelected] = useState<Set<string>>(() => {
@@ -151,4 +151,4 @@ export const Hotspot = ({
151
151
  )}
152
152
  </div>
153
153
  );
154
- };
154
+ });
@@ -12,6 +12,8 @@ export { InlineChoice } from "./inline-choice";
12
12
  export { Scenario } from "./scenario";
13
13
  export { Spreadsheet } from "./spreadsheet";
14
14
  export { scoreQuestion, scoreScenarioSubQuestions } from "./scoring";
15
+ export { ConfidenceIndicator } from "./confidence-indicator";
16
+ export type { ConfidenceIndicatorProps, ConfidenceLevel } from "./confidence-indicator";
15
17
 
16
18
  export type {
17
19
  QuestionProps,
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, memo } from "react";
2
2
  import { Alert, AlertDescription } from "../ui/alert";
3
3
  import { cn } from "../lib/utils";
4
4
  import type { QuestionProps, InlineBlank } from "./types";
@@ -44,14 +44,14 @@ function parseInlineContent(html: string): ContentPart[] {
44
44
  * onAnswer={(answers) => handleAnswer(answers)}
45
45
  * />
46
46
  */
47
- export const InlineChoice = ({
47
+ export const InlineChoice = memo(function InlineChoice({
48
48
  question,
49
49
  sessionAnswers,
50
50
  onAnswer,
51
51
  readOnly = false,
52
52
  showCorrectAnswers = false,
53
53
  disabled = false,
54
- }: QuestionProps) => {
54
+ }: QuestionProps) {
55
55
  const blanksMap = useMemo(() => {
56
56
  const map = new Map<string, InlineBlank>();
57
57
  for (const blank of question.inlineBlanks ?? []) {
@@ -118,6 +118,7 @@ export const InlineChoice = ({
118
118
  return (
119
119
  <select
120
120
  key={part.uid}
121
+ aria-label={`Blank ${blank.sequence + 1}`}
121
122
  value={selections.get(part.uid) || ""}
122
123
  onChange={(e) => handleSelect(part.uid, e.target.value)}
123
124
  disabled={readOnly || disabled}
@@ -148,4 +149,4 @@ export const InlineChoice = ({
148
149
  )}
149
150
  </div>
150
151
  );
151
- };
152
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useCallback } from "react";
1
+ import { useState, useMemo, useCallback, memo } from "react";
2
2
  import { Alert, AlertDescription } from "../ui/alert";
3
3
  import { cn } from "../lib/utils";
4
4
  import type { QuestionProps } from "./types";
@@ -23,14 +23,14 @@ import type { QuestionProps } from "./types";
23
23
  * onAnswer={(answers) => handleAnswer(answers)}
24
24
  * />
25
25
  */
26
- export const Matching = ({
26
+ export const Matching = memo(function Matching({
27
27
  question,
28
28
  sessionAnswers,
29
29
  onAnswer,
30
30
  readOnly = false,
31
31
  showCorrectAnswers = false,
32
32
  disabled = false,
33
- }: QuestionProps) => {
33
+ }: QuestionProps) {
34
34
  const sortedPairs = useMemo(
35
35
  () =>
36
36
  [...(question.matchingPairs ?? [])].sort(
@@ -158,6 +158,7 @@ export const Matching = ({
158
158
  dangerouslySetInnerHTML={{ __html: pair.item }}
159
159
  />
160
160
  <select
161
+ aria-label={`Match for: ${pair.item.replace(/<[^>]+>/g, "")}`}
161
162
  value={matches.get(pair.uid) || ""}
162
163
  onChange={(e) => handleSelect(pair.uid, e.target.value)}
163
164
  disabled={readOnly || disabled}
@@ -225,4 +226,4 @@ export const Matching = ({
225
226
  )}
226
227
  </div>
227
228
  );
228
- };
229
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, memo } from "react";
2
2
  import type { QuestionProps } from "./types";
3
3
  import { Alert, AlertDescription } from "../ui/alert";
4
4
  import { cn } from "../lib/utils";
@@ -12,14 +12,14 @@ import { cn } from "../lib/utils";
12
12
  * onAnswer={(answers) => handleAnswer(answers)}
13
13
  * />
14
14
  */
15
- export const MultipleChoice = ({
15
+ export const MultipleChoice = memo(function MultipleChoice({
16
16
  question,
17
17
  sessionAnswers,
18
18
  onAnswer,
19
19
  readOnly = false,
20
20
  showCorrectAnswers = false,
21
21
  disabled = false,
22
- }: QuestionProps) => {
22
+ }: QuestionProps) {
23
23
  const [selectedAnswers, setSelectedAnswers] = useState<string[]>(
24
24
  () => sessionAnswers?.map((sa) => sa.answerUid) || [],
25
25
  );
@@ -59,7 +59,8 @@ export const MultipleChoice = ({
59
59
  <div className="flex flex-col gap-4">
60
60
  <div dangerouslySetInnerHTML={{ __html: question.content }} />
61
61
 
62
- <div className="flex flex-col gap-2">
62
+ <fieldset className="flex flex-col gap-2 border-0 p-0 m-0">
63
+ <legend className="sr-only">Select all correct answers</legend>
63
64
  {sortedAnswers.map((answer) => (
64
65
  <div
65
66
  key={answer.uid}
@@ -77,10 +78,16 @@ export const MultipleChoice = ({
77
78
  className="text-sm"
78
79
  dangerouslySetInnerHTML={{ __html: answer.content }}
79
80
  />
81
+ {showCorrectAnswers && answer.isCorrect && (
82
+ <span className="sr-only">(Correct answer)</span>
83
+ )}
84
+ {showCorrectAnswers && selectedAnswers.includes(answer.uid) && !answer.isCorrect && (
85
+ <span className="sr-only">(Your answer — incorrect)</span>
86
+ )}
80
87
  </label>
81
88
  </div>
82
89
  ))}
83
- </div>
90
+ </fieldset>
84
91
 
85
92
  {showCorrectAnswers && question.explanation && (
86
93
  <Alert className="mt-2">
@@ -92,4 +99,4 @@ export const MultipleChoice = ({
92
99
  )}
93
100
  </div>
94
101
  );
95
- };
102
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useRef } from "react";
1
+ import { useState, useMemo, useRef, useId, useEffect, memo } from "react";
2
2
  import { debounce } from "../utils/debounce";
3
3
  import { Input } from "../ui/input";
4
4
  import { Alert, AlertDescription } from "../ui/alert";
@@ -14,14 +14,15 @@ import type { QuestionProps } from "./types";
14
14
  * onAnswer={(answers) => handleAnswer(answers)}
15
15
  * />
16
16
  */
17
- export const Numeric = ({
17
+ export const Numeric = memo(function Numeric({
18
18
  question,
19
19
  sessionAnswers,
20
20
  onAnswer,
21
21
  readOnly = false,
22
22
  showCorrectAnswers = false,
23
23
  disabled = false,
24
- }: QuestionProps) => {
24
+ }: QuestionProps) {
25
+ const inputId = useId();
25
26
  const [value, setValue] = useState(() => sessionAnswers?.[0]?.content || "");
26
27
 
27
28
  const onAnswerRef = useRef(onAnswer);
@@ -36,6 +37,7 @@ export const Numeric = ({
36
37
  }, 300),
37
38
  [],
38
39
  );
40
+ useEffect(() => () => debouncedAnswer.cancel(), [debouncedAnswer]);
39
41
 
40
42
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
41
43
  const newValue = e.target.value;
@@ -64,7 +66,9 @@ export const Numeric = ({
64
66
  <div dangerouslySetInnerHTML={{ __html: question.content }} />
65
67
 
66
68
  <div className="flex items-center gap-2">
69
+ <label htmlFor={inputId} className="sr-only">Numeric answer</label>
67
70
  <Input
71
+ id={inputId}
68
72
  type="number"
69
73
  value={value}
70
74
  onChange={handleChange}
@@ -99,4 +103,4 @@ export const Numeric = ({
99
103
  )}
100
104
  </div>
101
105
  );
102
- };
106
+ });
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, memo } from "react";
2
2
  import { ChevronUp, ChevronDown, GripVertical } from "lucide-react";
3
3
  import { Alert, AlertDescription } from "../ui/alert";
4
4
  import { cn } from "../lib/utils";
@@ -15,20 +15,22 @@ import type { QuestionProps, AnswerOption } from "./types";
15
15
  * onAnswer={(answers) => handleAnswer(answers)}
16
16
  * />
17
17
  */
18
- export const Ordering = ({
18
+ export const Ordering = memo(function Ordering({
19
19
  question,
20
20
  sessionAnswers,
21
21
  onAnswer,
22
22
  readOnly = false,
23
23
  showCorrectAnswers = false,
24
24
  disabled = false,
25
- }: QuestionProps) => {
25
+ }: QuestionProps) {
26
26
  const correctOrder = useMemo(
27
27
  () =>
28
28
  [...(question.answers || [])].sort((a, b) => a.sequence - b.sequence),
29
29
  [question.answers],
30
30
  );
31
31
 
32
+ const [announcement, setAnnouncement] = useState("");
33
+
32
34
  const [items, setItems] = useState<AnswerOption[]>(() => {
33
35
  if (sessionAnswers && sessionAnswers.length > 0) {
34
36
  // Rebuild order from sessionAnswers position indices
@@ -66,6 +68,9 @@ export const Ordering = ({
66
68
  next.splice(toIndex, 0, moved);
67
69
  setItems(next);
68
70
  emitAnswer(next);
71
+ setAnnouncement(
72
+ `${moved.content.replace(/<[^>]+>/g, "")} moved to position ${toIndex + 1} of ${items.length}`,
73
+ );
69
74
  };
70
75
 
71
76
  const { getDragProps, dragIndex, dragOverIndex } = useDragReorder({
@@ -85,6 +90,9 @@ export const Ordering = ({
85
90
  return (
86
91
  <div className="flex flex-col gap-4">
87
92
  <div dangerouslySetInnerHTML={{ __html: question.content }} />
93
+ <span className="sr-only" aria-live="assertive" role="status">
94
+ {announcement}
95
+ </span>
88
96
 
89
97
  <div className="flex flex-col gap-1.5">
90
98
  {items.map((item, index) => (
@@ -156,4 +164,4 @@ export const Ordering = ({
156
164
  )}
157
165
  </div>
158
166
  );
159
- };
167
+ });
@@ -1,3 +1,4 @@
1
+ import { memo } from "react";
1
2
  import type { QuestionProps } from "./types";
2
3
  import { MultipleChoice } from "./multiple-choice";
3
4
  import { Choice } from "./choice";
@@ -22,7 +23,7 @@ import { Spreadsheet } from "./spreadsheet";
22
23
  * onAnswer={handleAnswer}
23
24
  * />
24
25
  */
25
- export const QuestionRenderer = (props: QuestionProps) => {
26
+ export const QuestionRenderer = memo(function QuestionRenderer(props: QuestionProps) {
26
27
  switch (props.question.type) {
27
28
  case "multiple_choice":
28
29
  return <MultipleChoice {...props} />;
@@ -55,4 +56,4 @@ export const QuestionRenderer = (props: QuestionProps) => {
55
56
  </p>
56
57
  );
57
58
  }
58
- };
59
+ });