@hydralms/components 0.1.3 → 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 +92 -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,7 +1,10 @@
1
1
  export { ContentBlock } from "./content-block";
2
2
  export { FileUploadZone } from "./file-upload-zone";
3
+ export { AttachmentList } from "./attachment-list";
3
4
  export type {
4
5
  LessonBlock,
5
6
  ContentBlockProps,
6
7
  FileUploadZoneProps,
8
+ AttachmentListProps,
9
+ AttachmentFile,
7
10
  } from "./types";
@@ -74,3 +74,44 @@ export interface FileUploadZoneProps {
74
74
  /** Inline styles for the root element */
75
75
  style?: React.CSSProperties;
76
76
  }
77
+
78
+ /**
79
+ * A single file attachment for display in AttachmentList.
80
+ */
81
+ export interface AttachmentFile {
82
+ /** File name with extension */
83
+ name: string;
84
+ /** File size in bytes */
85
+ size: number;
86
+ /** MIME type (e.g. "application/pdf", "image/png") */
87
+ type: string;
88
+ /** Optional download URL */
89
+ url?: string;
90
+ }
91
+
92
+ /**
93
+ * AttachmentList displays a read-only list of file attachments
94
+ * with type icons, formatted sizes, and optional download/remove actions.
95
+ *
96
+ * @example
97
+ * <AttachmentList
98
+ * files={[
99
+ * { name: "syllabus.pdf", size: 245000, type: "application/pdf", url: "/files/syllabus.pdf" },
100
+ * ]}
101
+ * onDownload={(file) => window.open(file.url)}
102
+ * />
103
+ */
104
+ export interface AttachmentListProps {
105
+ /** List of file attachments to display */
106
+ files: AttachmentFile[];
107
+ /** Called when the user clicks a file's download action */
108
+ onDownload?: (file: AttachmentFile) => void;
109
+ /** Called when the user clicks a file's remove button (only shown when not readOnly) */
110
+ onRemove?: (file: AttachmentFile) => void;
111
+ /** When true, hides the remove button. @default true */
112
+ readOnly?: boolean;
113
+ /** CSS class name for the root element */
114
+ className?: string;
115
+ /** Inline styles for the root element */
116
+ style?: React.CSSProperties;
117
+ }
@@ -1,10 +1,11 @@
1
+ import { memo } from "react";
1
2
  import { ChevronRight, ChevronDown, CheckCircle2, Circle } from "lucide-react";
2
3
  import { LearningObjectIcon } from "./learning-object-icon";
3
4
  import { formatDuration } from "../utils/format-duration";
4
5
  import type { CurriculumItemRowProps } from "./types";
5
6
  import { cn } from "../lib/utils";
6
7
 
