@hydralms/components 0.1.2 → 0.2.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 (199) hide show
  1. package/dist/ForumBoard-CHXU3mjC.js +2207 -0
  2. package/dist/ForumBoard-d1w5-r6n.cjs +1 -0
  3. package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
  4. package/dist/assessment-toolbar/index.d.ts +5 -1
  5. package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
  6. package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
  7. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  8. package/dist/assessment-toolbar/types.d.ts +52 -4
  9. package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
  10. package/dist/common/index.d.ts +2 -1
  11. package/dist/common/stepper.d.ts +6 -0
  12. package/dist/common/types.d.ts +37 -0
  13. package/dist/components.css +1 -1
  14. package/dist/content/attachment-list.d.ts +6 -0
  15. package/dist/content/content-block.d.ts +1 -1
  16. package/dist/content/index.d.ts +2 -1
  17. package/dist/content/types.d.ts +39 -0
  18. package/dist/curriculum/curriculum-item.d.ts +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.js +551 -312
  21. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +8 -0
  22. package/dist/modules/AssignmentModule/types.d.ts +65 -0
  23. package/dist/modules/CertificateModule/CertificateModule.d.ts +9 -0
  24. package/dist/modules/CertificateModule/types.d.ts +49 -0
  25. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +8 -0
  26. package/dist/modules/DiscussionModule/types.d.ts +47 -0
  27. package/dist/modules/ExamModule/ExamModule.d.ts +8 -0
  28. package/dist/modules/ExamModule/types.d.ts +64 -0
  29. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +9 -0
  30. package/dist/modules/GradeCenterModule/types.d.ts +54 -0
  31. package/dist/modules/QuizModule/QuizModule.d.ts +1 -1
  32. package/dist/modules/QuizModule/types.d.ts +6 -1
  33. package/dist/modules/SurveyModule/SurveyModule.d.ts +7 -0
  34. package/dist/modules/SurveyModule/types.d.ts +49 -0
  35. package/dist/modules/index.d.ts +12 -0
  36. package/dist/modules.cjs +1 -0
  37. package/dist/modules.js +1422 -0
  38. package/dist/progress/achievement-badge.d.ts +6 -0
  39. package/dist/progress/activity-timeline.d.ts +6 -0
  40. package/dist/progress/index.d.ts +4 -1
  41. package/dist/progress/stat-card.d.ts +1 -1
  42. package/dist/progress/streak-badge.d.ts +6 -0
  43. package/dist/progress/types.d.ts +97 -0
  44. package/dist/questions/essay.d.ts +1 -1
  45. package/dist/questions/hotspot.d.ts +21 -0
  46. package/dist/questions/index.d.ts +9 -1
  47. package/dist/questions/inline-choice.d.ts +21 -0
  48. package/dist/questions/matching.d.ts +22 -0
  49. package/dist/questions/numeric.d.ts +11 -0
  50. package/dist/questions/ordering.d.ts +12 -0
  51. package/dist/questions/scenario.d.ts +23 -0
  52. package/dist/questions/scoring.d.ts +22 -0
  53. package/dist/questions/spreadsheet.d.ts +29 -0
  54. package/dist/questions/types.d.ts +106 -1
  55. package/dist/questions/use-drag-reorder.d.ts +17 -0
  56. package/dist/sections/CertificateViewer/types.d.ts +7 -5
  57. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  58. package/dist/sections/ExamSession/types.d.ts +6 -1
  59. package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
  60. package/dist/sections/ForumBoard/types.d.ts +64 -0
  61. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  62. package/dist/sections/QuizSession/types.d.ts +6 -1
  63. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
  64. package/dist/sections/RequirementsChecklist/types.d.ts +37 -0
  65. package/dist/sections/RubricView/RubricView.d.ts +9 -0
  66. package/dist/sections/RubricView/types.d.ts +50 -0
  67. package/dist/sections/index.d.ts +7 -1
  68. package/dist/sections.cjs +1 -1
  69. package/dist/sections.js +250 -1715
  70. package/dist/social/post-card.d.ts +1 -1
  71. package/dist/tabs-DRM2Iq_J.cjs +172 -0
  72. package/dist/tabs-Wf3h_Cx3.js +21580 -0
  73. package/dist/ui/alert.d.ts +1 -1
  74. package/dist/ui/badge.d.ts +1 -1
  75. package/dist/ui/button.d.ts +1 -1
  76. package/dist/ui/drawer.d.ts +84 -0
  77. package/dist/ui/index.d.ts +3 -0
  78. package/dist/ui/progress.d.ts +1 -1
  79. package/dist/ui/rich-text-editor.d.ts +30 -0
  80. package/dist/ui/rich-text-toolbar.d.ts +8 -0
  81. package/dist/utils/array-utils.d.ts +4 -0
  82. package/dist/utils/flatten-leaves.d.ts +6 -0
  83. package/dist/utils/format-file-size.d.ts +1 -0
  84. package/dist/utils/format-timestamp.d.ts +1 -0
  85. package/dist/utils/is-empty-html.d.ts +5 -0
  86. package/dist/utils/shuffle.d.ts +1 -0
  87. package/dist/utils/string-utils.d.ts +12 -0
  88. package/dist/video/video-bookmark.d.ts +1 -1
  89. package/dist/video/video-playlist-item.d.ts +1 -1
  90. package/package.json +141 -3
  91. package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
  92. package/src/assessment-toolbar/index.ts +6 -0
  93. package/src/assessment-toolbar/question-header-bar.tsx +61 -0
  94. package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
  95. package/src/assessment-toolbar/question-navigator.tsx +3 -31
  96. package/src/assessment-toolbar/timer-display.tsx +2 -2
  97. package/src/assessment-toolbar/types.ts +54 -4
  98. package/src/assessment-toolbar/use-countdown.ts +153 -0
  99. package/src/common/index.ts +3 -0
  100. package/src/common/search-input.tsx +7 -6
  101. package/src/common/stepper.tsx +100 -0
  102. package/src/common/types.ts +39 -0
  103. package/src/content/attachment-list.tsx +90 -0
  104. package/src/content/content-block.tsx +4 -2
  105. package/src/content/file-upload-zone.tsx +1 -6
  106. package/src/content/index.ts +3 -0
  107. package/src/content/types.ts +41 -0
  108. package/src/curriculum/curriculum-item.tsx +7 -3
  109. package/src/feedback/feedback-banner.tsx +12 -14
  110. package/src/flashcards/flashcard-deck.tsx +1 -9
  111. package/src/flashcards/flashcard.tsx +1 -1
  112. package/src/modules/AssignmentModule/AssignmentModule.tsx +305 -0
  113. package/src/modules/AssignmentModule/types.ts +73 -0
  114. package/src/modules/CertificateModule/CertificateModule.tsx +161 -0
  115. package/src/modules/CertificateModule/types.ts +47 -0
  116. package/src/modules/CoursePlayer/CoursePlayer.tsx +44 -48
  117. package/src/modules/DiscussionModule/DiscussionModule.tsx +110 -0
  118. package/src/modules/DiscussionModule/types.ts +54 -0
  119. package/src/modules/ExamModule/ExamModule.tsx +285 -0
  120. package/src/modules/ExamModule/types.ts +66 -0
  121. package/src/modules/FlashcardLab/FlashcardLab.tsx +29 -16
  122. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +169 -0
  123. package/src/modules/GradeCenterModule/types.ts +63 -0
  124. package/src/modules/QuizModule/QuizModule.tsx +88 -88
  125. package/src/modules/QuizModule/types.ts +6 -1
  126. package/src/modules/SurveyModule/SurveyModule.tsx +180 -0
  127. package/src/modules/SurveyModule/types.ts +51 -0
  128. package/src/modules/index.ts +24 -0
  129. package/src/progress/achievement-badge.tsx +52 -0
  130. package/src/progress/activity-timeline.tsx +84 -0
  131. package/src/progress/index.ts +7 -0
  132. package/src/progress/stat-card.tsx +30 -18
  133. package/src/progress/streak-badge.tsx +35 -0
  134. package/src/progress/types.ts +101 -0
  135. package/src/questions/choice.tsx +7 -9
  136. package/src/questions/essay.tsx +23 -25
  137. package/src/questions/fill-in-the-blank.tsx +13 -16
  138. package/src/questions/hotspot.tsx +154 -0
  139. package/src/questions/index.ts +16 -0
  140. package/src/questions/inline-choice.tsx +151 -0
  141. package/src/questions/matching.tsx +228 -0
  142. package/src/questions/multiple-choice.tsx +7 -9
  143. package/src/questions/numeric.tsx +102 -0
  144. package/src/questions/ordering.tsx +159 -0
  145. package/src/questions/question-renderer.tsx +21 -0
  146. package/src/questions/scenario.tsx +140 -0
  147. package/src/questions/scoring.ts +201 -0
  148. package/src/questions/spreadsheet.tsx +259 -0
  149. package/src/questions/true-false.tsx +7 -9
  150. package/src/questions/types.ts +123 -1
  151. package/src/questions/use-drag-reorder.ts +80 -0
  152. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +2 -15
  153. package/src/sections/AssessmentReview/AssessmentReview.tsx +13 -2
  154. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +7 -5
  155. package/src/sections/CertificateViewer/CertificateViewer.tsx +409 -56
  156. package/src/sections/CertificateViewer/types.ts +13 -5
  157. package/src/sections/CourseOutline/CourseOutline.tsx +4 -14
  158. package/src/sections/DiscussionThread/DiscussionThread.tsx +13 -10
  159. package/src/sections/ExamSession/ExamSession.tsx +44 -7
  160. package/src/sections/ExamSession/types.ts +6 -1
  161. package/src/sections/ForumBoard/ForumBoard.tsx +284 -0
  162. package/src/sections/ForumBoard/types.ts +67 -0
  163. package/src/sections/GradebookTable/GradebookTable.tsx +1 -1
  164. package/src/sections/LecturePlayer/LecturePlayer.tsx +1 -1
  165. package/src/sections/LessonPage/LessonPage.tsx +5 -9
  166. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +15 -26
  167. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +65 -65
  168. package/src/sections/QuizSession/QuizSession.tsx +67 -8
  169. package/src/sections/QuizSession/types.ts +6 -1
  170. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +107 -0
  171. package/src/sections/RequirementsChecklist/types.ts +38 -0
  172. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +4 -9
  173. package/src/sections/RubricView/RubricView.tsx +138 -0
  174. package/src/sections/RubricView/types.ts +52 -0
  175. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +23 -9
  176. package/src/sections/SurveyForm/SurveyForm.tsx +8 -5
  177. package/src/sections/index.ts +20 -1
  178. package/src/social/post-card.tsx +8 -19
  179. package/src/social/user-avatar.tsx +1 -0
  180. package/src/styles/globals.css +13 -0
  181. package/src/ui/drawer.tsx +600 -0
  182. package/src/ui/index.ts +19 -0
  183. package/src/ui/rich-text-editor.tsx +109 -0
  184. package/src/ui/rich-text-toolbar.tsx +156 -0
  185. package/src/utils/array-utils.ts +17 -0
  186. package/src/utils/flatten-leaves.ts +17 -0
  187. package/src/utils/format-file-size.ts +5 -0
  188. package/src/utils/format-timestamp.ts +13 -0
  189. package/src/utils/is-empty-html.ts +7 -0
  190. package/src/utils/shuffle.ts +8 -0
  191. package/src/utils/string-utils.ts +30 -0
  192. package/src/video/video-bookmark.tsx +4 -3
  193. package/src/video/video-chapter-list.tsx +9 -4
  194. package/src/video/video-player.tsx +11 -4
  195. package/src/video/video-playlist-item.tsx +8 -3
  196. package/src/video/video-thumbnail-card.tsx +4 -0
  197. package/src/video/video-transcript.tsx +8 -5
  198. package/dist/table-BrS5cDQu.js +0 -2510
  199. package/dist/table-D6AkBBEo.cjs +0 -1
