@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
@@ -1,12 +1,23 @@
1
+ import { useMemo } from "react";
1
2
  import { Award, Download, Printer } from "lucide-react";
2
3
  import { cva } from "class-variance-authority";
4
+ import { Skeleton } from "../../ui/skeleton";
3
5
  import { Button } from "../../ui/button";
4
6
  import type { CertificateViewerProps, CertificateVariant } from "./types";
5
7
  import { cn } from "../../lib/utils";
8
+ import { SectionShell } from "../_shared/section-shell";
9
+ import {
10
+ CORNER_CONFIGS,
11
+ CORNER_POSITIONS,
12
+ DIVIDER_CONFIGS,
13
+ BACKGROUND_GLOW,
14
+ ICON_COLORS,
15
+ HEADING_STYLES,
16
+ COURSE_TITLE_COLORS,
17
+ SERIF_VARIANTS,
18
+ } from "./certificate-variants";
6
19
 
7
20
  // ─── Frame CVAs ────────────────────────────────────────────────
8
- // Double-frame variants (classic, elegant, academic) use outer + inner borders
9
- // with a gap between them. Single-frame variants collapse the outer.
10
21
 
11
22
  const outerFrameVariants = cva("mx-auto max-w-4xl", {
12
23
  variants: {
@@ -36,61 +47,14 @@ const innerFrameVariants = cva(
36
47
  "border border-foreground/8 rounded bg-linear-to-b from-background to-muted/20 dark:border-foreground/5 dark:to-muted/10",
37
48
  academic: "border border-info/15 rounded bg-info/3",
38
49
  minimal: "border border-border rounded-lg bg-background",
39
- bold: "border-[3px] border-primary rounded-lg bg-linear-to-br from-primary/8 to-purple/8",
50
+ bold: "border-[3px] border-primary rounded-lg bg-linear-to-br from-primary/8 to-palette-0/8",
40
51
  },
41
52
  },
42
53
  defaultVariants: { variant: "classic" },
43
54
  },
44
55
  );
45
56
 
46
- // ─── Corner ornaments ──────────────────────────────────────────
47
-
48
- interface CornerConfig {
49
- size: number;
50
- paths: { d: string; strokeWidth: number }[];
51
- dots?: { cx: number; cy: number; r: number }[];
52
- color: string;
53
- }
54
-
55
- const CORNER_CONFIGS: Record<CertificateVariant, CornerConfig | null> = {
56
- classic: {
57
- size: 40,
58
- paths: [
59
- { d: "M 2 38 L 2 10 Q 2 2 10 2 L 38 2", strokeWidth: 1.5 },
60
- { d: "M 7 38 L 7 13 Q 7 7 13 7 L 38 7", strokeWidth: 0.75 },
61
- ],
62
- color: "text-warning/50",
63
- },
64
- modern: null,
65
- elegant: {
66
- size: 32,
67
- paths: [{ d: "M 0 30 L 0 0 L 30 0", strokeWidth: 0.5 }],
68
- dots: [{ cx: 2.5, cy: 2.5, r: 1.5 }],
69
- color: "text-foreground/20",
70
- },
71
- academic: {
72
- size: 36,
73
- paths: [
74
- { d: "M 0 36 L 0 0 L 36 0", strokeWidth: 2 },
75
- { d: "M 5 36 L 5 5 L 36 5", strokeWidth: 1 },
76
- ],
77
- color: "text-info/35",
78
- },
79
- minimal: null,
80
- bold: {
81
- size: 20,
82
- paths: [],
83
- dots: [{ cx: 5, cy: 5, r: 5 }],
84
- color: "text-primary/40",
85
- },
86
- };
87
-
88
- const CORNER_POSITIONS = [
89
- { key: "tl", pos: "top-3 left-3", transform: undefined },
90
- { key: "tr", pos: "top-3 right-3", transform: "scaleX(-1)" },
91
- { key: "bl", pos: "bottom-3 left-3", transform: "scaleY(-1)" },
92
- { key: "br", pos: "bottom-3 right-3", transform: "scale(-1)" },
93
- ] as const;
57
+ // ─── Sub-components ─────────────────────────────────────────────
94
58
 
95
59
  function CornerOrnaments({ variant }: { variant: CertificateVariant }) {
96
60
  const config = CORNER_CONFIGS[variant];
@@ -129,96 +93,6 @@ function CornerOrnaments({ variant }: { variant: CertificateVariant }) {
129
93
  );
130
94
  }
131
95
 
