@hydralms/components 0.2.0 → 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/question-navigator.d.ts +1 -1
- package/dist/assessment-toolbar/timer-display.d.ts +1 -1
- package/dist/common/index.d.ts +2 -1
- package/dist/common/pagination.d.ts +26 -0
- package/dist/common/types.d.ts +1 -0
- package/dist/components.css +1 -1
- package/dist/content/audio-player.d.ts +22 -0
- package/dist/content/code-block.d.ts +30 -0
- package/dist/content/embed-block.d.ts +28 -0
- package/dist/content/index.d.ts +6 -0
- package/dist/content/types.d.ts +24 -0
- package/dist/curriculum/course-card.d.ts +51 -0
- 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 +494 -444
- 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 +4 -7
- package/dist/modules/AssignmentModule/types.d.ts +5 -1
- package/dist/modules/CertificateModule/CertificateModule.d.ts +4 -8
- package/dist/modules/CertificateModule/types.d.ts +6 -4
- 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 +4 -7
- package/dist/modules/ExamModule/ExamModule.d.ts +4 -7
- package/dist/modules/ExamModule/types.d.ts +5 -14
- 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 +4 -8
- package/dist/modules/GradeCenterModule/types.d.ts +2 -0
- package/dist/modules/QuizModule/QuizModule.d.ts +4 -1
- package/dist/modules/QuizModule/types.d.ts +5 -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 +4 -6
- package/dist/modules/SurveyModule/types.d.ts +2 -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 +6 -0
- package/dist/modules.cjs +1 -1
- package/dist/modules.js +1266 -854
- package/dist/progress/types.d.ts +2 -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 +1 -1
- package/dist/questions/fill-in-the-blank.d.ts +1 -1
- package/dist/questions/hotspot.d.ts +1 -1
- package/dist/questions/index.d.ts +2 -0
- package/dist/questions/inline-choice.d.ts +1 -1
- package/dist/questions/matching.d.ts +1 -1
- package/dist/questions/multiple-choice.d.ts +1 -1
- package/dist/questions/numeric.d.ts +1 -1
- package/dist/questions/ordering.d.ts +1 -1
- package/dist/questions/question-renderer.d.ts +1 -1
- package/dist/questions/scenario.d.ts +1 -1
- package/dist/questions/spreadsheet.d.ts +1 -1
- package/dist/questions/true-false.d.ts +1 -1
- 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 +6 -0
- 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 +6 -0
- 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 +1 -1
- package/dist/sections/ForumBoard/types.d.ts +14 -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 +6 -0
- package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +1 -1
- package/dist/sections/RequirementsChecklist/types.d.ts +6 -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 +1 -1
- package/dist/sections/RubricView/types.d.ts +6 -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 +6 -0
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +268 -307
- package/dist/tabs-BsfVo2Bl.cjs +173 -0
- package/dist/{tabs-Wf3h_Cx3.js → tabs-BuY1iNJE.js} +7532 -6807
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/rich-text-editor.d.ts +3 -1
- package/dist/ui/toast.d.ts +43 -0
- package/dist/utils/debounce.d.ts +5 -1
- package/dist/utils/pick-palette-color.d.ts +19 -0
- package/dist/video/types.d.ts +15 -0
- package/dist/video/video-player.d.ts +1 -1
- package/dist/withProGate-BWqcKdPM.js +137 -0
- package/dist/withProGate-DX6XqKLp.cjs +1 -0
- package/package.json +34 -220
- package/src/assessment-toolbar/question-navigator.tsx +10 -5
- package/src/assessment-toolbar/timer-display.tsx +4 -3
- package/src/assessment-toolbar/use-countdown.ts +1 -1
- package/src/common/empty-state.tsx +1 -0
- package/src/common/index.ts +2 -0
- package/src/common/pagination.tsx +135 -0
- package/src/common/search-input.tsx +2 -1
- package/src/common/types.ts +2 -0
- package/src/content/attachment-list.tsx +2 -0
- package/src/content/audio-player.tsx +196 -0
- package/src/content/code-block.tsx +113 -0
- package/src/content/content-block.tsx +64 -0
- package/src/content/embed-block.tsx +78 -0
- package/src/content/file-upload-zone.tsx +10 -0
- package/src/content/index.ts +6 -0
- package/src/content/types.ts +5 -0
- package/src/curriculum/course-card.tsx +199 -0
- package/src/curriculum/curriculum-item.tsx +3 -3
- package/src/curriculum/curriculum-tree.tsx +20 -13
- package/src/curriculum/index.ts +2 -0
- package/src/curriculum/types.ts +2 -2
- package/src/flashcards/flashcard.tsx +28 -8
- 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 +17 -8
- package/src/modules/AssignmentModule/types.ts +5 -1
- package/src/modules/CertificateModule/CertificateModule.tsx +21 -9
- package/src/modules/CertificateModule/types.ts +6 -4
- package/src/modules/CourseCatalogModule/CourseCatalogModule.tsx +126 -0
- package/src/modules/CourseCatalogModule/types.ts +47 -0
- package/src/modules/CoursePlayer/CoursePlayer.tsx +37 -22
- package/src/modules/DiscussionModule/DiscussionModule.tsx +57 -22
- package/src/modules/ExamModule/ExamModule.tsx +64 -198
- package/src/modules/ExamModule/types.ts +5 -14
- package/src/modules/FlashcardLab/FlashcardLab.tsx +10 -5
- package/src/modules/FlashcardLab/types.ts +2 -0
- package/src/modules/GradeCenterModule/GradeCenterModule.tsx +7 -2
- package/src/modules/GradeCenterModule/types.ts +2 -0
- package/src/modules/QuizModule/QuizModule.tsx +49 -169
- package/src/modules/QuizModule/types.ts +5 -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 +9 -4
- package/src/modules/SurveyModule/types.ts +2 -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 +9 -0
- package/src/progress/achievement-badge.tsx +3 -3
- package/src/progress/grade-indicator.tsx +9 -1
- package/src/progress/progress-ring.tsx +2 -1
- package/src/progress/stat-card.tsx +8 -1
- package/src/progress/types.ts +2 -0
- package/src/provider/HydraProvider.tsx +15 -6
- package/src/questions/choice.tsx +13 -6
- package/src/questions/confidence-indicator.tsx +107 -0
- package/src/questions/essay.tsx +6 -4
- package/src/questions/fill-in-the-blank.tsx +8 -4
- package/src/questions/hotspot.tsx +4 -4
- package/src/questions/index.ts +2 -0
- package/src/questions/inline-choice.tsx +5 -4
- package/src/questions/matching.tsx +5 -4
- package/src/questions/multiple-choice.tsx +13 -6
- package/src/questions/numeric.tsx +8 -4
- package/src/questions/ordering.tsx +12 -4
- package/src/questions/question-renderer.tsx +3 -2
- package/src/questions/scenario.tsx +4 -4
- package/src/questions/spreadsheet.tsx +5 -4
- package/src/questions/true-false.tsx +13 -6
- package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +64 -8
- package/src/sections/AnnouncementFeed/types.ts +15 -1
- package/src/sections/AssessmentReview/AssessmentReview.tsx +37 -0
- package/src/sections/AssessmentReview/types.ts +6 -0
- package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +37 -1
- package/src/sections/AssignmentSubmission/types.ts +6 -0
- package/src/sections/CertificateViewer/CertificateViewer.tsx +29 -227
- package/src/sections/CertificateViewer/certificate-variants.tsx +170 -0
- package/src/sections/CertificateViewer/types.ts +6 -0
- package/src/sections/CourseCatalog/CourseCatalog.tsx +220 -0
- package/src/sections/CourseCatalog/types.ts +76 -0
- package/src/sections/CourseOutline/CourseOutline.tsx +41 -0
- package/src/sections/CourseOutline/types.ts +6 -0
- package/src/sections/DiscussionThread/DiscussionThread.tsx +42 -1
- 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 +100 -94
- package/src/sections/ExamSession/types.ts +6 -0
- package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +53 -36
- package/src/sections/FlashcardStudySession/types.ts +6 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +59 -1
- package/src/sections/ForumBoard/types.ts +14 -0
- package/src/sections/GradebookTable/GradebookTable.tsx +54 -1
- 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 +36 -5
- package/src/sections/LessonPage/types.ts +6 -0
- package/src/sections/PracticeQuiz/PracticeQuiz.tsx +106 -74
- package/src/sections/PracticeQuiz/types.ts +6 -0
- package/src/sections/ProgressDashboard/ProgressDashboard.tsx +64 -10
- package/src/sections/ProgressDashboard/types.ts +6 -0
- package/src/sections/QuizSession/QuizSession.tsx +71 -82
- package/src/sections/QuizSession/types.ts +6 -0
- package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +41 -1
- package/src/sections/RequirementsChecklist/types.ts +6 -0
- package/src/sections/ResourceLibrary/ResourceLibrary.tsx +64 -8
- package/src/sections/ResourceLibrary/types.ts +15 -1
- package/src/sections/RubricView/RubricView.tsx +37 -1
- package/src/sections/RubricView/types.ts +6 -0
- package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +36 -15
- 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 +32 -5
- 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 +22 -0
- package/src/social/user-avatar.tsx +9 -5
- package/src/styles/globals.css +39 -41
- package/src/ui/badge.tsx +8 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/progress.tsx +4 -0
- package/src/ui/rich-text-editor.tsx +10 -0
- package/src/ui/rich-text-toolbar.tsx +2 -1
- package/src/ui/toast.tsx +170 -0
- package/src/utils/debounce.ts +8 -2
- package/src/utils/pick-palette-color.ts +33 -0
- package/src/video/types.ts +16 -0
- package/src/video/video-player.tsx +13 -1
- package/dist/ForumBoard-CHXU3mjC.js +0 -2207
- package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
- package/dist/tabs-DRM2Iq_J.cjs +0 -172
|
@@ -5,9 +5,9 @@ import type { AchievementBadgeProps } from "./types";
|
|
|
5
5
|
|
|
6
6
|
const VARIANT_STYLES = {
|
|
7
7
|
default: "text-primary",
|
|
8
|
-
gold: "text-
|
|
9
|
-
silver: "text-
|
|
10
|
-
bronze: "text-
|
|
8
|
+
gold: "text-palette-3",
|
|
9
|
+
silver: "text-muted-foreground",
|
|
10
|
+
bronze: "text-palette-3/70",
|
|
11
11
|
} as const;
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -36,7 +36,15 @@ export function GradeIndicator({
|
|
|
36
36
|
if (variant === "linear") {
|
|
37
37
|
return (
|
|
38
38
|
<div className={cn("flex flex-row items-center gap-1", className)} style={style}>
|
|
39
|
-
<div
|
|
39
|
+
<div
|
|
40
|
+
className="flex-1 bg-muted rounded-full overflow-hidden"
|
|
41
|
+
style={{ height: TRACK_HEIGHTS[size] }}
|
|
42
|
+
role="progressbar"
|
|
43
|
+
aria-valuenow={Math.round(percentage)}
|
|
44
|
+
aria-valuemin={0}
|
|
45
|
+
aria-valuemax={100}
|
|
46
|
+
aria-label={`Grade: ${letterGrade ?? `${Math.round(percentage)}%`}`}
|
|
47
|
+
>
|
|
40
48
|
<div
|
|
41
49
|
className="h-full rounded-full transition-[width] duration-300 ease-in-out"
|
|
42
50
|
style={{ width: `${percentage}%`, background: color }}
|
|
@@ -21,7 +21,7 @@ export function ProgressRing({
|
|
|
21
21
|
className={cn("relative inline-flex", className)}
|
|
22
22
|
style={{ width: `${size}px`, height: `${size}px`, ...style }}
|
|
23
23
|
>
|
|
24
|
-
<svg width={size} height={size}>
|
|
24
|
+
<svg width={size} height={size} role="img" aria-label={label ?? `${Math.round(value)}% progress`}>
|
|
25
25
|
<circle
|
|
26
26
|
cx={center}
|
|
27
27
|
cy={center}
|
|
@@ -48,6 +48,7 @@ export function ProgressRing({
|
|
|
48
48
|
<span
|
|
49
49
|
className="absolute inset-0 flex items-center justify-center font-bold text-foreground"
|
|
50
50
|
style={{ fontSize: `${size * 0.2}px` }}
|
|
51
|
+
aria-hidden="true"
|
|
51
52
|
>
|
|
52
53
|
{label ?? `${Math.round(value)}%`}
|
|
53
54
|
</span>
|
|
@@ -18,6 +18,7 @@ export const StatCard = memo(function StatCard({
|
|
|
18
18
|
value,
|
|
19
19
|
subtitle,
|
|
20
20
|
trend,
|
|
21
|
+
accent,
|
|
21
22
|
className,
|
|
22
23
|
style,
|
|
23
24
|
}: StatCardProps) {
|
|
@@ -27,7 +28,13 @@ export const StatCard = memo(function StatCard({
|
|
|
27
28
|
<Card className={cn(className)} style={style}>
|
|
28
29
|
<CardContent className="p-4">
|
|
29
30
|
{icon && (
|
|
30
|
-
<div
|
|
31
|
+
<div
|
|
32
|
+
className="mb-2 w-9 h-9 rounded-lg flex items-center justify-center [&>svg]:size-5"
|
|
33
|
+
style={{
|
|
34
|
+
backgroundColor: `color-mix(in oklch, ${accent ?? "var(--primary)"} 10%, transparent)`,
|
|
35
|
+
color: accent ?? "var(--primary)",
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
31
38
|
{icon}
|
|
32
39
|
</div>
|
|
33
40
|
)}
|
package/src/progress/types.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface StatCardProps {
|
|
|
68
68
|
subtitle?: string;
|
|
69
69
|
/** Optional trend data */
|
|
70
70
|
trend?: { value: number; direction: "up" | "down" | "flat" };
|
|
71
|
+
/** Accent color CSS value for the icon background. Defaults to primary. */
|
|
72
|
+
accent?: string;
|
|
71
73
|
/** CSS class name for the root element */
|
|
72
74
|
className?: string;
|
|
73
75
|
/** Inline styles for the root element */
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import "../styles/globals.css";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
+
import { HydraLicenseProvider } from "../license/HydraContext";
|
|
3
4
|
|
|
4
5
|
export interface HydraProviderProps {
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
/** Controls color mode. Defaults to `"dark"`. Set to `"light"` to use the light theme. */
|
|
7
8
|
colorMode?: "light" | "dark";
|
|
9
|
+
/** HydraLMS Pro license key. Omit for free tier. */
|
|
10
|
+
licenseKey?: string;
|
|
11
|
+
/** Validation endpoint URL. When empty (default), all features are unlocked (dev mode). */
|
|
12
|
+
validateUrl?: string;
|
|
8
13
|
className?: string;
|
|
9
14
|
style?: React.CSSProperties;
|
|
10
15
|
}
|
|
@@ -12,15 +17,19 @@ export interface HydraProviderProps {
|
|
|
12
17
|
export function HydraProvider({
|
|
13
18
|
children,
|
|
14
19
|
colorMode = "dark",
|
|
20
|
+
licenseKey,
|
|
21
|
+
validateUrl,
|
|
15
22
|
className,
|
|
16
23
|
style,
|
|
17
24
|
}: HydraProviderProps) {
|
|
18
25
|
return (
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
<HydraLicenseProvider licenseKey={licenseKey} validateUrl={validateUrl}>
|
|
27
|
+
<div
|
|
28
|
+
className={`hydra-root${colorMode === "dark" ? " dark" : ""}${className ? ` ${className}` : ""}`}
|
|
29
|
+
style={style}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
</HydraLicenseProvider>
|
|
25
34
|
);
|
|
26
35
|
}
|
package/src/questions/choice.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
1
|
+
import { useState, useMemo, memo } from "react";
|
|
2
2
|
import type { QuestionProps } from "./types";
|
|
3
3
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
@@ -12,14 +12,14 @@ import { cn } from "../lib/utils";
|
|
|
12
12
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
13
13
|
* />
|
|
14
14
|
*/
|
|
15
|
-
export const Choice = ({
|
|
15
|
+
export const Choice = memo(function Choice({
|
|
16
16
|
question,
|
|
17
17
|
sessionAnswers,
|
|
18
18
|
onAnswer,
|
|
19
19
|
readOnly = false,
|
|
20
20
|
showCorrectAnswers = false,
|
|
21
21
|
disabled = false,
|
|
22
|
-
}: QuestionProps)
|
|
22
|
+
}: QuestionProps) {
|
|
23
23
|
const [selectedAnswer, setSelectedAnswer] = useState<string>(
|
|
24
24
|
() => sessionAnswers?.[0]?.answerUid || "",
|
|
25
25
|
);
|
|
@@ -53,7 +53,8 @@ export const Choice = ({
|
|
|
53
53
|
<div className="flex flex-col gap-4">
|
|
54
54
|
<div dangerouslySetInnerHTML={{ __html: question.content }} />
|
|
55
55
|
|
|
56
|
-
<
|
|
56
|
+
<fieldset className="flex flex-col gap-2 border-0 p-0 m-0">
|
|
57
|
+
<legend className="sr-only">Answer choices</legend>
|
|
57
58
|
{sortedAnswers.map((answer) => (
|
|
58
59
|
<div
|
|
59
60
|
key={answer.uid}
|
|
@@ -70,10 +71,16 @@ export const Choice = ({
|
|
|
70
71
|
className="accent-primary m-0 shrink-0"
|
|
71
72
|
/>
|
|
72
73
|
<span dangerouslySetInnerHTML={{ __html: answer.content }} />
|
|
74
|
+
{showCorrectAnswers && answer.isCorrect && (
|
|
75
|
+
<span className="sr-only">(Correct answer)</span>
|
|
76
|
+
)}
|
|
77
|
+
{showCorrectAnswers && selectedAnswer === answer.uid && !answer.isCorrect && (
|
|
78
|
+
<span className="sr-only">(Your answer — incorrect)</span>
|
|
79
|
+
)}
|
|
73
80
|
</label>
|
|
74
81
|
</div>
|
|
75
82
|
))}
|
|
76
|
-
</
|
|
83
|
+
</fieldset>
|
|
77
84
|
|
|
78
85
|
{showCorrectAnswers && question.explanation && (
|
|
79
86
|
<Alert className="mt-2">
|
|
@@ -85,4 +92,4 @@ export const Choice = ({
|
|
|
85
92
|
)}
|
|
86
93
|
</div>
|
|
87
94
|
);
|
|
88
|
-
};
|
|
95
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { HelpCircle, Brain, Lightbulb } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
/** A single confidence level option. */
|
|
7
|
+
export interface ConfidenceLevel {
|
|
8
|
+
/** Value stored in SessionAnswer.confidence */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Display label */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Optional icon */
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ConfidenceIndicator lets learners self-report their answer confidence,
|
|
18
|
+
* supporting metacognitive assessment strategies.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <ConfidenceIndicator
|
|
22
|
+
* value={answer.confidence}
|
|
23
|
+
* onChange={(level) => updateConfidence(level)}
|
|
24
|
+
* />
|
|
25
|
+
*/
|
|
26
|
+
export interface ConfidenceIndicatorProps {
|
|
27
|
+
/** Currently selected confidence level value */
|
|
28
|
+
value: string | null;
|
|
29
|
+
/** Called when the user selects a confidence level */
|
|
30
|
+
onChange: (level: string) => void;
|
|
31
|
+
/** Custom confidence levels (defaults to 3 levels) */
|
|
32
|
+
levels?: ConfidenceLevel[];
|
|
33
|
+
/** When true, disables interaction */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** When true, shows selected state but disables interaction */
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
/** CSS class name for the root element */
|
|
38
|
+
className?: string;
|
|
39
|
+
/** Inline styles for the root element */
|
|
40
|
+
style?: React.CSSProperties;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_LEVELS: ConfidenceLevel[] = [
|
|
44
|
+
{
|
|
45
|
+
value: "low",
|
|
46
|
+
label: "Guessing",
|
|
47
|
+
icon: <HelpCircle className="size-3.5" />,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: "medium",
|
|
51
|
+
label: "Somewhat sure",
|
|
52
|
+
icon: <Brain className="size-3.5" />,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: "high",
|
|
56
|
+
label: "Confident",
|
|
57
|
+
icon: <Lightbulb className="size-3.5" />,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const ConfidenceIndicator = memo(function ConfidenceIndicator({
|
|
62
|
+
value,
|
|
63
|
+
onChange,
|
|
64
|
+
levels = DEFAULT_LEVELS,
|
|
65
|
+
disabled = false,
|
|
66
|
+
readOnly = false,
|
|
67
|
+
className,
|
|
68
|
+
style,
|
|
69
|
+
}: ConfidenceIndicatorProps) {
|
|
70
|
+
const isDisabled = disabled || readOnly;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-slot="confidence-indicator"
|
|
75
|
+
className={cn("flex items-center gap-1", className)}
|
|
76
|
+
style={style}
|
|
77
|
+
role="radiogroup"
|
|
78
|
+
aria-label="Confidence level"
|
|
79
|
+
>
|
|
80
|
+
{levels.map((level) => {
|
|
81
|
+
const isSelected = value === level.value;
|
|
82
|
+
return (
|
|
83
|
+
<button
|
|
84
|
+
key={level.value}
|
|
85
|
+
type="button"
|
|
86
|
+
role="radio"
|
|
87
|
+
aria-checked={isSelected}
|
|
88
|
+
aria-label={level.label}
|
|
89
|
+
disabled={isDisabled}
|
|
90
|
+
onClick={() => onChange(level.value)}
|
|
91
|
+
className={cn(
|
|
92
|
+
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-all",
|
|
93
|
+
"border outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
94
|
+
isSelected
|
|
95
|
+
? "border-primary bg-primary/10 text-primary"
|
|
96
|
+
: "border-transparent bg-muted text-muted-foreground hover:bg-muted/80",
|
|
97
|
+
isDisabled && "opacity-50 pointer-events-none",
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
{level.icon}
|
|
101
|
+
{level.label}
|
|
102
|
+
</button>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
});
|
package/src/questions/essay.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useRef } from "react";
|
|
1
|
+
import { useState, useMemo, useRef, useEffect, memo } from "react";
|
|
2
2
|
import { debounce } from "../utils/debounce";
|
|
3
3
|
import { RichTextEditor } from "../ui/rich-text-editor";
|
|
4
4
|
import type { QuestionProps } from "./types";
|
|
@@ -12,13 +12,13 @@ import type { QuestionProps } from "./types";
|
|
|
12
12
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
13
13
|
* />
|
|
14
14
|
*/
|
|
15
|
-
export const Essay = ({
|
|
15
|
+
export const Essay = memo(function Essay({
|
|
16
16
|
question,
|
|
17
17
|
sessionAnswers,
|
|
18
18
|
onAnswer,
|
|
19
19
|
readOnly = false,
|
|
20
20
|
disabled = false,
|
|
21
|
-
}: QuestionProps)
|
|
21
|
+
}: QuestionProps) {
|
|
22
22
|
const [value, setValue] = useState(() => sessionAnswers?.[0]?.content || "");
|
|
23
23
|
|
|
24
24
|
const onAnswerRef = useRef(onAnswer);
|
|
@@ -33,6 +33,7 @@ export const Essay = ({
|
|
|
33
33
|
}, 500),
|
|
34
34
|
[],
|
|
35
35
|
);
|
|
36
|
+
useEffect(() => () => debouncedAnswer.cancel(), [debouncedAnswer]);
|
|
36
37
|
|
|
37
38
|
const handleChange = (html: string) => {
|
|
38
39
|
setValue(html);
|
|
@@ -51,7 +52,8 @@ export const Essay = ({
|
|
|
51
52
|
readOnly={readOnly}
|
|
52
53
|
disabled={disabled}
|
|
53
54
|
variant="default"
|
|
55
|
+
ariaLabel="Essay response"
|
|
54
56
|
/>
|
|
55
57
|
</div>
|
|
56
58
|
);
|
|
57
|
-
};
|
|
59
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useRef } from "react";
|
|
1
|
+
import { useState, useMemo, useRef, useId, useEffect, memo } from "react";
|
|
2
2
|
import { debounce } from "../utils/debounce";
|
|
3
3
|
import { Input } from "../ui/input";
|
|
4
4
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
@@ -13,14 +13,15 @@ import type { QuestionProps } from "./types";
|
|
|
13
13
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
14
14
|
* />
|
|
15
15
|
*/
|
|
16
|
-
export const FillInTheBlank = ({
|
|
16
|
+
export const FillInTheBlank = memo(function FillInTheBlank({
|
|
17
17
|
question,
|
|
18
18
|
sessionAnswers,
|
|
19
19
|
onAnswer,
|
|
20
20
|
readOnly = false,
|
|
21
21
|
showCorrectAnswers = false,
|
|
22
22
|
disabled = false,
|
|
23
|
-
}: QuestionProps)
|
|
23
|
+
}: QuestionProps) {
|
|
24
|
+
const inputId = useId();
|
|
24
25
|
const [value, setValue] = useState(() => sessionAnswers?.[0]?.content || "");
|
|
25
26
|
|
|
26
27
|
const onAnswerRef = useRef(onAnswer);
|
|
@@ -35,6 +36,7 @@ export const FillInTheBlank = ({
|
|
|
35
36
|
}, 300),
|
|
36
37
|
[],
|
|
37
38
|
);
|
|
39
|
+
useEffect(() => () => debouncedAnswer.cancel(), [debouncedAnswer]);
|
|
38
40
|
|
|
39
41
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
40
42
|
const newValue = e.target.value;
|
|
@@ -46,7 +48,9 @@ export const FillInTheBlank = ({
|
|
|
46
48
|
<div className="flex flex-col gap-2">
|
|
47
49
|
<div dangerouslySetInnerHTML={{ __html: question.content }} />
|
|
48
50
|
|
|
51
|
+
<label htmlFor={inputId} className="sr-only">Your answer</label>
|
|
49
52
|
<Input
|
|
53
|
+
id={inputId}
|
|
50
54
|
value={value}
|
|
51
55
|
onChange={handleChange}
|
|
52
56
|
placeholder="Type your answer here..."
|
|
@@ -63,4 +67,4 @@ export const FillInTheBlank = ({
|
|
|
63
67
|
)}
|
|
64
68
|
</div>
|
|
65
69
|
);
|
|
66
|
-
};
|
|
70
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
1
|
+
import { useState, useMemo, memo } from "react";
|
|
2
2
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
3
3
|
import { cn } from "../lib/utils";
|
|
4
4
|
import type { QuestionProps, HotspotRegion } from "./types";
|
|
@@ -43,14 +43,14 @@ function getRegionStyle(region: HotspotRegion): React.CSSProperties {
|
|
|
43
43
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
44
44
|
* />
|
|
45
45
|
*/
|
|
46
|
-
export const Hotspot = ({
|
|
46
|
+
export const Hotspot = memo(function Hotspot({
|
|
47
47
|
question,
|
|
48
48
|
sessionAnswers,
|
|
49
49
|
onAnswer,
|
|
50
50
|
readOnly = false,
|
|
51
51
|
showCorrectAnswers = false,
|
|
52
52
|
disabled = false,
|
|
53
|
-
}: QuestionProps)
|
|
53
|
+
}: QuestionProps) {
|
|
54
54
|
const multiSelect = question.hotspotMultiSelect ?? false;
|
|
55
55
|
|
|
56
56
|
const [selected, setSelected] = useState<Set<string>>(() => {
|
|
@@ -151,4 +151,4 @@ export const Hotspot = ({
|
|
|
151
151
|
)}
|
|
152
152
|
</div>
|
|
153
153
|
);
|
|
154
|
-
};
|
|
154
|
+
});
|
package/src/questions/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { InlineChoice } from "./inline-choice";
|
|
|
12
12
|
export { Scenario } from "./scenario";
|
|
13
13
|
export { Spreadsheet } from "./spreadsheet";
|
|
14
14
|
export { scoreQuestion, scoreScenarioSubQuestions } from "./scoring";
|
|
15
|
+
export { ConfidenceIndicator } from "./confidence-indicator";
|
|
16
|
+
export type { ConfidenceIndicatorProps, ConfidenceLevel } from "./confidence-indicator";
|
|
15
17
|
|
|
16
18
|
export type {
|
|
17
19
|
QuestionProps,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
1
|
+
import { useState, useMemo, memo } from "react";
|
|
2
2
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
3
3
|
import { cn } from "../lib/utils";
|
|
4
4
|
import type { QuestionProps, InlineBlank } from "./types";
|
|
@@ -44,14 +44,14 @@ function parseInlineContent(html: string): ContentPart[] {
|
|
|
44
44
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
45
45
|
* />
|
|
46
46
|
*/
|
|
47
|
-
export const InlineChoice = ({
|
|
47
|
+
export const InlineChoice = memo(function InlineChoice({
|
|
48
48
|
question,
|
|
49
49
|
sessionAnswers,
|
|
50
50
|
onAnswer,
|
|
51
51
|
readOnly = false,
|
|
52
52
|
showCorrectAnswers = false,
|
|
53
53
|
disabled = false,
|
|
54
|
-
}: QuestionProps)
|
|
54
|
+
}: QuestionProps) {
|
|
55
55
|
const blanksMap = useMemo(() => {
|
|
56
56
|
const map = new Map<string, InlineBlank>();
|
|
57
57
|
for (const blank of question.inlineBlanks ?? []) {
|
|
@@ -118,6 +118,7 @@ export const InlineChoice = ({
|
|
|
118
118
|
return (
|
|
119
119
|
<select
|
|
120
120
|
key={part.uid}
|
|
121
|
+
aria-label={`Blank ${blank.sequence + 1}`}
|
|
121
122
|
value={selections.get(part.uid) || ""}
|
|
122
123
|
onChange={(e) => handleSelect(part.uid, e.target.value)}
|
|
123
124
|
disabled={readOnly || disabled}
|
|
@@ -148,4 +149,4 @@ export const InlineChoice = ({
|
|
|
148
149
|
)}
|
|
149
150
|
</div>
|
|
150
151
|
);
|
|
151
|
-
};
|
|
152
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useCallback } from "react";
|
|
1
|
+
import { useState, useMemo, useCallback, memo } from "react";
|
|
2
2
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
3
3
|
import { cn } from "../lib/utils";
|
|
4
4
|
import type { QuestionProps } from "./types";
|
|
@@ -23,14 +23,14 @@ import type { QuestionProps } from "./types";
|
|
|
23
23
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
24
24
|
* />
|
|
25
25
|
*/
|
|
26
|
-
export const Matching = ({
|
|
26
|
+
export const Matching = memo(function Matching({
|
|
27
27
|
question,
|
|
28
28
|
sessionAnswers,
|
|
29
29
|
onAnswer,
|
|
30
30
|
readOnly = false,
|
|
31
31
|
showCorrectAnswers = false,
|
|
32
32
|
disabled = false,
|
|
33
|
-
}: QuestionProps)
|
|
33
|
+
}: QuestionProps) {
|
|
34
34
|
const sortedPairs = useMemo(
|
|
35
35
|
() =>
|
|
36
36
|
[...(question.matchingPairs ?? [])].sort(
|
|
@@ -158,6 +158,7 @@ export const Matching = ({
|
|
|
158
158
|
dangerouslySetInnerHTML={{ __html: pair.item }}
|
|
159
159
|
/>
|
|
160
160
|
<select
|
|
161
|
+
aria-label={`Match for: ${pair.item.replace(/<[^>]+>/g, "")}`}
|
|
161
162
|
value={matches.get(pair.uid) || ""}
|
|
162
163
|
onChange={(e) => handleSelect(pair.uid, e.target.value)}
|
|
163
164
|
disabled={readOnly || disabled}
|
|
@@ -225,4 +226,4 @@ export const Matching = ({
|
|
|
225
226
|
)}
|
|
226
227
|
</div>
|
|
227
228
|
);
|
|
228
|
-
};
|
|
229
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
1
|
+
import { useState, useMemo, memo } from "react";
|
|
2
2
|
import type { QuestionProps } from "./types";
|
|
3
3
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
@@ -12,14 +12,14 @@ import { cn } from "../lib/utils";
|
|
|
12
12
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
13
13
|
* />
|
|
14
14
|
*/
|
|
15
|
-
export const MultipleChoice = ({
|
|
15
|
+
export const MultipleChoice = memo(function MultipleChoice({
|
|
16
16
|
question,
|
|
17
17
|
sessionAnswers,
|
|
18
18
|
onAnswer,
|
|
19
19
|
readOnly = false,
|
|
20
20
|
showCorrectAnswers = false,
|
|
21
21
|
disabled = false,
|
|
22
|
-
}: QuestionProps)
|
|
22
|
+
}: QuestionProps) {
|
|
23
23
|
const [selectedAnswers, setSelectedAnswers] = useState<string[]>(
|
|
24
24
|
() => sessionAnswers?.map((sa) => sa.answerUid) || [],
|
|
25
25
|
);
|
|
@@ -59,7 +59,8 @@ export const MultipleChoice = ({
|
|
|
59
59
|
<div className="flex flex-col gap-4">
|
|
60
60
|
<div dangerouslySetInnerHTML={{ __html: question.content }} />
|
|
61
61
|
|
|
62
|
-
<
|
|
62
|
+
<fieldset className="flex flex-col gap-2 border-0 p-0 m-0">
|
|
63
|
+
<legend className="sr-only">Select all correct answers</legend>
|
|
63
64
|
{sortedAnswers.map((answer) => (
|
|
64
65
|
<div
|
|
65
66
|
key={answer.uid}
|
|
@@ -77,10 +78,16 @@ export const MultipleChoice = ({
|
|
|
77
78
|
className="text-sm"
|
|
78
79
|
dangerouslySetInnerHTML={{ __html: answer.content }}
|
|
79
80
|
/>
|
|
81
|
+
{showCorrectAnswers && answer.isCorrect && (
|
|
82
|
+
<span className="sr-only">(Correct answer)</span>
|
|
83
|
+
)}
|
|
84
|
+
{showCorrectAnswers && selectedAnswers.includes(answer.uid) && !answer.isCorrect && (
|
|
85
|
+
<span className="sr-only">(Your answer — incorrect)</span>
|
|
86
|
+
)}
|
|
80
87
|
</label>
|
|
81
88
|
</div>
|
|
82
89
|
))}
|
|
83
|
-
</
|
|
90
|
+
</fieldset>
|
|
84
91
|
|
|
85
92
|
{showCorrectAnswers && question.explanation && (
|
|
86
93
|
<Alert className="mt-2">
|
|
@@ -92,4 +99,4 @@ export const MultipleChoice = ({
|
|
|
92
99
|
)}
|
|
93
100
|
</div>
|
|
94
101
|
);
|
|
95
|
-
};
|
|
102
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useRef } from "react";
|
|
1
|
+
import { useState, useMemo, useRef, useId, useEffect, memo } from "react";
|
|
2
2
|
import { debounce } from "../utils/debounce";
|
|
3
3
|
import { Input } from "../ui/input";
|
|
4
4
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
@@ -14,14 +14,15 @@ import type { QuestionProps } from "./types";
|
|
|
14
14
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
15
15
|
* />
|
|
16
16
|
*/
|
|
17
|
-
export const Numeric = ({
|
|
17
|
+
export const Numeric = memo(function Numeric({
|
|
18
18
|
question,
|
|
19
19
|
sessionAnswers,
|
|
20
20
|
onAnswer,
|
|
21
21
|
readOnly = false,
|
|
22
22
|
showCorrectAnswers = false,
|
|
23
23
|
disabled = false,
|
|
24
|
-
}: QuestionProps)
|
|
24
|
+
}: QuestionProps) {
|
|
25
|
+
const inputId = useId();
|
|
25
26
|
const [value, setValue] = useState(() => sessionAnswers?.[0]?.content || "");
|
|
26
27
|
|
|
27
28
|
const onAnswerRef = useRef(onAnswer);
|
|
@@ -36,6 +37,7 @@ export const Numeric = ({
|
|
|
36
37
|
}, 300),
|
|
37
38
|
[],
|
|
38
39
|
);
|
|
40
|
+
useEffect(() => () => debouncedAnswer.cancel(), [debouncedAnswer]);
|
|
39
41
|
|
|
40
42
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
41
43
|
const newValue = e.target.value;
|
|
@@ -64,7 +66,9 @@ export const Numeric = ({
|
|
|
64
66
|
<div dangerouslySetInnerHTML={{ __html: question.content }} />
|
|
65
67
|
|
|
66
68
|
<div className="flex items-center gap-2">
|
|
69
|
+
<label htmlFor={inputId} className="sr-only">Numeric answer</label>
|
|
67
70
|
<Input
|
|
71
|
+
id={inputId}
|
|
68
72
|
type="number"
|
|
69
73
|
value={value}
|
|
70
74
|
onChange={handleChange}
|
|
@@ -99,4 +103,4 @@ export const Numeric = ({
|
|
|
99
103
|
)}
|
|
100
104
|
</div>
|
|
101
105
|
);
|
|
102
|
-
};
|
|
106
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
1
|
+
import { useState, useMemo, memo } from "react";
|
|
2
2
|
import { ChevronUp, ChevronDown, GripVertical } from "lucide-react";
|
|
3
3
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
@@ -15,20 +15,22 @@ import type { QuestionProps, AnswerOption } from "./types";
|
|
|
15
15
|
* onAnswer={(answers) => handleAnswer(answers)}
|
|
16
16
|
* />
|
|
17
17
|
*/
|
|
18
|
-
export const Ordering = ({
|
|
18
|
+
export const Ordering = memo(function Ordering({
|
|
19
19
|
question,
|
|
20
20
|
sessionAnswers,
|
|
21
21
|
onAnswer,
|
|
22
22
|
readOnly = false,
|
|
23
23
|
showCorrectAnswers = false,
|
|
24
24
|
disabled = false,
|
|
25
|
-
}: QuestionProps)
|
|
25
|
+
}: QuestionProps) {
|
|
26
26
|
const correctOrder = useMemo(
|
|
27
27
|
() =>
|
|
28
28
|
[...(question.answers || [])].sort((a, b) => a.sequence - b.sequence),
|
|
29
29
|
[question.answers],
|
|
30
30
|
);
|
|
31
31
|
|
|
32
|
+
const [announcement, setAnnouncement] = useState("");
|
|
33
|
+
|
|
32
34
|
const [items, setItems] = useState<AnswerOption[]>(() => {
|
|
33
35
|
if (sessionAnswers && sessionAnswers.length > 0) {
|
|
34
36
|
// Rebuild order from sessionAnswers position indices
|
|
@@ -66,6 +68,9 @@ export const Ordering = ({
|
|
|
66
68
|
next.splice(toIndex, 0, moved);
|
|
67
69
|
setItems(next);
|
|
68
70
|
emitAnswer(next);
|
|
71
|
+
setAnnouncement(
|
|
72
|
+
`${moved.content.replace(/<[^>]+>/g, "")} moved to position ${toIndex + 1} of ${items.length}`,
|
|
73
|
+
);
|
|
69
74
|
};
|
|
70
75
|
|
|
71
76
|
const { getDragProps, dragIndex, dragOverIndex } = useDragReorder({
|
|
@@ -85,6 +90,9 @@ export const Ordering = ({
|
|
|
85
90
|
return (
|
|
86
91
|
<div className="flex flex-col gap-4">
|
|
87
92
|
<div dangerouslySetInnerHTML={{ __html: question.content }} />
|
|
93
|
+
<span className="sr-only" aria-live="assertive" role="status">
|
|
94
|
+
{announcement}
|
|
95
|
+
</span>
|
|
88
96
|
|
|
89
97
|
<div className="flex flex-col gap-1.5">
|
|
90
98
|
{items.map((item, index) => (
|
|
@@ -156,4 +164,4 @@ export const Ordering = ({
|
|
|
156
164
|
)}
|
|
157
165
|
</div>
|
|
158
166
|
);
|
|
159
|
-
};
|
|
167
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
1
2
|
import type { QuestionProps } from "./types";
|
|
2
3
|
import { MultipleChoice } from "./multiple-choice";
|
|
3
4
|
import { Choice } from "./choice";
|
|
@@ -22,7 +23,7 @@ import { Spreadsheet } from "./spreadsheet";
|
|
|
22
23
|
* onAnswer={handleAnswer}
|
|
23
24
|
* />
|
|
24
25
|
*/
|
|
25
|
-
export const QuestionRenderer = (props: QuestionProps)
|
|
26
|
+
export const QuestionRenderer = memo(function QuestionRenderer(props: QuestionProps) {
|
|
26
27
|
switch (props.question.type) {
|
|
27
28
|
case "multiple_choice":
|
|
28
29
|
return <MultipleChoice {...props} />;
|
|
@@ -55,4 +56,4 @@ export const QuestionRenderer = (props: QuestionProps) => {
|
|
|
55
56
|
</p>
|
|
56
57
|
);
|
|
57
58
|
}
|
|
58
|
-
};
|
|
59
|
+
});
|