@@ -1,15 +1,320 @@
1
1
  import { Award, Download, Printer } from "lucide-react";
2
+ import { cva } from "class-variance-authority";
2
3
  import { Button } from "../../ui/button";
3
- import { Separator } from "../../ui/separator";
4
- import type { CertificateViewerProps } from "./types";
4
+ import type { CertificateViewerProps, CertificateVariant } from "./types";
5
5
  import { cn } from "../../lib/utils";
6
6
 
7
- const VARIANT_CLASSES: Record<string, string> = {
8
- classic: "border-[3px] border-double border-warning bg-linear-to-br from-[#fffbe6] to-[#fff8e1]",
9
- modern: "border border-primary bg-linear-to-br from-[#fff5f5] to-[#fee2e2]",
10
- minimal: "border border-border",
7
+ // ─── 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
+
11
+ const outerFrameVariants = cva("mx-auto max-w-4xl", {
12
+ variants: {
13
+ variant: {
14
+ classic:
15
+ "border-[3px] border-double border-warning/50 rounded-lg p-1.5",
16
+ modern: "rounded-lg",
17
+ elegant:
18
+ "border border-foreground/15 rounded-lg p-2.5 dark:border-foreground/10",
19
+ academic: "border-2 border-info/30 rounded-lg p-1.5",
20
+ minimal: "rounded-lg",
21
+ bold: "rounded-lg",
22
+ },
23
+ },
24
+ defaultVariants: { variant: "classic" },
25
+ });
26
+
27
+ const innerFrameVariants = cva(
28
+ "relative text-center p-8 sm:p-12 md:p-16 overflow-hidden",
29
+ {
30
+ variants: {
31
+ variant: {
32
+ classic: "border border-warning/25 rounded bg-warning/5",
33
+ modern:
34
+ "border border-primary/20 rounded-lg bg-linear-to-br from-primary/5 to-primary/8 border-t-[3px] border-t-primary/60",
35
+ elegant:
36
+ "border border-foreground/8 rounded bg-linear-to-b from-background to-muted/20 dark:border-foreground/5 dark:to-muted/10",
37
+ academic: "border border-info/15 rounded bg-info/3",
38
+ 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",
40
+ },
41
+ },
42
+ defaultVariants: { variant: "classic" },
43
+ },
44
+ );
45
+
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
+ },
11
86
  };