132
- // ─── Ornamental dividers ───────────────────────────────────────
133
-
134
- interface DividerConfig {
135
- lineColor: string;
136
- motif: React.ReactNode;
137
- }
138
-
139
- const DIVIDER_CONFIGS: Record<CertificateVariant, DividerConfig> = {
140
- classic: {
141
- lineColor: "bg-warning/30",
142
- motif: (
143
- <svg
144
- width="12"
145
- height="12"
146
- viewBox="0 0 12 12"
147
- className="text-warning/50 shrink-0"
148
- aria-hidden="true"
149
- >
150
- <path d="M6 0 L12 6 L6 12 L0 6 Z" fill="currentColor" />
151
- </svg>
152
- ),
153
- },
154
- modern: {
155
- lineColor: "bg-primary/20",
156
- motif: (
157
- <svg
158
- width="8"
159
- height="8"
160
- viewBox="0 0 8 8"
161
- className="text-primary/40 shrink-0"
162
- aria-hidden="true"
163
- >
164
- <circle cx="4" cy="4" r="3" fill="currentColor" />
165
- </svg>
166
- ),
167
- },
168
- elegant: {
169
- lineColor: "bg-foreground/10",
170
- motif: (
171
- <svg
172
- width="10"
173
- height="10"
174
- viewBox="0 0 10 10"
175
- className="text-foreground/15 shrink-0"
176
- aria-hidden="true"
177
- >
178
- <path
179
- d="M5 0 L6 4 L10 5 L6 6 L5 10 L4 6 L0 5 L4 4 Z"
180
- fill="currentColor"
181
- />
182
- </svg>
183
- ),
184
- },
185
- academic: {
186
- lineColor: "bg-info/25",
187
- motif: (
188
- <svg
189
- width="10"
190
- height="10"
191
- viewBox="0 0 10 10"
192
- className="text-info/40 shrink-0"
193
- aria-hidden="true"
194
- >
195
- <path
196
- d="M4 0 L6 0 L6 4 L10 4 L10 6 L6 6 L6 10 L4 10 L4 6 L0 6 L0 4 L4 4 Z"
197
- fill="currentColor"
198
- />
199
- </svg>
200
- ),
201
- },
202
- minimal: {
203
- lineColor: "bg-border",
204
- motif: null,
205
- },
206
- bold: {
207
- lineColor: "bg-primary/40",
208
- motif: (
209
- <svg
210
- width="10"
211
- height="10"
212
- viewBox="0 0 10 10"
213
- className="text-primary/50 shrink-0"
214
- aria-hidden="true"
215
- >
216
- <rect x="1" y="1" width="8" height="8" fill="currentColor" />
217
- </svg>
218
- ),
219
- },
220
- };
221
-
222
96
  function OrnamentalDivider({
223
97
  variant,
224
98
  className,
@@ -243,76 +117,6 @@ function OrnamentalDivider({
243
117
  );
244
118
  }
245
119
 
246
- // ─── Background glow ───────────────────────────────────────────
247
- // Subtle radial glows using CSS vars for dark mode compat
248
-
249
- const BACKGROUND_GLOW: Record<
250
- CertificateVariant,
251
- React.CSSProperties | null
252
- > = {
253
- classic: {
254
- background:
255
- "radial-gradient(ellipse at center, var(--color-warning) 0%, transparent 65%)",
256
- opacity: 0.06,
257
- },
258
- modern: null,
259
- elegant: {
260
- background:
261
- "radial-gradient(ellipse at center, var(--color-foreground) 0%, transparent 70%)",
262
- opacity: 0.03,
263
- },
264
- academic: {
265
- background:
266
- "radial-gradient(ellipse at center, var(--color-info) 0%, transparent 65%)",
267
- opacity: 0.05,
268
- },
269
- minimal: null,
270
- bold: {
271
- background:
272
- "radial-gradient(ellipse at 30% 50%, var(--color-primary) 0%, transparent 50%), radial-gradient(ellipse at 70% 50%, var(--color-purple) 0%, transparent 50%)",
273
- opacity: 0.06,
274
- },
275
- };
276
-
277
- // ─── Typography maps ───────────────────────────────────────────
278
-
279
- const ICON_COLORS: Record<CertificateVariant, string> = {
280
- classic: "text-warning",
281
- modern: "text-primary",
282
- elegant: "text-foreground/60",
283
- academic: "text-info",
284
- minimal: "text-muted-foreground",
285
- bold: "text-primary",
286
- };
287
-
288
- const HEADING_STYLES: Record<CertificateVariant, string> = {
289
- classic:
290
- "uppercase tracking-[4px] text-base font-medium text-foreground/80 font-serif",
291
- modern: "uppercase tracking-[3px] text-base font-medium text-primary/80",
292
- elegant:
293
- "uppercase tracking-[5px] text-sm font-light text-foreground/50",
294
- academic:
295
- "uppercase tracking-[3px] text-base font-semibold text-info/80",
296
- minimal:
297
- "uppercase tracking-[2px] text-sm font-normal text-muted-foreground",
298
- bold: "uppercase tracking-[3px] text-lg font-black text-primary",
299
- };
300
-
301
- const COURSE_TITLE_COLORS: Record<CertificateVariant, string> = {
302
- classic: "text-warning",
303
- modern: "text-primary",
304
- elegant: "text-foreground/80",
305
- academic: "text-info",
306
- minimal: "text-foreground",
307
- bold: "text-primary",
308
- };
309
-
310
- const SERIF_VARIANTS: ReadonlySet<CertificateVariant> = new Set([
311
- "classic",
312
- "elegant",
313
- "academic",
314
- ]);
315
-
316
120
  // ─── Main component ────────────────────────────────────────────
317
121
 
318
122
  export function CertificateViewer({
@@ -327,10 +131,13 @@ export function CertificateViewer({
327
131
  showActions = true,
328
132
  onPrint,
329
133
  onDownload,
134
+ isLoading,
135
+ error,
136
+ onRetry,
330
137
  className,
331
138
  style,
332
139
  }: CertificateViewerProps) {
333
- const formattedDate = (() => {
140
+ const formattedDate = useMemo(() => {
334
141
  try {
335
142
  return new Date(completionDate).toLocaleDateString("en-US", {
336
143
  year: "numeric",
@@ -340,7 +147,7 @@ export function CertificateViewer({
340
147
  } catch {
341
148
  return completionDate;
342
149
  }
343
- })();
150
+ }, [completionDate]);
344
151
 
345
152
  function handlePrint() {
346
153
  if (onPrint) {
@@ -353,15 +160,21 @@ export function CertificateViewer({
353
160
  const glowStyle = BACKGROUND_GLOW[variant];
354
161
 
355
162
  return (
163
+ <SectionShell
164
+ isLoading={isLoading}
165
+ error={error}
166
+ onRetry={onRetry}
167
+ className={className}
168
+ style={style}
169
+ skeleton={<Skeleton className="h-80 w-full rounded-lg border" />}
170
+ >
356
171
  <div className={className} style={style}>
357
172
  {/* Outer certificate frame */}
358
173
  <div className={outerFrameVariants({ variant })}>
359
174
  {/* Inner certificate frame */}
360
175
  <div className={innerFrameVariants({ variant })}>
361
- {/* Corner ornaments */}
362
176
  <CornerOrnaments variant={variant} />
363
177
 
364
- {/* Background radial glow */}
365
178
  {glowStyle && (
366
179
  <div
367
180
  className="absolute inset-0 pointer-events-none rounded-[inherit]"
@@ -370,9 +183,7 @@ export function CertificateViewer({
370
183
  />
371
184
  )}
372
185
 
373
- {/* Content */}
374
186
  <div className="relative">
375
- {/* Logo / Icon */}
376
187
  {organizationLogo ? (
377
188
  <img
378
189
  src={organizationLogo}
@@ -386,20 +197,16 @@ export function CertificateViewer({
386
197
  />
387
198
  )}
388
199
 
389
- {/* Certificate heading */}
390
200
  <p className={cn("mb-2", HEADING_STYLES[variant])}>
391
201
  Certificate of Completion
392
202
  </p>
393
203
 
394
- {/* Top ornamental divider */}
395
204
  <OrnamentalDivider variant={variant} />
396
205
 
397
- {/* Certify text */}
398
206
  <p className="text-base text-foreground/70 mb-2">
399
207
  This is to certify that
400
208
  </p>
401
209
 
402
- {/* Recipient name */}
403
210
  <p
404
211
  className={cn(
405
212
  "text-4xl font-bold mb-4 text-foreground",
@@ -409,12 +216,10 @@ export function CertificateViewer({
409
216
  {recipientName}
410
217
  </p>
411
218
 
412
- {/* Completion text */}
413
219
  <p className="text-base text-foreground/70 mb-2">
414
220
  has successfully completed
415
221
  </p>
416
222
 
417
- {/* Course title */}
418
223
  <p
419
224
  className={cn(
420
225
  "text-2xl font-semibold mb-4",
@@ -424,12 +229,10 @@ export function CertificateViewer({
424
229
  {courseTitle}
425
230
  </p>
426
231
 
427
- {/* Issuance line */}
428
232
  <p className="text-base text-foreground/60 mb-6">
429
233
  Issued by {organizationName} on {formattedDate}
430
234
  </p>
431
235
 
432
- {/* Signatory */}
433
236
  {signatory && (
434
237
  <div className="mt-6 mb-4">
435
238
  <OrnamentalDivider variant={variant} />
@@ -442,7 +245,6 @@ export function CertificateViewer({
442
245
  </div>
443
246
  )}
444
247
 
445
- {/* Certificate ID */}
446
248
  {certificateId && (
447
249
  <span className="block text-xs text-muted-foreground mt-4">
448
250
  Certificate ID: {certificateId}
@@ -452,7 +254,6 @@ export function CertificateViewer({
452
254
  </div>
453
255
  </div>
454
256
 
455
- {/* Actions */}
456
257
  {showActions && (
457
258
  <div className="flex justify-center gap-3 mt-6">
458
259
  <Button variant="outline" onClick={handlePrint}>
@@ -466,5 +267,6 @@ export function CertificateViewer({
466
267
  </div>
467
268
  )}
468
269
  </div>
270
+ </SectionShell>
469
271
  );
470
272
  }
@@ -0,0 +1,170 @@
1
+ import type { CertificateVariant } from "./types";
2
+
3
+ // ─── Corner ornaments ──────────────────────────────────────────
4
+
5
+ export interface CornerConfig {
6
+ size: number;
7
+ paths: { d: string; strokeWidth: number }[];
8
+ dots?: { cx: number; cy: number; r: number }[];
9
+ color: string;
10
+ }
11
+
12
+ export const CORNER_CONFIGS: Record<CertificateVariant, CornerConfig | null> = {
13
+ classic: {
14
+ size: 40,
15
+ paths: [
16
+ { d: "M 2 38 L 2 10 Q 2 2 10 2 L 38 2", strokeWidth: 1.5 },
17
+ { d: "M 7 38 L 7 13 Q 7 7 13 7 L 38 7", strokeWidth: 0.75 },
18
+ ],
19
+ color: "text-warning/50",
20
+ },
21
+ modern: null,
22
+ elegant: {
23
+ size: 32,
24
+ paths: [{ d: "M 0 30 L 0 0 L 30 0", strokeWidth: 0.5 }],
25
+ dots: [{ cx: 2.5, cy: 2.5, r: 1.5 }],
26
+ color: "text-foreground/20",
27
+ },
28
+ academic: {
29
+ size: 36,
30
+ paths: [
31
+ { d: "M 0 36 L 0 0 L 36 0", strokeWidth: 2 },
32
+ { d: "M 5 36 L 5 5 L 36 5", strokeWidth: 1 },
33
+ ],
34
+ color: "text-info/35",
35
+ },
36
+ minimal: null,
37
+ bold: {
38
+ size: 20,
39
+ paths: [],
40
+ dots: [{ cx: 5, cy: 5, r: 5 }],
41
+ color: "text-primary/40",
42
+ },
43
+ };
44
+
45
+ export const CORNER_POSITIONS = [
46
+ { key: "tl", pos: "top-3 left-3", transform: undefined },
47
+ { key: "tr", pos: "top-3 right-3", transform: "scaleX(-1)" },
48
+ { key: "bl", pos: "bottom-3 left-3", transform: "scaleY(-1)" },
49
+ { key: "br", pos: "bottom-3 right-3", transform: "scale(-1)" },
50
+ ] as const;
51
+
52
+ // ─── Divider configs ────────────────────────────────────────────
53
+
54
+ export interface DividerConfig {
55
+ lineColor: string;
56
+ motif: React.ReactNode;
57
+ }
58
+
59
+ export const DIVIDER_CONFIGS: Record<CertificateVariant, DividerConfig> = {
60
+ classic: {
61
+ lineColor: "bg-warning/30",
62
+ motif: (
63
+ <svg width="12" height="12" viewBox="0 0 12 12" className="text-warning/50 shrink-0" aria-hidden="true">
64
+ <path d="M6 0 L12 6 L6 12 L0 6 Z" fill="currentColor" />
65
+ </svg>
66
+ ),
67
+ },
68
+ modern: {
69
+ lineColor: "bg-primary/20",
70
+ motif: (
71
+ <svg width="8" height="8" viewBox="0 0 8 8" className="text-primary/40 shrink-0" aria-hidden="true">
72
+ <circle cx="4" cy="4" r="3" fill="currentColor" />
73
+ </svg>
74
+ ),
75
+ },
76
+ elegant: {
77
+ lineColor: "bg-foreground/10",
78
+ motif: (
79
+ <svg width="10" height="10" viewBox="0 0 10 10" className="text-foreground/15 shrink-0" aria-hidden="true">
80
+ <path d="M5 0 L6 4 L10 5 L6 6 L5 10 L4 6 L0 5 L4 4 Z" fill="currentColor" />
81
+ </svg>
82
+ ),
83
+ },
84
+ academic: {
85
+ lineColor: "bg-info/25",
86
+ motif: (
87
+ <svg width="10" height="10" viewBox="0 0 10 10" className="text-info/40 shrink-0" aria-hidden="true">
88
+ <path d="M4 0 L6 0 L6 4 L10 4 L10 6 L6 6 L6 10 L4 10 L4 6 L0 6 L0 4 L4 4 Z" fill="currentColor" />
89
+ </svg>
90
+ ),
91
+ },
92
+ minimal: {
93
+ lineColor: "bg-border",
94
+ motif: null,
95
+ },
96
+ bold: {
97
+ lineColor: "bg-primary/40",
98
+ motif: (
99
+ <svg width="10" height="10" viewBox="0 0 10 10" className="text-primary/50 shrink-0" aria-hidden="true">
100
+ <rect x="1" y="1" width="8" height="8" fill="currentColor" />
101
+ </svg>
102
+ ),
103
+ },
104
+ };
105
+
106
+ // ─── Background glow ────────────────────────────────────────────
107
+
108
+ export const BACKGROUND_GLOW: Record<CertificateVariant, React.CSSProperties | null> = {
109
+ classic: {
110
+ background:
111
+ "radial-gradient(ellipse at center, var(--color-warning) 0%, transparent 65%)",
112
+ opacity: 0.06,
113
+ },
114
+ modern: null,
115
+ elegant: {
116
+ background:
117
+ "radial-gradient(ellipse at center, var(--color-foreground) 0%, transparent 70%)",
118
+ opacity: 0.03,
119
+ },
120
+ academic: {
121
+ background:
122
+ "radial-gradient(ellipse at center, var(--color-info) 0%, transparent 65%)",
123
+ opacity: 0.05,
124
+ },
125
+ minimal: null,
126
+ bold: {
127
+ background:
128
+ "radial-gradient(ellipse at 30% 50%, var(--color-primary) 0%, transparent 50%), radial-gradient(ellipse at 70% 50%, var(--color-palette-0) 0%, transparent 50%)",
129
+ opacity: 0.06,
130
+ },
131
+ };
132
+
133
+ // ─── Typography maps ────────────────────────────────────────────
134
+
135
+ export const ICON_COLORS: Record<CertificateVariant, string> = {
136
+ classic: "text-warning",
137
+ modern: "text-primary",
138
+ elegant: "text-foreground/60",
139
+ academic: "text-info",
140
+ minimal: "text-muted-foreground",
141
+ bold: "text-primary",
142
+ };
143
+
144
+ export const HEADING_STYLES: Record<CertificateVariant, string> = {
145
+ classic:
146
+ "uppercase tracking-[4px] text-base font-medium text-foreground/80 font-serif",
147
+ modern: "uppercase tracking-[3px] text-base font-medium text-primary/80",
148
+ elegant:
149
+ "uppercase tracking-[5px] text-sm font-light text-foreground/50",
150
+ academic:
151
+ "uppercase tracking-[3px] text-base font-semibold text-info/80",
152
+ minimal:
153
+ "uppercase tracking-[2px] text-sm font-normal text-muted-foreground",
154
+ bold: "uppercase tracking-[3px] text-lg font-black text-primary",
155
+ };
156
+
157
+ export const COURSE_TITLE_COLORS: Record<CertificateVariant, string> = {
158
+ classic: "text-warning",
159
+ modern: "text-primary",
160
+ elegant: "text-foreground/80",
161
+ academic: "text-info",
162
+ minimal: "text-foreground",
163
+ bold: "text-primary",
164
+ };
165
+
166
+ export const SERIF_VARIANTS: ReadonlySet<CertificateVariant> = new Set([
167
+ "classic",
168
+ "elegant",
169
+ "academic",
170
+ ]);
@@ -46,6 +46,12 @@ export interface CertificateViewerProps {
46
46
  onPrint?: () => void;
47
47
  /** Called when download is triggered */
48
48
  onDownload?: () => void;
49
+ /** Render skeleton placeholders instead of content */
50
+ isLoading?: boolean;
51
+ /** Error message — renders an error state with optional retry */
52
+ error?: string | null;
53
+ /** Called when the user clicks retry in the error state */
54
+ onRetry?: () => void;
49
55
  /** CSS class name for the root element */
50
56
  className?: string;
51
57
  /** Inline styles for the root element */