@hydralms/components 0.2.0 → 0.3.1
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-BPsZBaJj.cjs +1 -0
- package/dist/StudentProfile-Cw2p-RZn.js +3273 -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 +495 -439
- 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 +6 -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 +1267 -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/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
- package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
- package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
- package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -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 +6 -0
- package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
- package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
- package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
- package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -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 +10 -0
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +1361 -307
- 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-BJdu1T9Y.cjs +2 -0
- package/dist/withProGate-BvFc7Jwy.js +4975 -0
- package/package.json +57 -226
- 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 +34 -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 +39 -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 +14 -2
- 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/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
- package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
- package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
- package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
- package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
- package/src/sections/AdaptiveLearningPath/types.ts +159 -0
- 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/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
- package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
- package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
- package/src/sections/ContentAuthoringStudio/types.ts +67 -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 +67 -7
- 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 +34 -6
- 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 +40 -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 +27 -6
- package/dist/ForumBoard-CHXU3mjC.js +0 -2207
- package/dist/ForumBoard-d1w5-r6n.cjs +0 -1
- package/dist/tabs-DRM2Iq_J.cjs +0 -172
- package/dist/tabs-Wf3h_Cx3.js +0 -21580
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { memo, useCallback, useRef, useState } from "react";
|
|
2
|
+
import { Play, Pause, Volume2, VolumeX } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
import { formatTimer } from "../utils/format-duration";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AudioPlayer provides an HTML5 audio player with custom controls
|
|
9
|
+
* including play/pause, seek, speed adjustment, and mute toggle.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <AudioPlayer src="/audio/lecture-1.mp3" title="Lecture 1: Introduction" />
|
|
13
|
+
*/
|
|
14
|
+
export interface AudioPlayerProps {
|
|
15
|
+
/** Audio source URL */
|
|
16
|
+
src: string;
|
|
17
|
+
/** Optional title displayed above the player */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Called when playback ends */
|
|
20
|
+
onEnded?: () => void;
|
|
21
|
+
/** Called on time update with current time and duration */
|
|
22
|
+
onTimeUpdate?: (currentTime: number, duration: number) => void;
|
|
23
|
+
/** CSS class name for the root element */
|
|
24
|
+
className?: string;
|
|
25
|
+
/** Inline styles for the root element */
|
|
26
|
+
style?: React.CSSProperties;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
30
|
+
|
|
31
|
+
export const AudioPlayer = memo(function AudioPlayer({
|
|
32
|
+
src,
|
|
33
|
+
title,
|
|
34
|
+
onEnded,
|
|
35
|
+
onTimeUpdate,
|
|
36
|
+
className,
|
|
37
|
+
style,
|
|
38
|
+
}: AudioPlayerProps) {
|
|
39
|
+
const audioRef = useRef<HTMLAudioElement>(null);
|
|
40
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
41
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
42
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
43
|
+
const [duration, setDuration] = useState(0);
|
|
44
|
+
const [speed, setSpeed] = useState(1);
|
|
45
|
+
const lastReportedTime = useRef(-1);
|
|
46
|
+
const onTimeUpdateRef = useRef(onTimeUpdate);
|
|
47
|
+
onTimeUpdateRef.current = onTimeUpdate;
|
|
48
|
+
|
|
49
|
+
const togglePlay = useCallback(() => {
|
|
50
|
+
const audio = audioRef.current;
|
|
51
|
+
if (!audio) return;
|
|
52
|
+
if (audio.paused) {
|
|
53
|
+
audio.play();
|
|
54
|
+
} else {
|
|
55
|
+
audio.pause();
|
|
56
|
+
}
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const toggleMute = useCallback(() => {
|
|
60
|
+
const audio = audioRef.current;
|
|
61
|
+
if (!audio) return;
|
|
62
|
+
audio.muted = !audio.muted;
|
|
63
|
+
setIsMuted((prev) => !prev);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const cycleSpeed = useCallback(() => {
|
|
67
|
+
const audio = audioRef.current;
|
|
68
|
+
if (!audio) return;
|
|
69
|
+
setSpeed((prev) => {
|
|
70
|
+
const idx = SPEEDS.indexOf(prev);
|
|
71
|
+
const next = SPEEDS[(idx + 1) % SPEEDS.length];
|
|
72
|
+
audio.playbackRate = next;
|
|
73
|
+
return next;
|
|
74
|
+
});
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const handleSeek = useCallback(
|
|
78
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
79
|
+
const audio = audioRef.current;
|
|
80
|
+
if (!audio) return;
|
|
81
|
+
const time = Number(e.target.value);
|
|
82
|
+
audio.currentTime = time;
|
|
83
|
+
lastReportedTime.current = Math.floor(time * 4) / 4;
|
|
84
|
+
setCurrentTime(time);
|
|
85
|
+
},
|
|
86
|
+
[],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const handleTimeUpdate = useCallback(() => {
|
|
90
|
+
const audio = audioRef.current;
|
|
91
|
+
if (!audio) return;
|
|
92
|
+
const rounded = Math.floor(audio.currentTime * 4) / 4;
|
|
93
|
+
if (rounded !== lastReportedTime.current) {
|
|
94
|
+
lastReportedTime.current = rounded;
|
|
95
|
+
setCurrentTime(audio.currentTime);
|
|
96
|
+
onTimeUpdateRef.current?.(audio.currentTime, audio.duration);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const handleLoadedMetadata = useCallback(() => {
|
|
101
|
+
const audio = audioRef.current;
|
|
102
|
+
if (audio && !isNaN(audio.duration)) setDuration(audio.duration);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
data-slot="audio-player"
|
|
110
|
+
className={cn("rounded-lg border bg-card p-4 space-y-3", className)}
|
|
111
|
+
style={style}
|
|
112
|
+
>
|
|
113
|
+
{title && (
|
|
114
|
+
<p className="text-sm font-medium text-foreground">{title}</p>
|
|
115
|
+
)}
|
|
116
|
+
<audio
|
|
117
|
+
ref={audioRef}
|
|
118
|
+
src={src}
|
|
119
|
+
onPlay={() => setIsPlaying(true)}
|
|
120
|
+
onPause={() => setIsPlaying(false)}
|
|
121
|
+
onEnded={() => {
|
|
122
|
+
setIsPlaying(false);
|
|
123
|
+
onEnded?.();
|
|
124
|
+
}}
|
|
125
|
+
onTimeUpdate={handleTimeUpdate}
|
|
126
|
+
onLoadedMetadata={handleLoadedMetadata}
|
|
127
|
+
preload="metadata"
|
|
128
|
+
/>
|
|
129
|
+
<div className="flex items-center gap-3">
|
|
130
|
+
<Button
|
|
131
|
+
variant="ghost"
|
|
132
|
+
size="icon-sm"
|
|
133
|
+
onClick={togglePlay}
|
|
134
|
+
aria-label={isPlaying ? "Pause" : "Play"}
|
|
135
|
+
>
|
|
136
|
+
{isPlaying ? (
|
|
137
|
+
<Pause className="size-4" />
|
|
138
|
+
) : (
|
|
139
|
+
<Play className="size-4" />
|
|
140
|
+
)}
|
|
141
|
+
</Button>
|
|
142
|
+
|
|
143
|
+
<span className="text-xs tabular-nums text-muted-foreground w-12 shrink-0">
|
|
144
|
+
{formatTimer(Math.floor(currentTime))}
|
|
145
|
+
</span>
|
|
146
|
+
|
|
147
|
+
<div className="relative flex-1 h-5 flex items-center">
|
|
148
|
+
<div className="absolute inset-y-0 left-0 flex items-center w-full">
|
|
149
|
+
<div className="w-full h-1 rounded-full bg-muted overflow-hidden">
|
|
150
|
+
<div
|
|
151
|
+
className="h-full bg-primary rounded-full transition-[width] duration-100"
|
|
152
|
+
style={{ width: `${progress}%` }}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<input
|
|
157
|
+
type="range"
|
|
158
|
+
min={0}
|
|
159
|
+
max={duration || 0}
|
|
160
|
+
step={0.1}
|
|
161
|
+
value={currentTime}
|
|
162
|
+
onChange={handleSeek}
|
|
163
|
+
className="absolute inset-0 w-full opacity-0 cursor-pointer"
|
|
164
|
+
aria-label="Seek"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<span className="text-xs tabular-nums text-muted-foreground w-12 shrink-0 text-right">
|
|
169
|
+
{formatTimer(Math.floor(duration))}
|
|
170
|
+
</span>
|
|
171
|
+
|
|
172
|
+
<Button
|
|
173
|
+
variant="ghost"
|
|
174
|
+
size="icon-xs"
|
|
175
|
+
onClick={toggleMute}
|
|
176
|
+
aria-label={isMuted ? "Unmute" : "Mute"}
|
|
177
|
+
>
|
|
178
|
+
{isMuted ? (
|
|
179
|
+
<VolumeX className="size-3.5" />
|
|
180
|
+
) : (
|
|
181
|
+
<Volume2 className="size-3.5" />
|
|
182
|
+
)}
|
|
183
|
+
</Button>
|
|
184
|
+
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
onClick={cycleSpeed}
|
|
188
|
+
className="text-xs font-medium text-muted-foreground hover:text-foreground transition-colors tabular-nums w-8 text-center"
|
|
189
|
+
aria-label={`Playback speed ${speed}x`}
|
|
190
|
+
>
|
|
191
|
+
{speed}x
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Copy, Check, FileCode } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CodeBlock renders source code with optional line numbers, a filename header,
|
|
8
|
+
* and a copy-to-clipboard button. Does not bundle a syntax highlighter —
|
|
9
|
+
* consumers can apply their own (Prism, Shiki, etc.) via the `language-*` class on the `<code>` element.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <CodeBlock
|
|
13
|
+
* code="console.log('hello');"
|
|
14
|
+
* language="javascript"
|
|
15
|
+
* filename="example.js"
|
|
16
|
+
* showLineNumbers
|
|
17
|
+
* />
|
|
18
|
+
*/
|
|
19
|
+
export interface CodeBlockProps {
|
|
20
|
+
/** The code content to display */
|
|
21
|
+
code: string;
|
|
22
|
+
/** Programming language identifier (applied as `language-*` class on `<code>`) */
|
|
23
|
+
language?: string;
|
|
24
|
+
/** Optional filename shown in the header bar */
|
|
25
|
+
filename?: string;
|
|
26
|
+
/** Whether to display line numbers */
|
|
27
|
+
showLineNumbers?: boolean;
|
|
28
|
+
/** Called after code is copied to clipboard */
|
|
29
|
+
onCopy?: () => void;
|
|
30
|
+
/** CSS class name for the root element */
|
|
31
|
+
className?: string;
|
|
32
|
+
/** Inline styles for the root element */
|
|
33
|
+
style?: React.CSSProperties;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const CodeBlock = memo(function CodeBlock({
|
|
37
|
+
code,
|
|
38
|
+
language,
|
|
39
|
+
filename,
|
|
40
|
+
showLineNumbers = false,
|
|
41
|
+
onCopy,
|
|
42
|
+
className,
|
|
43
|
+
style,
|
|
44
|
+
}: CodeBlockProps) {
|
|
45
|
+
const [copied, setCopied] = useState(false);
|
|
46
|
+
const hasHeader = !!(filename || language);
|
|
47
|
+
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
48
|
+
useEffect(() => () => clearTimeout(copyTimerRef.current), []);
|
|
49
|
+
|
|
50
|
+
const handleCopy = useCallback(async () => {
|
|
51
|
+
try {
|
|
52
|
+
await navigator.clipboard.writeText(code);
|
|
53
|
+
setCopied(true);
|
|
54
|
+
onCopy?.();
|
|
55
|
+
clearTimeout(copyTimerRef.current);
|
|
56
|
+
copyTimerRef.current = setTimeout(() => setCopied(false), 2000);
|
|
57
|
+
} catch {
|
|
58
|
+
// Clipboard API not available
|
|
59
|
+
}
|
|
60
|
+
}, [code, onCopy]);
|
|
61
|
+
|
|
62
|
+
const lines = code.split("\n");
|
|
63
|
+
|
|
64
|
+
const copyButton = (
|
|
65
|
+
<Button
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="icon-xs"
|
|
68
|
+
onClick={handleCopy}
|
|
69
|
+
aria-label={copied ? "Copied" : "Copy code"}
|
|
70
|
+
>
|
|
71
|
+
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
|
72
|
+
</Button>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="code-block"
|
|
78
|
+
className={cn(
|
|
79
|
+
"relative rounded-lg border bg-card overflow-hidden text-sm",
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
style={style}
|
|
83
|
+
>
|
|
84
|
+
{hasHeader ? (
|
|
85
|
+
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50">
|
|
86
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
87
|
+
<FileCode className="size-3.5" />
|
|
88
|
+
<span>{filename ?? language}</span>
|
|
89
|
+
</div>
|
|
90
|
+
{copyButton}
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
<div className="absolute top-2 right-2 z-10">{copyButton}</div>
|
|
94
|
+
)}
|
|
95
|
+
<div className="overflow-x-auto">
|
|
96
|
+
<pre className="p-4 m-0">
|
|
97
|
+
<code className={language ? `language-${language}` : undefined}>
|
|
98
|
+
{showLineNumbers
|
|
99
|
+
? lines.map((line, i) => (
|
|
100
|
+
<div key={i} className="flex">
|
|
101
|
+
<span className="select-none text-muted-foreground/50 w-8 shrink-0 text-right pr-4 tabular-nums">
|
|
102
|
+
{i + 1}
|
|
103
|
+
</span>
|
|
104
|
+
<span>{line}</span>
|
|
105
|
+
</div>
|
|
106
|
+
))
|
|
107
|
+
: code}
|
|
108
|
+
</code>
|
|
109
|
+
</pre>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
});
|
|
@@ -5,6 +5,19 @@ import { QuestionRenderer } from "../questions";
|
|
|
5
5
|
import { FlashcardDeck } from "../flashcards";
|
|
6
6
|
import { Alert, AlertDescription } from "../ui/alert";
|
|
7
7
|
import { Separator } from "../ui/separator";
|
|
8
|
+
import {
|
|
9
|
+
Table,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableRow,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableCell,
|
|
15
|
+
TableCaption,
|
|
16
|
+
} from "../ui/table";
|
|
17
|
+
import { AudioPlayer } from "./audio-player";
|
|
18
|
+
import { CodeBlock } from "./code-block";
|
|
19
|
+
import { EmbedBlock } from "./embed-block";
|
|
20
|
+
import { AttachmentList } from "./attachment-list";
|
|
8
21
|
import type { ContentBlockProps } from "./types";
|
|
9
22
|
import { cn } from "../lib/utils";
|
|
10
23
|
|
|
@@ -106,6 +119,57 @@ export const ContentBlock = memo(function ContentBlock({
|
|
|
106
119
|
/>
|
|
107
120
|
);
|
|
108
121
|
|
|
122
|
+
case "audio":
|
|
123
|
+
return wrapper(
|
|
124
|
+
<AudioPlayer src={block.src} title={block.title} />
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
case "code":
|
|
128
|
+
return wrapper(
|
|
129
|
+
<CodeBlock
|
|
130
|
+
code={block.code}
|
|
131
|
+
language={block.language}
|
|
132
|
+
filename={block.filename}
|
|
133
|
+
showLineNumbers={block.showLineNumbers}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
case "embed":
|
|
138
|
+
return wrapper(
|
|
139
|
+
<EmbedBlock
|
|
140
|
+
src={block.src}
|
|
141
|
+
title={block.title}
|
|
142
|
+
aspectRatio={block.aspectRatio}
|
|
143
|
+
allowFullscreen={block.allowFullscreen}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
case "table":
|
|
148
|
+
return wrapper(
|
|
149
|
+
<Table>
|
|
150
|
+
{block.caption && <TableCaption>{block.caption}</TableCaption>}
|
|
151
|
+
<TableHeader>
|
|
152
|
+
<TableRow>
|
|
153
|
+
{block.headers.map((header, i) => (
|
|
154
|
+
<TableHead key={i}>{header}</TableHead>
|
|
155
|
+
))}
|
|
156
|
+
</TableRow>
|
|
157
|
+
</TableHeader>
|
|
158
|
+
<TableBody>
|
|
159
|
+
{block.rows.map((row, i) => (
|
|
160
|
+
<TableRow key={i}>
|
|
161
|
+
{row.map((cell, j) => (
|
|
162
|
+
<TableCell key={j}>{cell}</TableCell>
|
|
163
|
+
))}
|
|
164
|
+
</TableRow>
|
|
165
|
+
))}
|
|
166
|
+
</TableBody>
|
|
167
|
+
</Table>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
case "file":
|
|
171
|
+
return wrapper(<AttachmentList files={block.files} readOnly />);
|
|
172
|
+
|
|
109
173
|
case "divider":
|
|
110
174
|
return wrapper(<Separator />);
|
|
111
175
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { memo, useState } from "react";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { Skeleton } from "../ui/skeleton";
|
|
5
|
+
|
|
6
|
+
export type EmbedAspectRatio = "16/9" | "4/3" | "1/1";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EmbedBlock renders a responsive iframe wrapper with an optional title bar
|
|
10
|
+
* and loading skeleton. Ideal for embedding YouTube videos, SCORM objects,
|
|
11
|
+
* Google Slides, and other external content.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <EmbedBlock
|
|
15
|
+
* src="https://www.youtube.com/embed/dQw4w9WgXcQ"
|
|
16
|
+
* title="Introduction Video"
|
|
17
|
+
* aspectRatio="16/9"
|
|
18
|
+
* />
|
|
19
|
+
*/
|
|
20
|
+
export interface EmbedBlockProps {
|
|
21
|
+
/** iframe source URL */
|
|
22
|
+
src: string;
|
|
23
|
+
/** Optional title for the embed */
|
|
24
|
+
title?: string;
|
|
25
|
+
/** Aspect ratio of the embed container */
|
|
26
|
+
aspectRatio?: EmbedAspectRatio;
|
|
27
|
+
/** Whether to allow fullscreen */
|
|
28
|
+
allowFullscreen?: boolean;
|
|
29
|
+
/** CSS class name for the root element */
|
|
30
|
+
className?: string;
|
|
31
|
+
/** Inline styles for the root element */
|
|
32
|
+
style?: React.CSSProperties;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ASPECT_CLASSES: Record<EmbedAspectRatio, string> = {
|
|
36
|
+
"16/9": "aspect-video",
|
|
37
|
+
"4/3": "aspect-[4/3]",
|
|
38
|
+
"1/1": "aspect-square",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const EmbedBlock = memo(function EmbedBlock({
|
|
42
|
+
src,
|
|
43
|
+
title,
|
|
44
|
+
aspectRatio = "16/9",
|
|
45
|
+
allowFullscreen = true,
|
|
46
|
+
className,
|
|
47
|
+
style,
|
|
48
|
+
}: EmbedBlockProps) {
|
|
49
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="embed-block"
|
|
54
|
+
className={cn("rounded-lg border bg-card overflow-hidden", className)}
|
|
55
|
+
style={style}
|
|
56
|
+
>
|
|
57
|
+
{title && (
|
|
58
|
+
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50">
|
|
59
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
60
|
+
<ExternalLink className="size-3.5" />
|
|
61
|
+
<span>{title}</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
<div className={cn("relative w-full", ASPECT_CLASSES[aspectRatio])}>
|
|
66
|
+
{!isLoaded && <Skeleton className="absolute inset-0 rounded-none" />}
|
|
67
|
+
<iframe
|
|
68
|
+
src={src}
|
|
69
|
+
title={title ?? "Embedded content"}
|
|
70
|
+
allowFullScreen={allowFullscreen}
|
|
71
|
+
onLoad={() => setIsLoaded(true)}
|
|
72
|
+
className="absolute inset-0 w-full h-full border-0"
|
|
73
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-presentation"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -39,7 +39,17 @@ export function FileUploadZone({
|
|
|
39
39
|
isDragging && "border-primary bg-muted",
|
|
40
40
|
disabled && "cursor-default opacity-50",
|
|
41
41
|
atLimit && "cursor-default",
|
|
42
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
42
43
|
)}
|
|
44
|
+
role="button"
|
|
45
|
+
tabIndex={disabled || atLimit ? -1 : 0}
|
|
46
|
+
aria-label={atLimit ? `Maximum ${maxFiles} files reached` : label}
|
|
47
|
+
onKeyDown={(e) => {
|
|
48
|
+
if ((e.key === "Enter" || e.key === " ") && !disabled && !atLimit) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
inputRef.current?.click();
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
43
53
|
onDragOver={(e) => {
|
|
44
54
|
e.preventDefault();
|
|
45
55
|
if (!disabled && !atLimit) setIsDragging(true);
|
package/src/content/index.ts
CHANGED
|
@@ -8,3 +8,9 @@ export type {
|
|
|
8
8
|
AttachmentListProps,
|
|
9
9
|
AttachmentFile,
|
|
10
10
|
} from "./types";
|
|
11
|
+
export { AudioPlayer } from "./audio-player";
|
|
12
|
+
export type { AudioPlayerProps } from "./audio-player";
|
|
13
|
+
export { CodeBlock } from "./code-block";
|
|
14
|
+
export type { CodeBlockProps } from "./code-block";
|
|
15
|
+
export { EmbedBlock } from "./embed-block";
|
|
16
|
+
export type { EmbedBlockProps, EmbedAspectRatio } from "./embed-block";
|
package/src/content/types.ts
CHANGED
|
@@ -14,6 +14,11 @@ export type LessonBlock =
|
|
|
14
14
|
| { type: "callout"; content: string; variant?: "info" | "warning" | "tip" }
|
|
15
15
|
| { type: "question"; question: QuestionData }
|
|
16
16
|
| { type: "flashcards"; cards: FlashcardData[]; deckName?: string }
|
|
17
|
+
| { type: "audio"; src: string; title?: string }
|
|
18
|
+
| { type: "code"; code: string; language?: string; filename?: string; showLineNumbers?: boolean }
|
|
19
|
+
| { type: "embed"; src: string; title?: string; aspectRatio?: "16/9" | "4/3" | "1/1"; allowFullscreen?: boolean }
|
|
20
|
+
| { type: "table"; headers: string[]; rows: string[][]; caption?: string }
|
|
21
|
+
| { type: "file"; files: AttachmentFile[] }
|
|
17
22
|
| { type: "divider" }
|
|
18
23
|
| { type: "custom"; render: ReactNode };
|
|
19
24
|
|