@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
@@ -0,0 +1,285 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import {
3
+ ShieldCheck,
4
+ RotateCcw,
5
+ Clock,
6
+ HelpCircle,
7
+ CheckCircle2,
8
+ XCircle,
9
+ Trophy,
10
+ Play,
11
+ } from "lucide-react";
12
+ import { ExamSession } from "../../sections/ExamSession/ExamSession";
13
+ import { AssessmentReview } from "../../sections/AssessmentReview/AssessmentReview";
14
+ import { ProgressRing } from "../../progress/progress-ring";
15
+ import { StatCard } from "../../progress/stat-card";
16
+ import { Button } from "../../ui/button";
17
+ import { Badge } from "../../ui/badge";
18
+ import { Card, CardContent } from "../../ui/card";
19
+ import { Alert, AlertDescription } from "../../ui/alert";
20
+ import { Separator } from "../../ui/separator";
21
+ import { formatDuration } from "../../utils/format-duration";
22
+ import { cn } from "../../lib/utils";
23
+ import type { SessionAnswer } from "../../questions/types";
24
+ import type { ExamSubmitMetadata } from "../../sections/ExamSession/types";
25
+ import { scoreAssessment } from "../../questions/scoring";
26
+ import type { ExamModuleProps, ExamModuleResult } from "./types";
27
+
28
+ type InternalStep =
29
+ | { tag: "intro" }
30
+ | { tag: "exam" }
31
+ | { tag: "results"; result: ExamModuleResult };
32
+
33
+ /**
34
+ * ExamModule — a complete formal exam experience.
35
+ *
36
+ * Steps: Intro (rules/instructions) → Exam (timed ExamSession) → Results (score + review).
37
+ * Manages an external timer that feeds elapsed time to ExamSession.
38
+ */
39
+ export function ExamModule({
40
+ title,
41
+ description,
42
+ instructions,
43
+ questions,
44
+ timeLimitSeconds,
45
+ passingScore,
46
+ allowBackNavigation = true,
47
+ autoSubmitOnTimeout = true,
48
+ timeWarningThreshold,
49
+ allowRetake = false,
50
+ showReview = true,
51
+ onComplete,
52
+ className,
53
+ style,
54
+ }: ExamModuleProps) {
55
+ const [step, setStep] = useState<InternalStep>({ tag: "intro" });
56
+ const [timeElapsed, setTimeElapsed] = useState(0);
57
+ const startTimeRef = useRef<number | null>(null);
58
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
59
+ const contentRef = useRef<HTMLDivElement>(null);
60
+
61
+ useEffect(() => {
62
+ contentRef.current?.focus({ preventScroll: true });
63
+ }, [step.tag]);
64
+
65
+ // Timer for exam step
66
+ useEffect(() => {
67
+ if (step.tag === "exam") {
68
+ startTimeRef.current = Date.now();
69
+ intervalRef.current = setInterval(() => {
70
+ if (startTimeRef.current) {
71
+ setTimeElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000));
72
+ }
73
+ }, 1000);
74
+ } else {
75
+ if (intervalRef.current) {
76
+ clearInterval(intervalRef.current);
77
+ intervalRef.current = null;
78
+ }
79
+ }
80
+ return () => {
81
+ if (intervalRef.current) clearInterval(intervalRef.current);
82
+ };
83
+ }, [step.tag]);
84
+
85
+ function scoreAnswers(
86
+ answers: SessionAnswer[],
87
+ metadata: ExamSubmitMetadata
88
+ ): ExamModuleResult {
89
+ const { correct, total, percentage } = scoreAssessment(questions, answers);
90
+ return {
91
+ answers,
92
+ correct,
93
+ total,
94
+ percentage,
95
+ passed: passingScore !== undefined ? percentage >= passingScore : true,
96
+ timeElapsedSeconds: metadata.timeElapsedSeconds,
97
+ wasAutoSubmitted: metadata.wasAutoSubmitted,
98
+ };
99
+ }
100
+
101
+ function handleSubmit(answers: SessionAnswer[], metadata: ExamSubmitMetadata) {
102
+ const result = scoreAnswers(answers, metadata);
103
+ setStep({ tag: "results", result });
104
+ onComplete?.(result);
105
+ }
106
+
107
+ function handleRetake() {
108
+ setTimeElapsed(0);
109
+ startTimeRef.current = null;
110
+ setStep({ tag: "intro" });
111
+ }
112
+
113
+ // ─── Intro Screen ───
114
+ if (step.tag === "intro") {
115
+ return (
116
+ <div
117
+ ref={contentRef}
118
+ tabIndex={-1}
119
+ className={cn("max-w-2xl mx-auto outline-none", className)}
120
+ style={style}
121
+ >
122
+ <Card>
123
+ <CardContent className="pt-8 pb-8 text-center">
124
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
125
+ <ShieldCheck className="size-7 text-primary" />
126
+ </div>
127
+ <h2 className="text-2xl font-bold text-foreground mb-2">{title}</h2>
128
+ {description && (
129
+ <p className="text-muted-foreground mb-6 max-w-md mx-auto">
130
+ {description}
131
+ </p>
132
+ )}
133
+
134
+ {/* Instructions */}
135
+ {instructions && (
136
+ <Alert className="text-left mb-6">
137
+ <AlertDescription>{instructions}</AlertDescription>
138
+ </Alert>
139
+ )}
140
+
141
+ {/* Metadata chips */}
142
+ <div className="flex flex-wrap justify-center gap-2 mb-8">
143
+ <Badge variant="outline" className="gap-1.5">
144
+ <HelpCircle className="size-3.5" />
145
+ {questions.length} questions
146
+ </Badge>
147
+ <Badge variant="outline" className="gap-1.5">
148
+ <Clock className="size-3.5" />
149
+ {formatDuration(timeLimitSeconds)} time limit
150
+ </Badge>
151
+ {passingScore !== undefined && (
152
+ <Badge variant="outline" className="gap-1.5">
153
+ <CheckCircle2 className="size-3.5" />
154
+ {passingScore}% to pass
155
+ </Badge>
156
+ )}
157
+ </div>
158
+
159
+ <Button size="lg" onClick={() => setStep({ tag: "exam" })}>
160
+ <Play className="size-4 mr-2" />
161
+ Begin Exam
162
+ </Button>
163
+ </CardContent>
164
+ </Card>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ // ─── Exam Screen ───
170
+ if (step.tag === "exam") {
171
+ return (
172
+ <div
173
+ ref={contentRef}
174
+ tabIndex={-1}
175
+ className={cn("outline-none", className)}
176
+ style={style}
177
+ >
178
+ <ExamSession
179
+ questions={questions}
180
+ timeLimitSeconds={timeLimitSeconds}
181
+ timeElapsedSeconds={timeElapsed}
182
+ autoSubmitOnTimeout={autoSubmitOnTimeout}
183
+ timeWarningThreshold={timeWarningThreshold}
184
+ allowBackNavigation={allowBackNavigation}
185
+ confirmBeforeSubmit
186
+ examTitle={title}
187
+ onSubmit={handleSubmit}
188
+ />
189
+ </div>
190
+ );
191
+ }
192
+
193
+ // ─── Results Screen ───
194
+ const { result } = step;
195
+ const passed = result.passed;
196
+
197
+ return (
198
+ <div
199
+ ref={contentRef}
200
+ tabIndex={-1}
201
+ className={cn("max-w-2xl mx-auto outline-none", className)}
202
+ style={style}
203
+ >
204
+ <Card>
205
+ <CardContent className="pt-8 pb-8">
206
+ {/* Score summary */}
207
+ <div className="text-center mb-8">
208
+ <ProgressRing
209
+ value={result.percentage}
210
+ size={140}
211
+ strokeWidth={10}
212
+ color={passed ? "var(--success)" : "var(--destructive)"}
213
+ className="mx-auto mb-4 text-foreground"
214
+ />
215
+ <Badge
216
+ variant={passed ? "success" : "destructive"}
217
+ className="text-sm px-3 py-1 mb-2"
218
+ >
219
+ {passed ? "Passed" : "Failed"}
220
+ </Badge>
221
+ {result.wasAutoSubmitted && (
222
+ <Badge variant="outline" className="text-sm px-3 py-1 mb-2 ml-2">
223
+ Auto-submitted
224
+ </Badge>
225
+ )}
226
+ <h2 className="text-xl font-bold text-foreground">{title}</h2>
227
+ </div>
228
+
229
+ {/* Stats grid */}
230
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
231
+ <StatCard
232
+ icon={<CheckCircle2 />}
233
+ label="Correct"
234
+ description="Questions answered right"
235
+ value={`${result.correct}/${result.total}`}
236
+ />
237
+ <StatCard
238
+ icon={<XCircle />}
239
+ label="Incorrect"
240
+ description="Questions to review"
241
+ value={`${result.total - result.correct}/${result.total}`}
242
+ />
243
+ <StatCard
244
+ icon={<Trophy />}
245
+ label="Score"
246
+ description="Overall percentage"
247
+ value={`${result.percentage}%`}
248
+ />
249
+ <StatCard
250
+ icon={<Clock />}
251
+ label="Time"
252
+ description="Total elapsed"
253
+ value={formatDuration(result.timeElapsedSeconds)}
254
+ />
255
+ </div>
256
+
257
+ {/* Actions */}
258
+ {allowRetake && (
259
+ <div className="flex justify-center mb-8">
260
+ <Button variant="outline" onClick={handleRetake}>
261
+ <RotateCcw className="size-4 mr-2" />
262
+ Retake Exam
263
+ </Button>
264
+ </div>
265
+ )}
266
+ </CardContent>
267
+ </Card>
268
+
269
+ {/* Per-question review */}
270
+ {showReview && (
271
+ <>
272
+ <Separator className="my-6" />
273
+ <h3 className="text-lg font-semibold text-foreground mb-4">
274
+ Question Review
275
+ </h3>
276
+ <AssessmentReview
277
+ questions={questions}
278
+ sessionAnswers={result.answers}
279
+ showCorrectAnswers
280
+ />
281
+ </>
282
+ )}
283
+ </div>
284
+ );
285
+ }
@@ -0,0 +1,66 @@
1
+ import type { ReactNode } from "react";
2
+ import type { QuestionData, SessionAnswer } from "../../questions/types";
3
+
4
+ /**
5
+ * ExamModule — a formal timed exam experience with intro, exam, and results steps.
6
+ *
7
+ * Wraps ExamSession with an intro screen showing rules/instructions and a
8
+ * results screen with scoring, stats, and optional answer review.
9
+ *
10
+ * @example
11
+ * <ExamModule
12
+ * title="Midterm Exam"
13
+ * instructions={<p>You have 60 minutes. No notes allowed.</p>}
14
+ * questions={questions}
15
+ * timeLimitSeconds={3600}
16
+ * passingScore={70}
17
+ * onComplete={(result) => saveResult(result)}
18
+ * />
19
+ */
20
+ export interface ExamModuleProps {
21
+ /** Exam title displayed on the intro screen */
22
+ title: string;
23
+ /** Exam description displayed on the intro screen */
24
+ description?: string;
25
+ /** Exam rules/instructions rendered on the intro screen */
26
+ instructions?: ReactNode;
27
+ /** Ordered list of questions */
28
+ questions: QuestionData[];
29
+ /** Time limit in seconds (required for exams) */
30
+ timeLimitSeconds: number;
31
+ /** Passing threshold as a percentage (e.g. 70 means 70%) */
32
+ passingScore?: number;
33
+ /** Whether the user can go back to previous questions. @default true */
34
+ allowBackNavigation?: boolean;
35
+ /** Auto-submit when time runs out. @default true */
36
+ autoSubmitOnTimeout?: boolean;
37
+ /** Seconds remaining at which to show a time warning */
38
+ timeWarningThreshold?: number;
39
+ /** Whether to allow retaking the exam from the results screen. @default false */
40
+ allowRetake?: boolean;
41
+ /** Whether to show correct/incorrect answer highlighting in the review. @default true */
42
+ showReview?: boolean;
43
+ /** Called when the exam is completed (submitted) */
44
+ onComplete?: (result: ExamModuleResult) => void;
45
+ /** CSS class name for the root element */
46
+ className?: string;
47
+ /** Inline styles for the root element */
48
+ style?: React.CSSProperties;
49
+ }
50
+
51
+ export interface ExamModuleResult {
52
+ /** The user's submitted answers */
53
+ answers: SessionAnswer[];
54
+ /** Number of correct answers */
55
+ correct: number;
56
+ /** Total number of gradable questions */
57
+ total: number;
58
+ /** Score as a percentage (0-100) */
59
+ percentage: number;
60
+ /** Whether the user passed (only meaningful when passingScore is set) */
61
+ passed: boolean;
62
+ /** Total time taken in seconds */
63
+ timeElapsedSeconds: number;
64
+ /** Whether the submission was triggered by timeout */
65
+ wasAutoSubmitted: boolean;
66
+ }
@@ -1,4 +1,4 @@
1
- import { useState, useRef } from "react";
1
+ import { useState, useRef, useEffect } from "react";
2
2
  import {
3
3
  BookOpen,
4
4
  Shuffle,
@@ -40,6 +40,11 @@ export function FlashcardLab({
40
40
  const [selectedUids, setSelectedUids] = useState<Set<string>>(new Set());
41
41
  const [shuffled, setShuffled] = useState(defaultShuffled);
42
42
  const startTimeRef = useRef<number | null>(null);
43
+ const contentRef = useRef<HTMLDivElement>(null);
44
+
45
+ useEffect(() => {
46
+ contentRef.current?.focus({ preventScroll: true });
47
+ }, [step.tag]);
43
48
 
44
49
  function toggleDeck(uid: string) {
45
50
  setSelectedUids((prev) => {
@@ -105,12 +110,14 @@ export function FlashcardLab({
105
110
  // ─── Setup Screen ───
106
111
  if (step.tag === "setup") {
107
112
  return (
108
- <div className={cn(className)} style={style}>
113
+ <div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
114
+ <Card>
115
+ <CardContent className="pt-8 pb-8">
109
116
  <div className="text-center mb-6">
110
- <div className="mx-auto mb-3 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
117
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
111
118
  <BookOpen className="size-7 text-primary" />
112
119
  </div>
113
- <h2 className="text-2xl font-bold text-foreground mb-1">
120
+ <h2 className="text-2xl font-bold text-foreground mb-2">
114
121
  Choose Your Decks
115
122
  </h2>
116
123
  <p className="text-muted-foreground text-sm">
@@ -159,19 +166,15 @@ export function FlashcardLab({
159
166
  <div className="flex items-center justify-between">
160
167
  <div className="flex items-center gap-3">
161
168
  {showShuffleToggle && (
162
- <button
163
- type="button"
164
- className={cn(
165
- "flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-md border transition-colors",
166
- shuffled
167
- ? "border-primary bg-primary/10 text-primary"
168
- : "border-border text-muted-foreground hover:text-foreground",
169
- )}
169
+ <Button
170
+ variant={shuffled ? "secondary" : "outline"}
171
+ size="sm"
172
+ className="gap-1.5"
170
173
  onClick={() => setShuffled((s) => !s)}
171
174
  >
172
175
  <Shuffle className="size-3.5" />
173
176
  Shuffle
174
- </button>
177
+ </Button>
175
178
  )}
176
179
  {selectedUids.size > 0 && (
177
180
  <span className="text-xs text-muted-foreground">
@@ -189,6 +192,8 @@ export function FlashcardLab({
189
192
  Start Studying
190
193
  </Button>
191
194
  </div>
195
+ </CardContent>
196
+ </Card>
192
197
  </div>
193
198
  );
194
199
  }
@@ -201,7 +206,7 @@ export function FlashcardLab({
201
206
  .join(", ");
202
207
 
203
208
  return (
204
- <div className={cn(className)} style={style}>
209
+ <div ref={contentRef} tabIndex={-1} className={cn("outline-none", className)} style={style}>
205
210
  <FlashcardStudySession
206
211
  cards={step.cards}
207
212
  title={deckNames}
@@ -216,7 +221,9 @@ export function FlashcardLab({
216
221
  const { result } = step;
217
222
 
218
223
  return (
219
- <div className={cn(className)} style={style}>
224
+ <div ref={contentRef} tabIndex={-1} className={cn("max-w-2xl mx-auto outline-none", className)} style={style}>
225
+ <Card>
226
+ <CardContent className="pt-8 pb-8">
220
227
  <div className="text-center mb-8">
221
228
  <div className="relative mx-auto mb-4">
222
229
  <ProgressRing
@@ -229,7 +236,7 @@ export function FlashcardLab({
229
236
  />
230
237
  <CheckCircle2 className="size-8 text-success absolute inset-0 m-auto" />
231
238
  </div>
232
- <h2 className="text-xl font-bold text-foreground mb-1">
239
+ <h2 className="text-xl font-bold text-foreground mb-2">
233
240
  Study Session Complete
234
241
  </h2>
235
242
  <p className="text-muted-foreground text-sm">
@@ -241,21 +248,25 @@ export function FlashcardLab({
241
248
  <StatCard
242
249
  icon={<Layers />}
243
250
  label="Cards Studied"
251
+ description="Total cards reviewed"
244
252
  value={String(result.totalCards)}
245
253
  />
246
254
  <StatCard
247
255
  icon={<BookOpen />}
248
256
  label="Decks"
257
+ description="Decks completed"
249
258
  value={String(result.decksStudied)}
250
259
  />
251
260
  <StatCard
252
261
  icon={<Clock />}
253
262
  label="Time Spent"
263
+ description="Session duration"
254
264
  value={formatDuration(result.timeElapsedSeconds)}
255
265
  />
256
266
  <StatCard
257
267
  icon={<Shuffle />}
258
268
  label="Shuffled"
269
+ description="Card order randomized"
259
270
  value={result.wasShuffled ? "Yes" : "No"}
260
271
  />
261
272
  </div>
@@ -270,6 +281,8 @@ export function FlashcardLab({
270
281
  Study Again
271
282
  </Button>
272
283
  </div>
284
+ </CardContent>
285
+ </Card>
273
286
  </div>
274
287
  );
275
288
  }
@@ -0,0 +1,169 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { ArrowLeft, BookOpen, BarChart3 } from "lucide-react";
3
+ import { GradebookTable } from "../../sections/GradebookTable/GradebookTable";
4
+ import { ProgressDashboard } from "../../sections/ProgressDashboard/ProgressDashboard";
5
+ import { AssessmentReview } from "../../sections/AssessmentReview/AssessmentReview";
6
+ import { GradeIndicator } from "../../progress/grade-indicator";
7
+ import { Button } from "../../ui/button";
8
+ import { Card, CardContent } from "../../ui/card";
9
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../ui/tabs";
10
+ import { Separator } from "../../ui/separator";
11
+ import { cn } from "../../lib/utils";
12
+ import type { GradeItem } from "../../sections/GradebookTable/types";
13
+ import type { GradeCenterModuleProps } from "./types";
14
+
15
+ /**
16
+ * GradeCenterModule — a tabbed grade center with gradebook, progress dashboard,
17
+ * and drill-down into individual assessment reviews.
18
+ *
19
+ * Uses a panel-based layout (like CoursePlayer) with tabs for Grades and Progress,
20
+ * and a slide-in detail panel for reviewing individual assessments.
21
+ */
22
+ export function GradeCenterModule({
23
+ courseTitle,
24
+ gradeItems,
25
+ categories,
26
+ overallGrade,
27
+ showWeights,
28
+ progressData,
29
+ reviewData,
30
+ className,
31
+ style,
32
+ }: GradeCenterModuleProps) {
33
+ const [drillDownUid, setDrillDownUid] = useState<string | null>(null);
34
+ const contentRef = useRef<HTMLDivElement>(null);
35
+
36
+ useEffect(() => {
37
+ contentRef.current?.focus({ preventScroll: true });
38
+ }, [drillDownUid]);
39
+
40
+ function handleItemClick(item: GradeItem) {
41
+ if (reviewData?.[item.uid]) {
42
+ setDrillDownUid(item.uid);
43
+ }
44
+ }
45
+
46
+ function handleBack() {
47
+ setDrillDownUid(null);
48
+ }
49
+
50
+ const drillDownItem = drillDownUid
51
+ ? gradeItems.find((i) => i.uid === drillDownUid)
52
+ : null;
53
+ const drillDownData = drillDownUid ? reviewData?.[drillDownUid] : null;
54
+
55
+ return (
56
+ <div
57
+ ref={contentRef}
58
+ tabIndex={-1}
59
+ className={cn("outline-none", className)}
60
+ style={style}
61
+ >
62
+ {/* Header */}
63
+ <div className="flex items-center justify-between mb-6">
64
+ <div>
65
+ {courseTitle && (
66
+ <h2 className="text-xl font-bold text-foreground">{courseTitle}</h2>
67
+ )}
68
+ <p className="text-sm text-muted-foreground">Grade Center</p>
69
+ </div>
70
+ {overallGrade && (
71
+ <div className="flex items-center gap-3">
72
+ <GradeIndicator
73
+ percentage={overallGrade.percentage}
74
+ letterGrade={overallGrade.letterGrade}
75
+ size="medium"
76
+ />
77
+ <div className="text-right">
78
+ <div className="text-sm font-medium text-foreground">
79
+ {overallGrade.pointsEarned}/{overallGrade.pointsPossible} pts
80
+ </div>
81
+ {overallGrade.letterGrade && (
82
+ <div className="text-xs text-muted-foreground">
83
+ {overallGrade.letterGrade}
84
+ </div>
85
+ )}
86
+ </div>
87
+ </div>
88
+ )}
89
+ </div>
90
+
91
+ <Separator className="mb-6" />
92
+
93
+ {/* Drill-down view */}
94
+ {drillDownItem && drillDownData ? (
95
+ <div>
96
+ <Button
97
+ variant="ghost"
98
+ size="sm"
99
+ onClick={handleBack}
100
+ className="mb-4"
101
+ >
102
+ <ArrowLeft className="size-4 mr-1.5" />
103
+ Back to Grades
104
+ </Button>
105
+ <Card className="mb-4">
106
+ <CardContent className="py-3 px-4">
107
+ <h3 className="font-semibold text-foreground">
108
+ {drillDownItem.name}
109
+ </h3>
110
+ {drillDownItem.score !== null && (
111
+ <p className="text-sm text-muted-foreground">
112
+ Score: {drillDownItem.score}/{drillDownItem.maxScore} (
113
+ {Math.round(
114
+ (drillDownItem.score / drillDownItem.maxScore) * 100
115
+ )}
116
+ %)
117
+ </p>
118
+ )}
119
+ </CardContent>
120
+ </Card>
121
+ <AssessmentReview
122
+ questions={drillDownData.questions}
123
+ sessionAnswers={drillDownData.sessionAnswers}
124
+ score={drillDownData.score}
125
+ showCorrectAnswers
126
+ />
127
+ </div>
128
+ ) : (
129
+ <Tabs defaultValue="grades">
130
+ {progressData && (
131
+ <TabsList className="mb-6">
132
+ <TabsTrigger value="grades">
133
+ <BookOpen className="size-4 mr-1.5" />
134
+ Grades
135
+ </TabsTrigger>
136
+ <TabsTrigger value="progress">
137
+ <BarChart3 className="size-4 mr-1.5" />
138
+ Progress
139
+ </TabsTrigger>
140
+ </TabsList>
141
+ )}
142
+
143
+ <TabsContent value="grades">
144
+ <GradebookTable
145
+ items={gradeItems}
146
+ categories={categories}
147
+ overallGrade={overallGrade}
148
+ showWeights={showWeights}
149
+ onItemClick={handleItemClick}
150
+ />
151
+ </TabsContent>
152
+
153
+ {progressData && (
154
+ <TabsContent value="progress">
155
+ <ProgressDashboard
156
+ overallProgress={progressData.overallProgress}
157
+ totalTimeSpent={progressData.totalTimeSpent}
158
+ modules={progressData.modules}
159
+ recentActivity={progressData.recentActivity}
160
+ streak={progressData.streak}
161
+ achievements={progressData.achievements}
162
+ />
163
+ </TabsContent>
164
+ )}
165
+ </Tabs>
166
+ )}
167
+ </div>
168
+ );
169
+ }