7
- export const CurriculumItemRow = ({
8
+ export const CurriculumItemRow = memo(function CurriculumItemRow({
8
9
  item,
9
10
  level,
10
11
  isActive = false,
@@ -16,7 +17,7 @@ export const CurriculumItemRow = ({
16
17
  showDuration = true,
17
18
  showIcon = true,
18
19
  showProgress = true,
19
- }: CurriculumItemRowProps) => {
20
+ }: CurriculumItemRowProps) {
20
21
  const isModule = hasChildren;
21
22
 
22
23
  const fontWeightClass = isModule ? "font-semibold" : isActive ? "font-medium" : "font-normal";
@@ -30,6 +31,9 @@ export const CurriculumItemRow = ({
30
31
  )}
31
32
  style={{ paddingLeft: `${(level * 20) + 8}px` }}
32
33
  onClick={onClick}
34
+ role={onClick ? "button" : undefined}
35
+ tabIndex={onClick ? 0 : undefined}
36
+ onKeyDown={onClick ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } } : undefined}
33
37
  >
34
38
  <div className="shrink-0 flex items-center justify-center size-5">
35
39
  {showProgress && isCompleted ? (
@@ -78,4 +82,4 @@ export const CurriculumItemRow = ({
78
82
  )}
79
83
  </div>
80
84
  );
81
- };
85
+ });
@@ -1,4 +1,6 @@
1
1
  import { CheckCircle, XCircle } from "lucide-react";
2
+ import { Alert } from "../ui/alert";
3
+ import { Button } from "../ui/button";
2
4
  import type { FeedbackBannerProps } from "./types";
3
5
  import { cn } from "../lib/utils";
4
6
 
@@ -11,18 +13,12 @@ export function FeedbackBanner({
11
13
  style,
12
14
  }: FeedbackBannerProps) {
13
15
  return (
14
- <div
15
- role="alert"
16
- className={cn(
17
- "flex items-start gap-2 rounded-md border p-3 text-sm",
18
- isCorrect
19
- ? "border-success/50 bg-success/5"
20
- : "border-destructive/50 bg-destructive/5",
21
- className,
22
- )}
16
+ <Alert
17
+ variant={isCorrect ? "success" : "destructive"}
18
+ className={cn(className)}
23
19
  style={style}
24
20
  >
25
- <span className={cn("shrink-0", isCorrect ? "text-success" : "text-destructive")}>
21
+ <span className="shrink-0">
26
22
  {isCorrect ? <CheckCircle size={20} /> : <XCircle size={20} />}
27
23
  </span>
28
24
  <div className="flex-1">
@@ -32,15 +28,17 @@ export function FeedbackBanner({
32
28
  {explanation && <p className="mt-1 text-foreground">{explanation}</p>}
33
29
  </div>
34
30
  {!isCorrect && onRetry && (
35
- <button
36
- className="shrink-0 rounded-md border border-border bg-background px-3 py-1 text-sm font-medium transition-colors hover:bg-muted"
31
+ <Button
32
+ variant="outline"
33
+ size="sm"
34
+ className="shrink-0"
37
35
  onClick={onRetry}
38
36
  >
39
37
  {retryLabel}
40
- </button>
38
+ </Button>
41
39
  )}
42
40
  </div>
43
41
  </div>
44
- </div>
42
+ </Alert>
45
43
  );
46
44
  }
@@ -3,15 +3,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
3
3
  import { Flashcard } from "./flashcard";
4
4
  import { Button } from "../ui/button";
5
5
  import type { FlashcardDeckProps } from "./types";
6
-
7
- function shuffle<T>(array: T[]): T[] {
8
- const shuffled = [...array];
9
- for (let i = shuffled.length - 1; i > 0; i--) {
10
- const j = Math.floor(Math.random() * (i + 1));
11
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
12
- }
13
- return shuffled;
14
- }
6
+ import { shuffle } from "../utils/shuffle";
15
7
 
16
8
  export const FlashcardDeck = ({
17
9
  cards,
@@ -50,7 +50,7 @@ export const Flashcard = ({
50
50
  >
51
51
  <div
52
52
  className={cn(
53
- "relative size-full transition-transform duration-500 transform-3d",
53
+ "relative size-full motion-safe:transition-transform motion-safe:duration-500 transform-3d",
54
54
  isFlipped && "transform-[rotateY(180deg)]",
55
55
  )}
56
56
  >
@@ -0,0 +1,305 @@
1
+ import { useState, useRef, useEffect, useMemo } from "react";
2
+ import {
3
+ FileEdit,
4
+ CheckCircle2,
5
+ Play,
6
+ ArrowLeft,
7
+ FileText,
8
+ Link as LinkIcon,
9
+ Paperclip,
10
+ } from "lucide-react";
11
+ import { AssignmentSubmission } from "../../sections/AssignmentSubmission/AssignmentSubmission";
12
+ import { RubricView } from "../../sections/RubricView/RubricView";
13
+ import { GradeIndicator } from "../../progress/grade-indicator";
14
+ import { StatusBadge } from "../../common/status-badge";
15
+ import { DueDateDisplay } from "../../common/due-date-display";
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 { cn } from "../../lib/utils";
22
+ import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
23
+ import type { AssignmentModuleProps } from "./types";
24
+
25
+ type InternalStep =
26
+ | { tag: "instructions" }
27
+ | { tag: "work" }
28
+ | { tag: "confirmation"; submission: SubmissionData };
29
+
30
+ const TYPE_LABELS: Record<string, string> = {
31
+ text: "Text Entry",
32
+ file: "File Upload",
33
+ url: "URL Submission",
34
+ };
35
+
36
+ /**
37
+ * AssignmentModule — a complete assignment experience with instructions,
38
+ * submission work area, and confirmation/grade review.
39
+ *
40
+ * Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
41
+ */
42
+ export function AssignmentModule({
43
+ title,
44
+ instructions,
45
+ dueDate,
46
+ maxScore,
47
+ submissionTypes,
48
+ fileConstraints,
49
+ rubric,
50
+ existingSubmission,
51
+ status = "not_started",
52
+ grade,
53
+ onSubmit,
54
+ onSaveDraft,
55
+ className,
56
+ style,
57
+ }: AssignmentModuleProps) {
58
+ const initialStep: InternalStep =
59
+ status === "submitted" || status === "graded"
60
+ ? {
61
+ tag: "confirmation",
62
+ submission: existingSubmission ?? {},
63
+ }
64
+ : { tag: "instructions" };
65
+
66
+ const [step, setStep] = useState<InternalStep>(initialStep);
67
+ const contentRef = useRef<HTMLDivElement>(null);
68
+
69
+ useEffect(() => {
70
+ contentRef.current?.focus({ preventScroll: true });
71
+ }, [step.tag]);
72
+
73
+ const rubricMaxScore = useMemo(() => {
74
+ if (!rubric) return 0;
75
+ return rubric.reduce(
76
+ (sum, c) => sum + Math.max(...c.levels.map((l) => l.points)),
77
+ 0
78
+ );
79
+ }, [rubric]);
80
+
81
+ function handleSubmit(submission: SubmissionData) {
82
+ onSubmit?.(submission);
83
+ setStep({ tag: "confirmation", submission });
84
+ }
85
+
86
+ function handleSaveDraft(submission: SubmissionData) {
87
+ onSaveDraft?.(submission);
88
+ }
89
+
90
+ const canEdit =
91
+ status === "not_started" ||
92
+ status === "draft" ||
93
+ status === "resubmit";
94
+
95
+ // ─── Instructions Screen ───
96
+ if (step.tag === "instructions") {
97
+ return (
98
+ <div
99
+ ref={contentRef}
100
+ tabIndex={-1}
101
+ className={cn("max-w-2xl mx-auto outline-none", className)}
102
+ style={style}
103
+ >
104
+ <Card>
105
+ <CardContent className="pt-8 pb-8">
106
+ <div className="text-center mb-6">
107
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
108
+ <FileEdit className="size-7 text-primary" />
109
+ </div>
110
+ <h2 className="text-2xl font-bold text-foreground mb-2">
111
+ {title}
112
+ </h2>
113
+ </div>
114
+
115
+ {/* Metadata chips */}
116
+ <div className="flex flex-wrap justify-center gap-2 mb-6">
117
+ {dueDate && (
118
+ <DueDateDisplay dueDate={dueDate} size="small" />
119
+ )}
120
+ {maxScore !== undefined && (
121
+ <Badge variant="outline" className="gap-1.5">
122
+ <CheckCircle2 className="size-3.5" />
123
+ {maxScore} points
124
+ </Badge>
125
+ )}
126
+ <Badge variant="outline" className="gap-1.5">
127
+ <Paperclip className="size-3.5" />
128
+ {submissionTypes.map((t) => TYPE_LABELS[t]).join(", ")}
129
+ </Badge>
130
+ <StatusBadge status={status} />
131
+ </div>
132
+
133
+ <Separator className="my-6" />
134
+
135
+ {/* Instructions content */}
136
+ <div className="prose prose-sm text-foreground mb-6">
137
+ {instructions}
138
+ </div>
139
+
140
+ {/* Rubric preview */}
141
+ {rubric && rubric.length > 0 && (
142
+ <>
143
+ <Separator className="my-6" />
144
+ <RubricView
145
+ criteria={rubric}
146
+ maxScore={rubricMaxScore}
147
+ />
148
+ </>
149
+ )}
150
+
151
+ {/* Action */}
152
+ {canEdit && (
153
+ <div className="text-center mt-8">
154
+ <Button
155
+ size="lg"
156
+ onClick={() => setStep({ tag: "work" })}
157
+ >
158
+ <Play className="size-4 mr-2" />
159
+ Start Assignment
160
+ </Button>
161
+ </div>
162
+ )}
163
+ </CardContent>
164
+ </Card>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ // ─── Work Screen ───
170
+ if (step.tag === "work") {
171
+ return (
172
+ <div
173
+ ref={contentRef}
174
+ tabIndex={-1}
175
+ className={cn("outline-none", className)}
176
+ style={style}
177
+ >
178
+ <Button
179
+ variant="ghost"
180
+ size="sm"
181
+ onClick={() => setStep({ tag: "instructions" })}
182
+ className="mb-4"
183
+ >
184
+ <ArrowLeft className="size-4 mr-1.5" />
185
+ Back to Instructions
186
+ </Button>
187
+ <AssignmentSubmission
188
+ title={title}
189
+ instructions={instructions}
190
+ dueDate={dueDate}
191
+ maxScore={maxScore}
192
+ status={status}
193
+ submissionTypes={submissionTypes}
194
+ existingSubmission={existingSubmission}
195
+ fileConstraints={fileConstraints}
196
+ grade={grade}
197
+ onSubmit={handleSubmit}
198
+ onSaveDraft={handleSaveDraft}
199
+ />
200
+ </div>
201
+ );
202
+ }
203
+
204
+ // ─── Confirmation Screen ───
205
+ const { submission } = step;
206
+
207
+ return (
208
+ <div
209
+ ref={contentRef}
210
+ tabIndex={-1}
211
+ className={cn("max-w-2xl mx-auto outline-none", className)}
212
+ style={style}
213
+ >
214
+ <Card>
215
+ <CardContent className="pt-8 pb-8">
216
+ <div className="text-center mb-6">
217
+ <div className="mx-auto mb-4 w-14 h-14 rounded-full bg-success/10 flex items-center justify-center">
218
+ <CheckCircle2 className="size-7 text-success" />
219
+ </div>
220
+ <h2 className="text-xl font-bold text-foreground mb-2">
221
+ {status === "graded" ? "Assignment Graded" : "Submission Received"}
222
+ </h2>
223
+ <div className="flex justify-center gap-2 mb-4">
224
+ <StatusBadge status={status} />
225
+ {dueDate && <DueDateDisplay dueDate={dueDate} size="small" />}
226
+ </div>
227
+ </div>
228
+
229
+ {/* Grade display */}
230
+ {status === "graded" && grade && (
231
+ <div className="text-center mb-6">
232
+ <GradeIndicator
233
+ percentage={
234
+ maxScore ? Math.round((grade.score / maxScore) * 100) : 0
235
+ }
236
+ size="large"
237
+ />
238
+ <p className="text-sm text-muted-foreground mt-2">
239
+ {grade.score}/{maxScore} points
240
+ </p>
241
+ {grade.feedback && (
242
+ <Alert className="text-left mt-4">
243
+ <AlertDescription>{grade.feedback}</AlertDescription>
244
+ </Alert>
245
+ )}
246
+ {rubric && grade.rubricLevels && (
247
+ <>
248
+ <Separator className="my-6" />
249
+ <RubricView
250
+ criteria={rubric}
251
+ selectedLevels={grade.rubricLevels}
252
+ totalScore={grade.score}
253
+ maxScore={rubricMaxScore}
254
+ />
255
+ </>
256
+ )}
257
+ </div>
258
+ )}
259
+
260
+ {/* Submission summary */}
261
+ <Separator className="my-6" />
262
+ <h3 className="text-sm font-semibold text-foreground mb-3">
263
+ What You Submitted
264
+ </h3>
265
+ <div className="space-y-2 text-sm text-muted-foreground">
266
+ {submission.textContent && (
267
+ <div className="flex items-start gap-2">
268
+ <FileText className="size-4 mt-0.5 shrink-0" />
269
+ <span className="line-clamp-2">{submission.textContent}</span>
270
+ </div>
271
+ )}
272
+ {submission.files && submission.files.length > 0 && (
273
+ <div className="flex items-start gap-2">
274
+ <Paperclip className="size-4 mt-0.5 shrink-0" />
275
+ <span>
276
+ {submission.files.length} file
277
+ {submission.files.length !== 1 ? "s" : ""} uploaded
278
+ </span>
279
+ </div>
280
+ )}
281
+ {submission.url && (
282
+ <div className="flex items-start gap-2">
283
+ <LinkIcon className="size-4 mt-0.5 shrink-0" />
284
+ <span className="truncate">{submission.url}</span>
285
+ </div>
286
+ )}
287
+ </div>
288
+
289
+ {/* Edit button */}
290
+ {canEdit && (
291
+ <div className="text-center mt-8">
292
+ <Button
293
+ variant="outline"
294
+ onClick={() => setStep({ tag: "work" })}
295
+ >
296
+ <FileEdit className="size-4 mr-2" />
297
+ Edit Submission
298
+ </Button>
299
+ </div>
300
+ )}
301
+ </CardContent>
302
+ </Card>
303
+ </div>
304
+ );
305
+ }
@@ -0,0 +1,73 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SubmissionData } from "../../sections/AssignmentSubmission/types";
3
+ import type { RubricCriterion } from "../../sections/RubricView/types";
4
+
5
+ /**
6
+ * AssignmentModule — a complete assignment experience with instructions,
7
+ * submission, and confirmation/grade review.
8
+ *
9
+ * Steps: Instructions → Work (AssignmentSubmission) → Confirmation.
10
+ * Optionally shows a grading rubric in both the instructions and
11
+ * graded confirmation views.
12
+ *
13
+ * @example
14
+ * <AssignmentModule
15
+ * title="Week 3 Essay"
16
+ * instructions={<p>Write a 500-word essay on React hooks.</p>}
17
+ * submissionTypes={["text", "file"]}
18
+ * rubric={rubricCriteria}
19
+ * onSubmit={(submission) => submitAssignment(submission)}
20
+ * />
21
+ */
22
+ export interface AssignmentModuleProps {
23
+ /** Assignment title */
24
+ title: string;
25
+ /** Assignment instructions (rich content) */
26
+ instructions: ReactNode;
27
+ /** Due date as ISO string */
28
+ dueDate?: string;
29
+ /** Maximum score points */
30
+ maxScore?: number;
31
+ /** Allowed submission types */
32
+ submissionTypes: ("text" | "file" | "url")[];
33
+ /** File upload constraints */
34
+ fileConstraints?: {
35
+ maxFiles?: number;
36
+ maxSizeMB?: number;
37
+ acceptedTypes?: string;
38
+ };
39
+ /** Rubric criteria for grading (shown in instructions and graded confirmation) */
40
+ rubric?: RubricCriterion[];
41
+ /** Existing submission for editing/viewing */
42
+ existingSubmission?: SubmissionData;
43
+ /** Current submission status. @default "not_started" */
44
+ status?:
45
+ | "not_started"
46
+ | "draft"
47
+ | "submitted"
48
+ | "late"
49
+ | "graded"
50
+ | "resubmit";
51
+ /** Grade data when graded */
52
+ grade?: {
53
+ score: number;
54
+ feedback?: ReactNode;
55
+ /** Selected rubric level UIDs per criterion UID */
56
+ rubricLevels?: Record<string, string>;
57
+ };
58
+ /** Called on final submission */
59
+ onSubmit?: (submission: SubmissionData) => void;
60
+ /** Called on draft save */
61
+ onSaveDraft?: (submission: SubmissionData) => void;
62
+ /** CSS class name for the root element */
63
+ className?: string;
64
+ /** Inline styles for the root element */
65
+ style?: React.CSSProperties;
66
+ }
67
+
68
+ export interface AssignmentModuleResult {
69
+ /** The submitted data */
70
+ submission: SubmissionData;
71
+ /** Status after submission */
72
+ status: string;
73
+ }