@hydralms/components 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/StudentProfile-BVfZMbnV.cjs +1 -0
- package/dist/StudentProfile-DeMxdrL3.js +3275 -0
- package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
- package/dist/assessment-toolbar/index.d.ts +5 -1
- package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
- package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
- package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/assessment-toolbar/types.d.ts +52 -4
- package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
- package/dist/common/index.d.ts +3 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/stepper.d.ts +6 -0
- package/dist/common/types.d.ts +38 -0
- package/dist/components.css +1 -1
- package/dist/content/attachment-list.d.ts +6 -0
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/content-block.d.ts +1 -1
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +8 -1
- package/dist/content/types.d.ts +63 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- package/dist/curriculum/curriculum-item.d.ts +1 -1
- package/dist/curriculum/index.d.ts +2 -0
- package/dist/curriculum/types.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +597 -308
- package/dist/license/HydraContext.d.ts +16 -0
- package/dist/license/ProBadge.d.ts +6 -0
- package/dist/license/index.d.ts +7 -0
- package/dist/license/tiers.d.ts +3 -0
- package/dist/license/useHydraLicense.d.ts +6 -0
- package/dist/license/validate.d.ts +13 -0
- package/dist/license/withProGate.d.ts +6 -0
- package/dist/modules/AssignmentModule/AssignmentModule.d.ts +5 -0
- package/dist/modules/AssignmentModule/types.d.ts +69 -0
- package/dist/modules/CertificateModule/CertificateModule.d.ts +5 -0
- package/dist/modules/CertificateModule/types.d.ts +51 -0
- package/dist/modules/CourseCatalogModule/CourseCatalogModule.d.ts +5 -0
- package/dist/modules/CourseCatalogModule/types.d.ts +43 -0
- package/dist/modules/CoursePlayer/CoursePlayer.d.ts +4 -1
- package/dist/modules/DiscussionModule/DiscussionModule.d.ts +5 -0
- package/dist/modules/DiscussionModule/types.d.ts +47 -0
- package/dist/modules/ExamModule/ExamModule.d.ts +5 -0
- package/dist/modules/ExamModule/types.d.ts +55 -0
- package/dist/modules/FlashcardLab/FlashcardLab.d.ts +4 -1
- package/dist/modules/FlashcardLab/types.d.ts +2 -0
- package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +5 -0
- package/dist/modules/GradeCenterModule/types.d.ts +56 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +10 -14
- package/dist/modules/StudentDashboardModule/StudentDashboardModule.d.ts +5 -0
- package/dist/modules/StudentDashboardModule/types.d.ts +54 -0
- package/dist/modules/StudentProfileModule/StudentProfileModule.d.ts +5 -0
- package/dist/modules/StudentProfileModule/types.d.ts +43 -0
- package/dist/modules/SurveyModule/SurveyModule.d.ts +5 -0
- package/dist/modules/SurveyModule/types.d.ts +51 -0
- package/dist/modules/_shared/assessment-intro.d.ts +16 -0
- package/dist/modules/_shared/assessment-results.d.ts +23 -0
- package/dist/modules/_shared/types.d.ts +10 -0
- package/dist/modules/_shared/use-timer.d.ts +9 -0
- package/dist/modules/index.d.ts +18 -0
- package/dist/modules.cjs +1 -0
- package/dist/modules.js +1834 -0
- package/dist/progress/achievement-badge.d.ts +6 -0
- package/dist/progress/activity-timeline.d.ts +6 -0
- package/dist/progress/index.d.ts +4 -1
- package/dist/progress/stat-card.d.ts +1 -1
- package/dist/progress/streak-badge.d.ts +6 -0
- package/dist/progress/types.d.ts +99 -0
- package/dist/provider/HydraProvider.d.ts +5 -1
- package/dist/questions/choice.d.ts +1 -1
- package/dist/questions/confidence-indicator.d.ts +37 -0
- package/dist/questions/essay.d.ts +2 -2
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +21 -0
- package/dist/questions/index.d.ts +11 -1
- package/dist/questions/inline-choice.d.ts +21 -0
- package/dist/questions/matching.d.ts +22 -0
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +11 -0
- package/dist/questions/ordering.d.ts +12 -0
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +23 -0
- package/dist/questions/scoring.d.ts +22 -0
- package/dist/questions/spreadsheet.d.ts +29 -0
- package/dist/questions/true-false.d.ts +1 -1
- package/dist/questions/types.d.ts +106 -1
- package/dist/questions/use-drag-reorder.d.ts +17 -0
- package/dist/sections/AnnouncementFeed/AnnouncementFeed.d.ts +1 -1
- package/dist/sections/AnnouncementFeed/types.d.ts +15 -1
- package/dist/sections/AssessmentReview/AssessmentReview.d.ts +1 -1
- package/dist/sections/AssessmentReview/types.d.ts +6 -0
- package/dist/sections/AssignmentSubmission/AssignmentSubmission.d.ts +1 -1
- package/dist/sections/AssignmentSubmission/types.d.ts +6 -0
- package/dist/sections/CertificateViewer/CertificateViewer.d.ts +1 -1
- package/dist/sections/CertificateViewer/certificate-variants.d.ts +42 -0
- package/dist/sections/CertificateViewer/types.d.ts +13 -5
- package/dist/sections/CourseCatalog/CourseCatalog.d.ts +2 -0
- package/dist/sections/CourseCatalog/types.d.ts +80 -0
- package/dist/sections/CourseOutline/CourseOutline.d.ts +1 -1
- package/dist/sections/CourseOutline/types.d.ts +6 -0
- package/dist/sections/DiscussionThread/DiscussionThread.d.ts +1 -1
- package/dist/sections/DiscussionThread/types.d.ts +6 -0
- package/dist/sections/EnrollmentWizard/EnrollmentWizard.d.ts +2 -0
- package/dist/sections/EnrollmentWizard/types.d.ts +66 -0
- package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
- package/dist/sections/ExamSession/types.d.ts +12 -1
- package/dist/sections/FlashcardStudySession/FlashcardStudySession.d.ts +1 -1
- package/dist/sections/FlashcardStudySession/types.d.ts +6 -0
- package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
- package/dist/sections/ForumBoard/types.d.ts +78 -0
- package/dist/sections/GradebookTable/GradebookTable.d.ts +1 -1
- package/dist/sections/GradebookTable/types.d.ts +14 -0
- package/dist/sections/LecturePlayer/LecturePlayer.d.ts +1 -1
- package/dist/sections/LecturePlayer/types.d.ts +8 -0
- package/dist/sections/LessonPage/LessonPage.d.ts +1 -1
- package/dist/sections/LessonPage/types.d.ts +6 -0
- package/dist/sections/PracticeQuiz/PracticeQuiz.d.ts +1 -1
- package/dist/sections/PracticeQuiz/types.d.ts +6 -0
- package/dist/sections/ProgressDashboard/ProgressDashboard.d.ts +1 -1
- package/dist/sections/ProgressDashboard/types.d.ts +6 -0
- package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
- package/dist/sections/QuizSession/types.d.ts +12 -1
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
- package/dist/sections/RequirementsChecklist/types.d.ts +43 -0
- package/dist/sections/ResourceLibrary/ResourceLibrary.d.ts +1 -1
- package/dist/sections/ResourceLibrary/types.d.ts +15 -1
- package/dist/sections/RubricView/RubricView.d.ts +9 -0
- package/dist/sections/RubricView/types.d.ts +56 -0
- package/dist/sections/ScrollableQuiz/ScrollableQuiz.d.ts +1 -1
- package/dist/sections/ScrollableQuiz/types.d.ts +6 -0
- package/dist/sections/StudentProfile/StudentProfile.d.ts +2 -0
- package/dist/sections/StudentProfile/types.d.ts +98 -0
- package/dist/sections/SurveyForm/SurveyForm.d.ts +1 -1
- package/dist/sections/SurveyForm/types.d.ts +6 -0
- package/dist/sections/_shared/merge-answers.d.ts +9 -0
- package/dist/sections/_shared/section-shell.d.ts +20 -0
- package/dist/sections/_shared/use-assessment-session.d.ts +30 -0
- package/dist/sections/index.d.ts +13 -1
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +282 -1786
- package/dist/social/post-card.d.ts +1 -1
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/tabs-BuY1iNJE.js +22305 -0
- package/dist/ui/alert.d.ts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/drawer.d.ts +84 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +32 -0
- package/dist/ui/rich-text-toolbar.d.ts +8 -0
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/array-utils.d.ts +4 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/flatten-leaves.d.ts +6 -0
- package/dist/utils/format-file-size.d.ts +1 -0
- package/dist/utils/format-timestamp.d.ts +1 -0
- package/dist/utils/is-empty-html.d.ts +5 -0
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/string-utils.d.ts +12 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-bookmark.d.ts +1 -1
- package/dist/video/video-player.d.ts +1 -1
- package/dist/video/video-playlist-item.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +40 -137
- package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
- package/src/assessment-toolbar/index.ts +6 -0
- package/src/assessment-toolbar/question-header-bar.tsx +61 -0
- package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
- package/src/assessment-toolbar/question-navigator.tsx +13 -36
- package/src/assessment-toolbar/timer-display.tsx +6 -5
- package/src/assessment-toolbar/types.ts +54 -4
- package/src/assessment-toolbar/use-countdown.ts +153 -0
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +5 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +8 -6
- package/src/common/stepper.tsx +100 -0
- package/src/common/types.ts +41 -0
- package/src/content/attachment-list.tsx +92 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +68 -2
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +11 -6
- package/src/content/index.ts +9 -0
- package/src/content/types.ts +46 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +9 -5
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/feedback/feedback-banner.tsx +12 -14
- package/src/flashcards/flashcard-deck.tsx +1 -9
- package/src/flashcards/flashcard.tsx +29 -9
- package/src/index.ts +3 -0
- package/src/license/HydraContext.tsx +62 -0
- package/src/license/ProBadge.tsx +43 -0
- package/src/license/index.ts +7 -0
- package/src/license/tiers.ts +24 -0
- package/src/license/useHydraLicense.ts +10 -0
- package/src/license/validate.ts +90 -0
- package/src/license/withProGate.tsx +21 -0
- package/src/modules/AssignmentModule/AssignmentModule.tsx +314 -0
- package/src/modules/AssignmentModule/types.ts +77 -0
- package/src/modules/CertificateModule/CertificateModule.tsx +173 -0
- package/src/modules/CertificateModule/types.ts +49 -0
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +80 -69
- package/src/modules/DiscussionModule/DiscussionModule.tsx +145 -0
- package/src/modules/DiscussionModule/types.ts +54 -0
- package/src/modules/ExamModule/ExamModule.tsx +151 -0
- package/src/modules/ExamModule/types.ts +57 -0
- package/src/modules/FlashcardLab/FlashcardLab.tsx +39 -21
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +174 -0
- package/src/modules/GradeCenterModule/types.ts +65 -0
- package/src/modules/QuizModule/QuizModule.tsx +58 -178
- package/src/modules/QuizModule/types.ts +10 -15
- package/src/modules/StudentDashboardModule/StudentDashboardModule.tsx +117 -0
- package/src/modules/StudentDashboardModule/types.ts +56 -0
- package/src/modules/StudentProfileModule/StudentProfileModule.tsx +289 -0
- package/src/modules/StudentProfileModule/types.ts +45 -0
- package/src/modules/SurveyModule/SurveyModule.tsx +185 -0
- package/src/modules/SurveyModule/types.ts +53 -0
- package/src/modules/_shared/assessment-intro.tsx +75 -0
- package/src/modules/_shared/assessment-results.tsx +133 -0
- package/src/modules/_shared/types.ts +11 -0
- package/src/modules/_shared/use-timer.ts +49 -0
- package/src/modules/index.ts +33 -0
- package/src/progress/achievement-badge.tsx +52 -0
- package/src/progress/activity-timeline.tsx +84 -0
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/index.ts +7 -0
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +37 -18
- package/src/progress/streak-badge.tsx +35 -0
- package/src/progress/types.ts +103 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +19 -14
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +28 -28
- package/src/questions/fill-in-the-blank.tsx +20 -19
- package/src/questions/hotspot.tsx +154 -0
- package/src/questions/index.ts +18 -0
- package/src/questions/inline-choice.tsx +152 -0
- package/src/questions/matching.tsx +229 -0
- package/src/questions/multiple-choice.tsx +19 -14
- package/src/questions/numeric.tsx +106 -0
- package/src/questions/ordering.tsx +167 -0
- package/src/questions/question-renderer.tsx +24 -2
- package/src/questions/scenario.tsx +140 -0
- package/src/questions/scoring.ts +201 -0
- package/src/questions/spreadsheet.tsx +260 -0
- package/src/questions/true-false.tsx +19 -14
- package/src/questions/types.ts +123 -1
- package/src/questions/use-drag-reorder.ts +80 -0
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +66 -23
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +50 -2
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +44 -6
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +215 -60
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +19 -5
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +45 -14
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +55 -11
- package/src/sections/DiscussionThread/types.ts +6 -0
- package/src/sections/EnrollmentWizard/EnrollmentWizard.tsx +343 -0
- package/src/sections/EnrollmentWizard/types.ts +65 -0
- package/src/sections/ExamSession/ExamSession.tsx +125 -82
- package/src/sections/ExamSession/types.ts +12 -1
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +342 -0
- package/src/sections/ForumBoard/types.ts +81 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +55 -2
- package/src/sections/GradebookTable/types.ts +14 -0
- package/src/sections/LecturePlayer/LecturePlayer.tsx +63 -37
- package/src/sections/LecturePlayer/types.ts +8 -0
- package/src/sections/LessonPage/LessonPage.tsx +40 -13
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +119 -98
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +121 -67
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +115 -67
- package/src/sections/QuizSession/types.ts +12 -1
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +147 -0
- package/src/sections/RequirementsChecklist/types.ts +44 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +68 -17
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +174 -0
- package/src/sections/RubricView/types.ts +58 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +58 -23
- package/src/sections/ScrollableQuiz/types.ts +6 -0
- package/src/sections/StudentProfile/StudentProfile.tsx +279 -0
- package/src/sections/StudentProfile/types.ts +99 -0
- package/src/sections/SurveyForm/SurveyForm.tsx +40 -10
- package/src/sections/SurveyForm/types.ts +6 -0
- package/src/sections/_shared/merge-answers.ts +22 -0
- package/src/sections/_shared/section-shell.tsx +64 -0
- package/src/sections/_shared/use-assessment-session.ts +125 -0
- package/src/sections/index.ts +42 -1
- package/src/social/post-card.tsx +8 -19
- package/src/social/user-avatar.tsx +10 -5
- package/src/styles/globals.css +52 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/drawer.tsx +600 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +119 -0
- package/src/ui/rich-text-toolbar.tsx +157 -0
- package/src/ui/toast.tsx +170 -0
- package/src/utils/array-utils.ts +17 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/flatten-leaves.ts +17 -0
- package/src/utils/format-file-size.ts +5 -0
- package/src/utils/format-timestamp.ts +13 -0
- package/src/utils/is-empty-html.ts +7 -0
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/utils/shuffle.ts +8 -0
- package/src/utils/string-utils.ts +30 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-bookmark.tsx +4 -3
- package/src/video/video-chapter-list.tsx +9 -4
- package/src/video/video-player.tsx +24 -5
- package/src/video/video-playlist-item.tsx +8 -3
- package/src/video/video-thumbnail-card.tsx +4 -0
- package/src/video/video-transcript.tsx +8 -5
- package/dist/table-BrS5cDQu.js +0 -2510
- package/dist/table-D6AkBBEo.cjs +0 -1
|
@@ -37,8 +37,6 @@ export interface AssessmentToolbarProps {
|
|
|
37
37
|
questions?: QuestionNavigatorItem[];
|
|
38
38
|
/** Called when the user navigates to a specific question via the navigator */
|
|
39
39
|
onNavigateToQuestion?: (questionUid: string) => void;
|
|
40
|
-
/** Called when the user flags or unflags a question */
|
|
41
|
-
onToggleFlag?: (questionUid: string) => void;
|
|
42
40
|
/** UID of the currently active question */
|
|
43
41
|
currentQuestionUid?: string;
|
|
44
42
|
/** Whether the assessment has been completed/submitted */
|
|
@@ -72,14 +70,12 @@ export interface TimerDisplayProps {
|
|
|
72
70
|
* questions={questions}
|
|
73
71
|
* currentQuestionUid={currentUid}
|
|
74
72
|
* onNavigate={handleNavigate}
|
|
75
|
-
* onToggleFlag={handleFlag}
|
|
76
73
|
* />
|
|
77
74
|
*/
|
|
78
75
|
export interface QuestionNavigatorProps {
|
|
79
76
|
questions: QuestionNavigatorItem[];
|
|
80
77
|
currentQuestionUid?: string;
|
|
81
78
|
onNavigate?: (questionUid: string) => void;
|
|
82
|
-
onToggleFlag?: (questionUid: string) => void;
|
|
83
79
|
readOnly?: boolean;
|
|
84
80
|
}
|
|
85
81
|
|
|
@@ -90,3 +86,57 @@ export interface QuestionNavigatorItem {
|
|
|
90
86
|
isAnswered: boolean;
|
|
91
87
|
isSkipped: boolean;
|
|
92
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* QuestionHeaderBar displays question number, flag toggle, and optional
|
|
92
|
+
* materials button inside the question Card above the QuestionRenderer.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* <QuestionHeaderBar
|
|
96
|
+
* questionNumber={3}
|
|
97
|
+
* totalQuestions={10}
|
|
98
|
+
* isFlagged={false}
|
|
99
|
+
* onToggleFlag={() => toggleFlag(questionUid)}
|
|
100
|
+
* hasMaterials
|
|
101
|
+
* onOpenMaterials={() => setDrawerOpen(true)}
|
|
102
|
+
* />
|
|
103
|
+
*/
|
|
104
|
+
export interface QuestionHeaderBarProps {
|
|
105
|
+
/** 1-based question number */
|
|
106
|
+
questionNumber: number;
|
|
107
|
+
/** Total number of questions */
|
|
108
|
+
totalQuestions: number;
|
|
109
|
+
/** Whether this question is currently flagged */
|
|
110
|
+
isFlagged: boolean;
|
|
111
|
+
/** Called when the user toggles the flag */
|
|
112
|
+
onToggleFlag?: () => void;
|
|
113
|
+
/** Whether the current question has related materials available */
|
|
114
|
+
hasMaterials?: boolean;
|
|
115
|
+
/** Called when the user clicks the materials button */
|
|
116
|
+
onOpenMaterials?: () => void;
|
|
117
|
+
/** When true, hides interactive elements */
|
|
118
|
+
readOnly?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* QuestionMaterialsDrawer renders a slide-in panel with content blocks
|
|
123
|
+
* linked to the current question for open-book reference.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* <QuestionMaterialsDrawer
|
|
127
|
+
* open={open}
|
|
128
|
+
* onOpenChange={setOpen}
|
|
129
|
+
* materials={currentMaterials}
|
|
130
|
+
* questionNumber={3}
|
|
131
|
+
* />
|
|
132
|
+
*/
|
|
133
|
+
export interface QuestionMaterialsDrawerProps {
|
|
134
|
+
/** Whether the drawer is open */
|
|
135
|
+
open: boolean;
|
|
136
|
+
/** Called when the drawer should open or close */
|
|
137
|
+
onOpenChange: (open: boolean) => void;
|
|
138
|
+
/** One or more material groups to display in the drawer */
|
|
139
|
+
materials: import("../questions/types").QuestionMaterial[];
|
|
140
|
+
/** Question number for the title display */
|
|
141
|
+
questionNumber?: number;
|
|
142
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseCountdownOptions {
|
|
4
|
+
/** Total countdown duration in seconds */
|
|
5
|
+
durationSeconds: number;
|
|
6
|
+
/** Seconds remaining at which to trigger warning state. @default 60 */
|
|
7
|
+
warningThresholdSeconds?: number;
|
|
8
|
+
/** Called once when the countdown reaches zero */
|
|
9
|
+
onExpire?: () => void;
|
|
10
|
+
/** Called once when the countdown enters warning territory */
|
|
11
|
+
onWarning?: () => void;
|
|
12
|
+
/** Whether to start the countdown immediately. @default false */
|
|
13
|
+
autoStart?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseCountdownReturn {
|
|
17
|
+
/** Seconds remaining in the countdown */
|
|
18
|
+
timeRemaining: number;
|
|
19
|
+
/** Whether the countdown is actively running */
|
|
20
|
+
isRunning: boolean;
|
|
21
|
+
/** Whether the countdown has been paused */
|
|
22
|
+
isPaused: boolean;
|
|
23
|
+
/** Whether the countdown has reached zero */
|
|
24
|
+
isExpired: boolean;
|
|
25
|
+
/** Whether the countdown is within the warning threshold */
|
|
26
|
+
isWarning: boolean;
|
|
27
|
+
/** Start or restart the countdown */
|
|
28
|
+
start: () => void;
|
|
29
|
+
/** Pause the countdown */
|
|
30
|
+
pause: () => void;
|
|
31
|
+
/** Resume after pausing */
|
|
32
|
+
resume: () => void;
|
|
33
|
+
/** Reset to the original duration, stopped */
|
|
34
|
+
reset: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TimerState = "idle" | "running" | "paused" | "expired";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* useCountdown manages a countdown timer lifecycle with warning and expiry callbacks.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const { timeRemaining, isWarning, start } = useCountdown({
|
|
44
|
+
* durationSeconds: 300,
|
|
45
|
+
* warningThresholdSeconds: 60,
|
|
46
|
+
* onExpire: () => handleAutoSubmit(),
|
|
47
|
+
* });
|
|
48
|
+
*/
|
|
49
|
+
export function useCountdown({
|
|
50
|
+
durationSeconds,
|
|
51
|
+
warningThresholdSeconds = 60,
|
|
52
|
+
onExpire,
|
|
53
|
+
onWarning,
|
|
54
|
+
autoStart = false,
|
|
55
|
+
}: UseCountdownOptions): UseCountdownReturn {
|
|
56
|
+
const [timeRemaining, setTimeRemaining] = useState(durationSeconds);
|
|
57
|
+
const stateRef = useRef<TimerState>(autoStart ? "running" : "idle");
|
|
58
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
59
|
+
const onExpireRef = useRef(onExpire);
|
|
60
|
+
const onWarningRef = useRef(onWarning);
|
|
61
|
+
const warningFiredRef = useRef(false);
|
|
62
|
+
|
|
63
|
+
// Keep callback refs current without recreating intervals
|
|
64
|
+
onExpireRef.current = onExpire;
|
|
65
|
+
onWarningRef.current = onWarning;
|
|
66
|
+
|
|
67
|
+
const clearTimer = useCallback(() => {
|
|
68
|
+
if (intervalRef.current != null) {
|
|
69
|
+
clearInterval(intervalRef.current);
|
|
70
|
+
intervalRef.current = null;
|
|
71
|
+
}
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const startInterval = useCallback(() => {
|
|
75
|
+
clearTimer();
|
|
76
|
+
intervalRef.current = setInterval(() => {
|
|
77
|
+
setTimeRemaining((prev) => {
|
|
78
|
+
const next = prev - 1;
|
|
79
|
+
|
|
80
|
+
// Warning check
|
|
81
|
+
if (
|
|
82
|
+
!warningFiredRef.current &&
|
|
83
|
+
next <= warningThresholdSeconds &&
|
|
84
|
+
next > 0
|
|
85
|
+
) {
|
|
86
|
+
warningFiredRef.current = true;
|
|
87
|
+
onWarningRef.current?.();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Expiry check
|
|
91
|
+
if (next <= 0) {
|
|
92
|
+
clearTimer();
|
|
93
|
+
stateRef.current = "expired";
|
|
94
|
+
onExpireRef.current?.();
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return next;
|
|
99
|
+
});
|
|
100
|
+
}, 1000);
|
|
101
|
+
}, [clearTimer, warningThresholdSeconds]);
|
|
102
|
+
|
|
103
|
+
const start = useCallback(() => {
|
|
104
|
+
setTimeRemaining(durationSeconds);
|
|
105
|
+
warningFiredRef.current = false;
|
|
106
|
+
stateRef.current = "running";
|
|
107
|
+
startInterval();
|
|
108
|
+
}, [durationSeconds, startInterval]);
|
|
109
|
+
|
|
110
|
+
const pause = useCallback(() => {
|
|
111
|
+
if (stateRef.current !== "running") return;
|
|
112
|
+
clearTimer();
|
|
113
|
+
stateRef.current = "paused";
|
|
114
|
+
// Force re-render so isPaused updates
|
|
115
|
+
setTimeRemaining((prev) => prev);
|
|
116
|
+
}, [clearTimer]);
|
|
117
|
+
|
|
118
|
+
const resume = useCallback(() => {
|
|
119
|
+
if (stateRef.current !== "paused") return;
|
|
120
|
+
stateRef.current = "running";
|
|
121
|
+
startInterval();
|
|
122
|
+
}, [startInterval]);
|
|
123
|
+
|
|
124
|
+
const reset = useCallback(() => {
|
|
125
|
+
clearTimer();
|
|
126
|
+
stateRef.current = "idle";
|
|
127
|
+
warningFiredRef.current = false;
|
|
128
|
+
setTimeRemaining(durationSeconds);
|
|
129
|
+
}, [clearTimer, durationSeconds]);
|
|
130
|
+
|
|
131
|
+
// Auto-start on mount if requested
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (autoStart) {
|
|
134
|
+
startInterval();
|
|
135
|
+
}
|
|
136
|
+
return clearTimer;
|
|
137
|
+
}, [autoStart, startInterval, clearTimer]);
|
|
138
|
+
|
|
139
|
+
const state = stateRef.current;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
timeRemaining,
|
|
143
|
+
isRunning: state === "running",
|
|
144
|
+
isPaused: state === "paused",
|
|
145
|
+
isExpired: state === "expired",
|
|
146
|
+
isWarning:
|
|
147
|
+
timeRemaining <= warningThresholdSeconds && timeRemaining > 0,
|
|
148
|
+
start,
|
|
149
|
+
pause,
|
|
150
|
+
resume,
|
|
151
|
+
reset,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -6,6 +6,7 @@ export function EmptyState({ icon, title, description, action, className, style
|
|
|
6
6
|
<div
|
|
7
7
|
className={cn("flex flex-col items-center justify-center text-center px-3 py-6", className)}
|
|
8
8
|
style={style}
|
|
9
|
+
role="status"
|
|
9
10
|
>
|
|
10
11
|
{icon && (
|
|
11
12
|
<div className="mb-2 text-muted-foreground [&>svg]:size-12">
|
package/src/common/index.ts
CHANGED
|
@@ -3,10 +3,15 @@ export { ConfirmDialog } from "./confirm-dialog";
|
|
|
3
3
|
export { SearchInput } from "./search-input";
|
|
4
4
|
export { StatusBadge } from "./status-badge";
|
|
5
5
|
export { DueDateDisplay } from "./due-date-display";
|
|
6
|
+
export { Stepper } from "./stepper";
|
|
7
|
+
export { Pagination } from "./pagination";
|
|
6
8
|
export type {
|
|
7
9
|
EmptyStateProps,
|
|
8
10
|
ConfirmDialogProps,
|
|
9
11
|
SearchInputProps,
|
|
10
12
|
StatusBadgeProps,
|
|
11
13
|
DueDateDisplayProps,
|
|
14
|
+
StepperProps,
|
|
15
|
+
StepDefinition,
|
|
16
|
+
PaginationProps,
|
|
12
17
|
} from "./types";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { memo, useMemo } from "react";
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pagination provides page navigation controls with page numbers,
|
|
8
|
+
* previous/next buttons, and ellipsis for large page ranges.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <Pagination
|
|
12
|
+
* currentPage={3}
|
|
13
|
+
* totalPages={10}
|
|
14
|
+
* onPageChange={(page) => setPage(page)}
|
|
15
|
+
* />
|
|
16
|
+
*/
|
|
17
|
+
export interface PaginationProps {
|
|
18
|
+
/** Current active page (1-indexed) */
|
|
19
|
+
currentPage: number;
|
|
20
|
+
/** Total number of pages */
|
|
21
|
+
totalPages: number;
|
|
22
|
+
/** Called when the user navigates to a page */
|
|
23
|
+
onPageChange: (page: number) => void;
|
|
24
|
+
/** Number of page buttons shown on each side of the current page (default 1) */
|
|
25
|
+
siblingCount?: number;
|
|
26
|
+
/** CSS class name for the root element */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Inline styles for the root element */
|
|
29
|
+
style?: React.CSSProperties;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPageRange(
|
|
33
|
+
current: number,
|
|
34
|
+
total: number,
|
|
35
|
+
siblings: number,
|
|
36
|
+
): (number | "ellipsis")[] {
|
|
37
|
+
const totalSlots = siblings * 2 + 5;
|
|
38
|
+
if (total <= totalSlots) {
|
|
39
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const leftSibling = Math.max(current - siblings, 1);
|
|
43
|
+
const rightSibling = Math.min(current + siblings, total);
|
|
44
|
+
const showLeftEllipsis = leftSibling > 2;
|
|
45
|
+
const showRightEllipsis = rightSibling < total - 1;
|
|
46
|
+
|
|
47
|
+
if (!showLeftEllipsis && showRightEllipsis) {
|
|
48
|
+
const leftCount = siblings * 2 + 3;
|
|
49
|
+
const leftRange = Array.from({ length: leftCount }, (_, i) => i + 1);
|
|
50
|
+
return [...leftRange, "ellipsis", total];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (showLeftEllipsis && !showRightEllipsis) {
|
|
54
|
+
const rightCount = siblings * 2 + 3;
|
|
55
|
+
const rightRange = Array.from(
|
|
56
|
+
{ length: rightCount },
|
|
57
|
+
(_, i) => total - rightCount + i + 1,
|
|
58
|
+
);
|
|
59
|
+
return [1, "ellipsis", ...rightRange];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const middleRange = Array.from(
|
|
63
|
+
{ length: rightSibling - leftSibling + 1 },
|
|
64
|
+
(_, i) => leftSibling + i,
|
|
65
|
+
);
|
|
66
|
+
return [1, "ellipsis", ...middleRange, "ellipsis", total];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Pagination = memo(function Pagination({
|
|
70
|
+
currentPage,
|
|
71
|
+
totalPages,
|
|
72
|
+
onPageChange,
|
|
73
|
+
siblingCount = 1,
|
|
74
|
+
className,
|
|
75
|
+
style,
|
|
76
|
+
}: PaginationProps) {
|
|
77
|
+
const pages = useMemo(
|
|
78
|
+
() => getPageRange(currentPage, totalPages, siblingCount),
|
|
79
|
+
[currentPage, totalPages, siblingCount],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (totalPages <= 1) return null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<nav
|
|
86
|
+
data-slot="pagination"
|
|
87
|
+
aria-label="Pagination"
|
|
88
|
+
className={cn("flex items-center justify-center gap-1", className)}
|
|
89
|
+
style={style}
|
|
90
|
+
>
|
|
91
|
+
<Button
|
|
92
|
+
variant="ghost"
|
|
93
|
+
size="icon-sm"
|
|
94
|
+
onClick={() => onPageChange(currentPage - 1)}
|
|
95
|
+
disabled={currentPage <= 1}
|
|
96
|
+
aria-label="Previous page"
|
|
97
|
+
>
|
|
98
|
+
<ChevronLeft className="size-4" />
|
|
99
|
+
</Button>
|
|
100
|
+
|
|
101
|
+
{pages.map((page, i) =>
|
|
102
|
+
page === "ellipsis" ? (
|
|
103
|
+
<span
|
|
104
|
+
key={`ellipsis-${i}`}
|
|
105
|
+
className="flex items-center justify-center size-8 text-xs text-muted-foreground"
|
|
106
|
+
>
|
|
107
|
+
<span aria-hidden="true">...</span>
|
|
108
|
+
<span className="sr-only">More pages</span>
|
|
109
|
+
</span>
|
|
110
|
+
) : (
|
|
111
|
+
<Button
|
|
112
|
+
key={page}
|
|
113
|
+
variant={page === currentPage ? "default" : "ghost"}
|
|
114
|
+
size="icon-sm"
|
|
115
|
+
onClick={() => onPageChange(page)}
|
|
116
|
+
aria-label={`Page ${page}`}
|
|
117
|
+
aria-current={page === currentPage ? "page" : undefined}
|
|
118
|
+
>
|
|
119
|
+
<span className="text-xs">{page}</span>
|
|
120
|
+
</Button>
|
|
121
|
+
),
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
<Button
|
|
125
|
+
variant="ghost"
|
|
126
|
+
size="icon-sm"
|
|
127
|
+
onClick={() => onPageChange(currentPage + 1)}
|
|
128
|
+
disabled={currentPage >= totalPages}
|
|
129
|
+
aria-label="Next page"
|
|
130
|
+
>
|
|
131
|
+
<ChevronRight className="size-4" />
|
|
132
|
+
</Button>
|
|
133
|
+
</nav>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useMemo, useRef, useEffect } from "react";
|
|
2
2
|
import { Search, X } from "lucide-react";
|
|
3
3
|
import { debounce } from "../utils/debounce";
|
|
4
4
|
import type { SearchInputProps } from "./types";
|
|
@@ -16,12 +16,14 @@ export function SearchInput({
|
|
|
16
16
|
}: SearchInputProps) {
|
|
17
17
|
const [localValue, setLocalValue] = useState(value);
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}, [value]);
|
|
19
|
+
const onChangeRef = useRef(onChange);
|
|
20
|
+
onChangeRef.current = onChange;
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const debouncedOnChange = useMemo(
|
|
23
|
+
() => debounce((val: string) => onChangeRef.current(val), debounceMs),
|
|
24
|
+
[debounceMs],
|
|
25
|
+
);
|
|
26
|
+
useEffect(() => () => debouncedOnChange.cancel(), [debouncedOnChange]);
|
|
25
27
|
|
|
26
28
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
27
29
|
const next = e.target.value;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Check } from "lucide-react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import type { StepperProps } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stepper renders a visual step indicator for multi-step flows,
|
|
7
|
+
* showing completed, current, and upcoming steps with labels.
|
|
8
|
+
*/
|
|
9
|
+
export function Stepper({
|
|
10
|
+
steps,
|
|
11
|
+
currentStep,
|
|
12
|
+
orientation = "horizontal",
|
|
13
|
+
variant = "default",
|
|
14
|
+
className,
|
|
15
|
+
style,
|
|
16
|
+
}: StepperProps) {
|
|
17
|
+
const isHorizontal = orientation === "horizontal";
|
|
18
|
+
const isCompact = variant === "compact";
|
|
19
|
+
|
|
20
|
+
const circleSize = isCompact ? "w-6 h-6 text-xs" : "w-8 h-8 text-sm";
|
|
21
|
+
const iconSize = isCompact ? 12 : 16;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
"flex",
|
|
27
|
+
isHorizontal ? "flex-row items-start" : "flex-col",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
style={style}
|
|
31
|
+
>
|
|
32
|
+
{steps.map((step, index) => {
|
|
33
|
+
const isCompleted = index < currentStep;
|
|
34
|
+
const isCurrent = index === currentStep;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div key={index} className={cn("flex", isHorizontal ? "flex-1 flex-row items-start" : "flex-row items-start")}>
|
|
38
|
+
{/* Step indicator */}
|
|
39
|
+
<div className={cn("flex", isHorizontal ? "flex-col items-center" : "flex-row items-start gap-3")}>
|
|
40
|
+
{/* Circle */}
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
"rounded-full flex items-center justify-center shrink-0 font-semibold transition-colors",
|
|
44
|
+
circleSize,
|
|
45
|
+
isCompleted && "bg-primary text-primary-foreground",
|
|
46
|
+
isCurrent && "border-2 border-primary bg-primary/10 text-primary",
|
|
47
|
+
!isCompleted && !isCurrent && "border-2 border-border bg-muted text-muted-foreground",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{isCompleted ? (
|
|
51
|
+
<Check size={iconSize} />
|
|
52
|
+
) : (
|
|
53
|
+
<span>{index + 1}</span>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{/* Label */}
|
|
58
|
+
<div className={cn(isHorizontal ? "mt-1.5 text-center" : "pt-0.5")}>
|
|
59
|
+
<p
|
|
60
|
+
className={cn(
|
|
61
|
+
"text-sm font-medium leading-tight",
|
|
62
|
+
isCurrent ? "text-foreground" : "text-muted-foreground",
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{step.label}
|
|
66
|
+
</p>
|
|
67
|
+
{!isCompact && step.description && (
|
|
68
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
69
|
+
{step.description}
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Connector line */}
|
|
76
|
+
{index < steps.length - 1 && (
|
|
77
|
+
isHorizontal ? (
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex-1 h-0.5 self-center mt-0 mx-2",
|
|
81
|
+
isCompact ? "mt-3" : "mt-4",
|
|
82
|
+
isCompleted ? "bg-primary" : "bg-border",
|
|
83
|
+
)}
|
|
84
|
+
/>
|
|
85
|
+
) : (
|
|
86
|
+
<div
|
|
87
|
+
className={cn(
|
|
88
|
+
"w-0.5 h-6 ml-3.5",
|
|
89
|
+
isCompact && "ml-2.5",
|
|
90
|
+
isCompleted ? "bg-primary" : "bg-border",
|
|
91
|
+
)}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
package/src/common/types.ts
CHANGED
|
@@ -127,3 +127,44 @@ export interface DueDateDisplayProps {
|
|
|
127
127
|
/** Inline styles for the root element */
|
|
128
128
|
style?: React.CSSProperties;
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A single step definition used by the Stepper component.
|
|
133
|
+
*/
|
|
134
|
+
export interface StepDefinition {
|
|
135
|
+
/** Step label text */
|
|
136
|
+
label: string;
|
|
137
|
+
/** Optional secondary description */
|
|
138
|
+
description?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Stepper renders a visual step indicator for multi-step flows,
|
|
143
|
+
* showing completed, current, and upcoming steps with labels.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* <Stepper
|
|
147
|
+
* steps={[
|
|
148
|
+
* { label: "Setup" },
|
|
149
|
+
* { label: "Questions" },
|
|
150
|
+
* { label: "Results" },
|
|
151
|
+
* ]}
|
|
152
|
+
* currentStep={1}
|
|
153
|
+
* />
|
|
154
|
+
*/
|
|
155
|
+
export interface StepperProps {
|
|
156
|
+
/** Ordered list of steps to display */
|
|
157
|
+
steps: StepDefinition[];
|
|
158
|
+
/** Zero-based index of the currently active step */
|
|
159
|
+
currentStep: number;
|
|
160
|
+
/** Layout orientation. @default 'horizontal' */
|
|
161
|
+
orientation?: "horizontal" | "vertical";
|
|
162
|
+
/** Visual density variant. @default 'default' */
|
|
163
|
+
variant?: "default" | "compact";
|
|
164
|
+
/** CSS class name for the root element */
|
|
165
|
+
className?: string;
|
|
166
|
+
/** Inline styles for the root element */
|
|
167
|
+
style?: React.CSSProperties;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type { PaginationProps } from "./pagination";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
File,
|
|
3
|
+
FileText,
|
|
4
|
+
Image,
|
|
5
|
+
Film,
|
|
6
|
+
Music,
|
|
7
|
+
Archive,
|
|
8
|
+
Download,
|
|
9
|
+
X,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { cn } from "../lib/utils";
|
|
12
|
+
import { formatFileSize } from "../utils/format-file-size";
|
|
13
|
+
import { Button } from "../ui/button";
|
|
14
|
+
import type { AttachmentListProps } from "./types";
|
|
15
|
+
|
|
16
|
+
function getFileIcon(mimeType: string) {
|
|
17
|
+
if (mimeType.startsWith("image/")) return Image;
|
|
18
|
+
if (mimeType.startsWith("video/")) return Film;
|
|
19
|
+
if (mimeType.startsWith("audio/")) return Music;
|
|
20
|
+
if (mimeType === "application/pdf") return FileText;
|
|
21
|
+
if (
|
|
22
|
+
mimeType === "application/zip" ||
|
|
23
|
+
mimeType === "application/x-rar-compressed" ||
|
|
24
|
+
mimeType === "application/gzip"
|
|
25
|
+
)
|
|
26
|
+
return Archive;
|
|
27
|
+
return File;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* AttachmentList displays a read-only list of file attachments
|
|
32
|
+
* with type icons, formatted sizes, and optional download/remove actions.
|
|
33
|
+
*/
|
|
34
|
+
export function AttachmentList({
|
|
35
|
+
files,
|
|
36
|
+
onDownload,
|
|
37
|
+
onRemove,
|
|
38
|
+
readOnly = true,
|
|
39
|
+
className,
|
|
40
|
+
style,
|
|
41
|
+
}: AttachmentListProps) {
|
|
42
|
+
if (files.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className={cn("flex flex-col gap-1", className)} style={style}>
|
|
46
|
+
{files.map((file, index) => {
|
|
47
|
+
const Icon = getFileIcon(file.type);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
key={`${file.name}-${index}`}
|
|
52
|
+
className="flex items-center gap-3 rounded-md px-2 py-1.5 hover:bg-muted/50 transition-colors"
|
|
53
|
+
>
|
|
54
|
+
<span className="shrink-0 text-muted-foreground">
|
|
55
|
+
<Icon size={18} />
|
|
56
|
+
</span>
|
|
57
|
+
<div className="flex-1 min-w-0">
|
|
58
|
+
<span className="text-sm text-foreground block truncate">
|
|
59
|
+
{file.name}
|
|
60
|
+
</span>
|
|
61
|
+
<span className="text-xs text-muted-foreground">
|
|
62
|
+
{formatFileSize(file.size)}
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
{onDownload && file.url && (
|
|
66
|
+
<Button
|
|
67
|
+
variant="ghost"
|
|
68
|
+
size="sm"
|
|
69
|
+
className="shrink-0 h-7 w-7 p-0"
|
|
70
|
+
onClick={() => onDownload(file)}
|
|
71
|
+
aria-label={`Download ${file.name}`}
|
|
72
|
+
>
|
|
73
|
+
<Download size={14} />
|
|
74
|
+
</Button>
|
|
75
|
+
)}
|
|
76
|
+
{!readOnly && onRemove && (
|
|
77
|
+
<Button
|
|
78
|
+
variant="ghost"
|
|
79
|
+
size="sm"
|
|
80
|
+
className="shrink-0 h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
|
|
81
|
+
onClick={() => onRemove(file)}
|
|
82
|
+
aria-label={`Remove ${file.name}`}
|
|
83
|
+
>
|
|
84
|
+
<X size={14} />
|
|
85
|
+
</Button>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|