12
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;
94
+
95
+ function CornerOrnaments({ variant }: { variant: CertificateVariant }) {
96
+ const config = CORNER_CONFIGS[variant];
97
+ if (!config) return null;
98
+
99
+ return (
100
+ <>
101
+ {CORNER_POSITIONS.map(({ key, pos, transform }) => (
102
+ <svg
103
+ key={key}
104
+ className={cn("absolute pointer-events-none", pos, config.color)}
105
+ width={config.size}
106
+ height={config.size}
107
+ viewBox={`0 0 ${config.size} ${config.size}`}
108
+ fill="none"
109
+ stroke="currentColor"
110
+ style={transform ? { transform } : undefined}
111
+ aria-hidden="true"
112
+ >
113
+ {config.paths.map((p, i) => (
114
+ <path key={i} d={p.d} strokeWidth={p.strokeWidth} />
115
+ ))}
116
+ {config.dots?.map((dot, i) => (
117
+ <circle
118
+ key={`d${i}`}
119
+ cx={dot.cx}
120
+ cy={dot.cy}
121
+ r={dot.r}
122
+ fill="currentColor"
123
+ stroke="none"
124
+ />
125
+ ))}
126
+ </svg>
127
+ ))}
128
+ </>
129
+ );
130
+ }
131
+
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
+ function OrnamentalDivider({
223
+ variant,
224
+ className,
225
+ }: {
226
+ variant: CertificateVariant;
227
+ className?: string;
228
+ }) {
229
+ const config = DIVIDER_CONFIGS[variant];
230
+ return (
231
+ <div
232
+ className={cn(
233
+ "flex items-center justify-center gap-3 my-5 mx-auto max-w-70",
234
+ className,
235
+ )}
236
+ role="separator"
237
+ aria-hidden="true"
238
+ >
239
+ <span className={cn("flex-1 h-px", config.lineColor)} />
240
+ {config.motif}
241
+ <span className={cn("flex-1 h-px", config.lineColor)} />
242
+ </div>
243
+ );
244
+ }
245
+
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
+ // ─── Main component ────────────────────────────────────────────
317
+
13
318
  export function CertificateViewer({
14
319
  recipientName,
15
320
  courseTitle,
@@ -45,63 +350,111 @@ export function CertificateViewer({
45
350
  }
46
351
  }
47
352
 
353
+ const glowStyle = BACKGROUND_GLOW[variant];
354
+
48
355
  return (
49
356
  <div className={className} style={style}>
50
- <div
51
- className={cn(
52
- "p-3 sm:p-5 md:p-6 text-center max-w-200 mx-auto rounded-md",
53
- VARIANT_CLASSES[variant],
54
- )}
55
- >
56
- {/* Logo or icon */}
57
- {organizationLogo ? (
58
- <img
59
- src={organizationLogo}
60
- alt={organizationName}
61
- className="h-15 mb-2 mx-auto block"
62
- />
63
- ) : (
64
- <Award
65
- size={48}
66
- className={cn("mx-auto mb-4", variant === "classic" && "text-warning")}
67
- />
68
- )}
69
-
70
- <p className="uppercase tracking-[3px] text-sm text-foreground/70">
71
- Certificate of Completion
72
- </p>
73
-
74
- <Separator className="my-2 mx-auto max-w-50" />
75
-
76
- <p className="text-sm text-foreground mb-1">This is to certify that</p>
77
- <p className={cn("text-2xl font-bold mb-2 text-foreground", variant === "classic" && "font-serif")}>
78
- {recipientName}
79
- </p>
80
- <p className="text-sm text-foreground mb-1">has successfully completed</p>
81
- <p className="text-xl font-bold mb-2 text-primary">{courseTitle}</p>
82
-
83
- <p className="text-sm text-foreground mb-3">
84
- Issued by {organizationName} on {formattedDate}
85
- </p>
86
-
87
- {signatory && (
88
- <div className="mt-4 mb-2">
89
- <Separator className="my-2 mx-auto max-w-50" />
90
- <p className="font-semibold text-sm text-foreground">{signatory.name}</p>
91
- <p className="text-xs text-muted-foreground">{signatory.title}</p>
92
- </div>
93
- )}
357
+ {/* Outer certificate frame */}
358
+ <div className={outerFrameVariants({ variant })}>
359
+ {/* Inner certificate frame */}
360
+ <div className={innerFrameVariants({ variant })}>
361
+ {/* Corner ornaments */}
362
+ <CornerOrnaments variant={variant} />
363
+
364
+ {/* Background radial glow */}
365
+ {glowStyle && (
366
+ <div
367
+ className="absolute inset-0 pointer-events-none rounded-[inherit]"
368
+ style={glowStyle}
369
+ aria-hidden="true"
370
+ />
371
+ )}
372
+
373
+ {/* Content */}
374
+ <div className="relative">
375
+ {/* Logo / Icon */}
376
+ {organizationLogo ? (
377
+ <img
378
+ src={organizationLogo}
379
+ alt={organizationName}
380
+ className="h-20 mb-4 mx-auto block"
381
+ />
382
+ ) : (
383
+ <Award
384
+ size={64}
385
+ className={cn("mx-auto mb-6", ICON_COLORS[variant])}
386
+ />
387
+ )}
388
+
389
+ {/* Certificate heading */}
390
+ <p className={cn("mb-2", HEADING_STYLES[variant])}>
391
+ Certificate of Completion
392
+ </p>
393
+
394
+ {/* Top ornamental divider */}
395
+ <OrnamentalDivider variant={variant} />
396
+
397
+ {/* Certify text */}
398
+ <p className="text-base text-foreground/70 mb-2">
399
+ This is to certify that
400
+ </p>
94
401
 
95
- {certificateId && (
96
- <span className="block text-xs text-muted-foreground mt-2">
97
- Certificate ID: {certificateId}
98
- </span>
99
- )}
402
+ {/* Recipient name */}
403
+ <p
404
+ className={cn(
405
+ "text-4xl font-bold mb-4 text-foreground",
406
+ SERIF_VARIANTS.has(variant) && "font-serif",
407
+ )}
408
+ >
409
+ {recipientName}
410
+ </p>
411
+
412
+ {/* Completion text */}
413
+ <p className="text-base text-foreground/70 mb-2">
414
+ has successfully completed
415
+ </p>
416
+
417
+ {/* Course title */}
418
+ <p
419
+ className={cn(
420
+ "text-2xl font-semibold mb-4",
421
+ COURSE_TITLE_COLORS[variant],
422
+ )}
423
+ >
424
+ {courseTitle}
425
+ </p>
426
+
427
+ {/* Issuance line */}
428
+ <p className="text-base text-foreground/60 mb-6">
429
+ Issued by {organizationName} on {formattedDate}
430
+ </p>
431
+
432
+ {/* Signatory */}
433
+ {signatory && (
434
+ <div className="mt-6 mb-4">
435
+ <OrnamentalDivider variant={variant} />
436
+ <p className="font-semibold text-base text-foreground">
437
+ {signatory.name}
438
+ </p>
439
+ <p className="text-sm text-muted-foreground">
440
+ {signatory.title}
441
+ </p>
442
+ </div>
443
+ )}
444
+
445
+ {/* Certificate ID */}
446
+ {certificateId && (
447
+ <span className="block text-xs text-muted-foreground mt-4">
448
+ Certificate ID: {certificateId}
449
+ </span>
450
+ )}
451
+ </div>
452
+ </div>
100
453
  </div>
101
454
 
102
455
  {/* Actions */}
103
456
  {showActions && (
104
- <div className="flex justify-center gap-2 mt-3">
457
+ <div className="flex justify-center gap-3 mt-6">
105
458
  <Button variant="outline" onClick={handlePrint}>
106
459
  <Printer size={16} /> Print
107
460
  </Button>
@@ -1,10 +1,18 @@
1
+ /** Available certificate visual styles */
2
+ export type CertificateVariant =
3
+ | "classic"
4
+ | "modern"
5
+ | "elegant"
6
+ | "academic"
7
+ | "minimal"
8
+ | "bold";
1
9
 
2
10
  /**
3
11
  * CertificateViewer section — a printable completion certificate.
4
12
  *
5
13
  * Displays a certificate with recipient details, course information,
6
- * signatory, and verification ID. Supports three visual variants:
7
- * classic, modern, and minimal.
14
+ * signatory, and verification ID. Supports six visual variants:
15
+ * classic, modern, elegant, academic, minimal, and bold.
8
16
  *
9
17
  * @example
10
18
  * <CertificateViewer
@@ -12,7 +20,7 @@
12
20
  * courseTitle="Advanced React"
13
21
  * completionDate="2025-03-01"
14
22
  * organizationName="HydraLMS Academy"
15
- * variant="modern"
23
+ * variant="elegant"
16
24
  * />
17
25
  */
18
26
  export interface CertificateViewerProps {
@@ -30,8 +38,8 @@ export interface CertificateViewerProps {
30
38
  signatory?: { name: string; title: string };
31
39
  /** Unique certificate ID */
32
40
  certificateId?: string;
33
- /** Certificate template variant */
34
- variant?: "classic" | "modern" | "minimal";
41
+ /** Certificate template variant @default "classic" */
42
+ variant?: CertificateVariant;
35
43
  /** Whether to show print/download actions */
36
44
  showActions?: boolean;
37
45
  /** Called when print is triggered */
@@ -1,22 +1,11 @@
1
1
  import { useMemo } from "react";
2
2
  import { CurriculumTree } from "../../curriculum";
3
- import type { CurriculumItem } from "../../curriculum/types";
4
3
  import { Progress } from "../../ui/progress";
4
+ import { Separator } from "../../ui/separator";
5
+ import { flattenLeaves } from "../../utils/flatten-leaves";
5
6
  import type { CourseOutlineProps } from "./types";
6
7
  import { cn } from "../../lib/utils";
7
8
 
8
- function flattenLeaves(items: CurriculumItem[]): string[] {
9
- const leaves: string[] = [];
10
- for (const item of items) {
11
- if (!item.children || item.children.length === 0) {
12
- leaves.push(item.uid);
13
- } else {
14
- leaves.push(...flattenLeaves(item.children));
15
- }
16
- }
17
- return leaves;
18
- }
19
-
20
9
  export function CourseOutline({
21
10
  items,
22
11
  progress,
@@ -48,7 +37,7 @@ export function CourseOutline({
48
37
  return (
49
38
  <div className={cn(className)} style={style}>
50
39
  {(courseTitle || showOverallProgress) && (
51
- <div className="px-2 pt-2 pb-1">
40
+ <div className="px-2 pt-2 pb-2">
52
41
  {courseTitle && (
53
42
  <p className={cn("font-semibold text-sm text-foreground", showOverallProgress && "mb-1")}>
54
43
  {courseTitle}
@@ -64,6 +53,7 @@ export function CourseOutline({
64
53
  )}
65
54
  </div>
66
55
  )}
56
+ {(courseTitle || showOverallProgress) && <Separator />}
67
57
  <CurriculumTree
68
58
  items={items}
69
59
  progress={progress}
@@ -2,7 +2,9 @@ import { useMemo, useState } from "react";
2
2
  import { CheckCircle, Heart, MessageSquare, Reply } from "lucide-react";
3
3
  import { PostCard } from "../../social";
4
4
  import { Button } from "../../ui/button";
5
- import { Textarea } from "../../ui/textarea";
5
+ import { RichTextEditor } from "../../ui/rich-text-editor";
6
+ import { isEmptyHtml } from "../../utils/is-empty-html";
7
+ import { Badge } from "../../ui/badge";
6
8
  import { Separator } from "../../ui/separator";
7
9
  import { Card, CardContent } from "../../ui/card";
8
10
  import { Tooltip, TooltipTrigger, TooltipContent } from "../../ui/tooltip";
@@ -48,8 +50,8 @@ export function DiscussionThread({
48
50
  }, [replies, rootPost.uid, sortOrder]);
49
51
 
50
52
  function handleSubmitReply(parentUid: string) {
51
- if (!replyContent.trim()) return;
52
- onReply(parentUid, replyContent.trim());
53
+ if (isEmptyHtml(replyContent)) return;
54
+ onReply(parentUid, replyContent);
53
55
  setReplyContent("");
54
56
  setReplyingToUid(null);
55
57
  }
@@ -132,18 +134,19 @@ export function DiscussionThread({
132
134
  className="mb-2"
133
135
  style={{ marginLeft: `${(effectiveDepth + 1) * 16}px` }}
134
136
  >
135
- <CardContent className="pt-4 pb-4">
136
- <Textarea
137
+ <CardContent className="py-4">
138
+ <RichTextEditor
137
139
  className="min-h-15 mb-2"
138
140
  placeholder="Write a reply..."
139
141
  value={replyContent}
140
- onChange={(e) => setReplyContent(e.target.value)}
142
+ onChange={(html) => setReplyContent(html)}
143
+ variant="minimal"
141
144
  />
142
145
  <div className="flex items-center gap-2">
143
146
  <Button
144
147
  size="sm"
145
148
  onClick={() => handleSubmitReply(post.uid)}
146
- disabled={!replyContent.trim()}
149
+ disabled={isEmptyHtml(replyContent)}
147
150
  >
148
151
  Post Reply
149
152
  </Button>
@@ -170,12 +173,12 @@ export function DiscussionThread({
170
173
 
171
174
  return (
172
175
  <div className={className} style={style}>
173
- <div className="flex items-center gap-1 mb-2">
176
+ <div className="flex items-center gap-2 mb-2">
174
177
  <MessageSquare size={20} className="text-foreground shrink-0" />
175
178
  <span className="text-lg font-semibold text-foreground">{title}</span>
176
- <span className="text-sm text-muted-foreground">
179
+ <Badge variant="muted" className="text-xs">
177
180
  {replies.length} {replies.length === 1 ? "reply" : "replies"}
178
- </span>
181
+ </Badge>
179
182
  </div>
180
183
 
181
184
  <Separator className="mb-2" />