@hydralms/components 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/components.css +1 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +442 -110
  4. package/dist/modules/CoursePlayer/CoursePlayer.d.ts +2 -0
  5. package/dist/modules/CoursePlayer/types.d.ts +59 -0
  6. package/dist/modules/FlashcardLab/FlashcardLab.d.ts +2 -0
  7. package/dist/modules/FlashcardLab/types.d.ts +55 -0
  8. package/dist/modules/QuizModule/QuizModule.d.ts +2 -0
  9. package/dist/modules/QuizModule/types.d.ts +54 -0
  10. package/dist/modules/index.d.ts +6 -0
  11. package/dist/provider/HydraProvider.d.ts +1 -1
  12. package/dist/sections.cjs +1 -1
  13. package/dist/sections.js +261 -291
  14. package/dist/table-BrS5cDQu.js +2510 -0
  15. package/dist/table-D6AkBBEo.cjs +1 -0
  16. package/dist/ui/alert-dialog.d.ts +14 -8
  17. package/dist/ui/button.d.ts +1 -1
  18. package/dist/ui/tabs.d.ts +15 -5
  19. package/dist/ui/tooltip.d.ts +12 -5
  20. package/dist/video/index.d.ts +6 -1
  21. package/dist/video/types.d.ts +167 -0
  22. package/dist/video/video-bookmark.d.ts +2 -0
  23. package/dist/video/video-chapter-list.d.ts +2 -0
  24. package/dist/video/video-playlist-item.d.ts +2 -0
  25. package/dist/video/video-thumbnail-card.d.ts +2 -0
  26. package/dist/video/video-transcript.d.ts +2 -0
  27. package/package.json +135 -24
  28. package/src/__tests__/setup.ts +1 -0
  29. package/src/assessment-toolbar/assessment-toolbar.tsx +96 -0
  30. package/src/assessment-toolbar/index.ts +10 -0
  31. package/src/assessment-toolbar/question-navigator.tsx +86 -0
  32. package/src/assessment-toolbar/timer-display.tsx +73 -0
  33. package/src/assessment-toolbar/types.ts +92 -0
  34. package/src/assets/hydra-icon.png +0 -0
  35. package/src/assets/hydra-icon.svg +18 -0
  36. package/src/assets/hydra-lms-icon.png +0 -0
  37. package/src/assets/hydra-lms-icon.svg +9 -0
  38. package/src/common/confirm-dialog.tsx +60 -0
  39. package/src/common/due-date-display.tsx +64 -0
  40. package/src/common/empty-state.tsx +24 -0
  41. package/src/common/index.ts +12 -0
  42. package/src/common/search-input.tsx +68 -0
  43. package/src/common/status-badge.test.tsx +43 -0
  44. package/src/common/status-badge.tsx +81 -0
  45. package/src/common/types.ts +129 -0
  46. package/src/content/content-block.tsx +116 -0
  47. package/src/content/file-upload-zone.tsx +109 -0
  48. package/src/content/index.ts +7 -0
  49. package/src/content/types.ts +76 -0
  50. package/src/curriculum/curriculum-item.tsx +81 -0
  51. package/src/curriculum/curriculum-tree.tsx +69 -0
  52. package/src/curriculum/index.ts +11 -0
  53. package/src/curriculum/learning-object-icon.tsx +44 -0
  54. package/src/curriculum/types.ts +83 -0
  55. package/src/feedback/feedback-banner.tsx +46 -0
  56. package/src/feedback/index.ts +8 -0
  57. package/src/feedback/likert-scale.tsx +58 -0
  58. package/src/feedback/star-rating.tsx +65 -0
  59. package/src/feedback/types.ts +86 -0
  60. package/src/flashcards/flashcard-deck.tsx +130 -0
  61. package/src/flashcards/flashcard.tsx +108 -0
  62. package/src/flashcards/index.ts +3 -0
  63. package/src/flashcards/types.ts +60 -0
  64. package/src/index.ts +38 -0
  65. package/src/lib/utils.ts +6 -0
  66. package/src/modules/CoursePlayer/CoursePlayer.tsx +281 -0
  67. package/src/modules/CoursePlayer/types.ts +48 -0
  68. package/src/modules/FlashcardLab/FlashcardLab.tsx +275 -0
  69. package/src/modules/FlashcardLab/types.ts +58 -0
  70. package/src/modules/QuizModule/QuizModule.tsx +241 -0
  71. package/src/modules/QuizModule/types.ts +56 -0
  72. package/src/modules/index.ts +12 -0
  73. package/src/progress/grade-indicator.tsx +65 -0
  74. package/src/progress/index.ts +8 -0
  75. package/src/progress/progress-ring.tsx +56 -0
  76. package/src/progress/stat-card.tsx +42 -0
  77. package/src/progress/types.ts +73 -0
  78. package/src/provider/HydraProvider.tsx +26 -0
  79. package/src/provider/index.ts +2 -0
  80. package/src/questions/choice.tsx +90 -0
  81. package/src/questions/essay.tsx +59 -0
  82. package/src/questions/fill-in-the-blank.tsx +69 -0
  83. package/src/questions/index.ts +14 -0
  84. package/src/questions/multiple-choice.test.tsx +104 -0
  85. package/src/questions/multiple-choice.tsx +97 -0
  86. package/src/questions/question-renderer.tsx +37 -0
  87. package/src/questions/true-false.test.tsx +89 -0
  88. package/src/questions/true-false.tsx +90 -0
  89. package/src/questions/types.ts +53 -0
  90. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +141 -0
  91. package/src/sections/AnnouncementFeed/types.ts +50 -0
  92. package/src/sections/AssessmentReview/AssessmentReview.tsx +148 -0
  93. package/src/sections/AssessmentReview/types.ts +61 -0
  94. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +190 -0
  95. package/src/sections/AssignmentSubmission/types.ts +60 -0
  96. package/src/sections/CertificateViewer/CertificateViewer.tsx +117 -0
  97. package/src/sections/CertificateViewer/types.ts +45 -0
  98. package/src/sections/CourseOutline/CourseOutline.tsx +79 -0
  99. package/src/sections/CourseOutline/types.ts +53 -0
  100. package/src/sections/DiscussionThread/DiscussionThread.tsx +186 -0
  101. package/src/sections/DiscussionThread/types.ts +77 -0
  102. package/src/sections/ExamSession/ExamSession.tsx +182 -0
  103. package/src/sections/ExamSession/types.ts +64 -0
  104. package/src/sections/FlashcardStudySession/FlashcardStudySession.tsx +76 -0
  105. package/src/sections/FlashcardStudySession/types.ts +42 -0
  106. package/src/sections/GradebookTable/GradebookTable.tsx +229 -0
  107. package/src/sections/GradebookTable/types.ts +75 -0
  108. package/src/sections/LecturePlayer/LecturePlayer.tsx +60 -0
  109. package/src/sections/LecturePlayer/types.ts +48 -0
  110. package/src/sections/LessonPage/LessonPage.tsx +91 -0
  111. package/src/sections/LessonPage/types.ts +41 -0
  112. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +199 -0
  113. package/src/sections/PracticeQuiz/types.ts +44 -0
  114. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +140 -0
  115. package/src/sections/ProgressDashboard/types.ts +74 -0
  116. package/src/sections/QuizSession/QuizSession.tsx +113 -0
  117. package/src/sections/QuizSession/types.ts +47 -0
  118. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +218 -0
  119. package/src/sections/ResourceLibrary/types.ts +57 -0
  120. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +170 -0
  121. package/src/sections/ScrollableQuiz/types.ts +40 -0
  122. package/src/sections/SurveyForm/SurveyForm.tsx +180 -0
  123. package/src/sections/SurveyForm/types.ts +69 -0
  124. package/src/sections/index.ts +90 -0
  125. package/src/social/index.ts +3 -0
  126. package/src/social/post-card.tsx +91 -0
  127. package/src/social/types.ts +57 -0
  128. package/src/social/user-avatar.tsx +76 -0
  129. package/src/styles/globals.css +125 -0
  130. package/src/ui/alert-dialog.tsx +343 -0
  131. package/src/ui/alert.tsx +65 -0
  132. package/src/ui/avatar.tsx +52 -0
  133. package/src/ui/badge.tsx +53 -0
  134. package/src/ui/button.tsx +62 -0
  135. package/src/ui/card.tsx +92 -0
  136. package/src/ui/index.ts +44 -0
  137. package/src/ui/input.tsx +21 -0
  138. package/src/ui/progress.tsx +73 -0
  139. package/src/ui/separator.tsx +29 -0
  140. package/src/ui/skeleton.tsx +15 -0
  141. package/src/ui/slot.tsx +48 -0
  142. package/src/ui/table.tsx +108 -0
  143. package/src/ui/tabs.tsx +147 -0
  144. package/src/ui/textarea.tsx +20 -0
  145. package/src/ui/tooltip.tsx +177 -0
  146. package/src/utils/debounce.test.ts +59 -0
  147. package/src/utils/debounce.ts +10 -0
  148. package/src/utils/format-duration.test.ts +55 -0
  149. package/src/utils/format-duration.ts +30 -0
  150. package/src/video/index.ts +17 -0
  151. package/src/video/types.ts +216 -0
  152. package/src/video/video-bookmark.tsx +76 -0
  153. package/src/video/video-chapter-list.tsx +93 -0
  154. package/src/video/video-player.tsx +103 -0
  155. package/src/video/video-playlist-item.tsx +90 -0
  156. package/src/video/video-thumbnail-card.tsx +74 -0
  157. package/src/video/video-transcript.tsx +102 -0
  158. package/dist/table-CW4_BYny.js +0 -9869
  159. package/dist/table-DSBBqb9X.cjs +0 -56
@@ -0,0 +1,93 @@
1
+ import { Play } from "lucide-react";
2
+ import type { VideoChapterListProps } from "./types";
3
+ import { cn } from "../lib/utils";
4
+ import { formatTimer, formatDuration } from "../utils/format-duration";
5
+
6
+ export const VideoChapterList = ({
7
+ chapters,
8
+ currentTime = 0,
9
+ onSeek,
10
+ className,
11
+ style,
12
+ }: VideoChapterListProps) => {
13
+ // Active chapter: last chapter whose time <= currentTime
14
+ const activeIndex = chapters.reduce<number>(
15
+ (acc, ch, i) => (ch.time <= currentTime ? i : acc),
16
+ -1,
17
+ );
18
+
19
+ return (
20
+ <div
21
+ className={cn(
22
+ "divide-y divide-border rounded-md border border-border",
23
+ className,
24
+ )}
25
+ style={style}
26
+ >
27
+ {chapters.map((chapter, i) => {
28
+ const isActive = i === activeIndex;
29
+ return (
30
+ <div
31
+ key={i}
32
+ className={cn(
33
+ "flex items-center gap-3 p-3 transition-colors",
34
+ isActive && "bg-primary/10",
35
+ onSeek && "cursor-pointer hover:bg-muted",
36
+ )}
37
+ onClick={() => onSeek?.(chapter.time)}
38
+ >
39
+ {chapter.thumbnail ? (
40
+ <div
41
+ className="relative shrink-0 w-16 overflow-hidden rounded"
42
+ style={{ aspectRatio: "16/9" }}
43
+ >
44
+ <img
45
+ src={chapter.thumbnail}
46
+ alt={chapter.title}
47
+ className="size-full object-cover"
48
+ />
49
+ {isActive && (
50
+ <div className="absolute inset-0 flex items-center justify-center bg-black/40">
51
+ <Play size={14} className="ml-px text-white" />
52
+ </div>
53
+ )}
54
+ </div>
55
+ ) : (
56
+ <div
57
+ className={cn(
58
+ "flex shrink-0 items-center justify-center size-8 rounded-full",
59
+ isActive
60
+ ? "bg-primary text-primary-foreground"
61
+ : "bg-muted text-muted-foreground",
62
+ )}
63
+ >
64
+ <span className="text-xs font-semibold">{i + 1}</span>
65
+ </div>
66
+ )}
67
+
68
+ <div className="min-w-0 flex-1">
69
+ <div
70
+ className={cn(
71
+ "truncate text-sm font-medium",
72
+ isActive ? "text-primary" : "text-foreground",
73
+ )}
74
+ >
75
+ {chapter.title}
76
+ </div>
77
+ <div className="mt-0.5 flex items-center gap-2">
78
+ <span className="font-mono text-xs tabular-nums text-muted-foreground">
79
+ {formatTimer(Math.floor(chapter.time))}
80
+ </span>
81
+ {chapter.duration != null && chapter.duration > 0 && (
82
+ <span className="text-xs text-muted-foreground">
83
+ {formatDuration(chapter.duration)}
84
+ </span>
85
+ )}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ );
90
+ })}
91
+ </div>
92
+ );
93
+ };
@@ -0,0 +1,103 @@
1
+ import { useRef } from "react";
2
+ import { Video, Play } from "lucide-react";
3
+ import type { VideoPlayerProps } from "./types";
4
+ import { cn } from "../lib/utils";
5
+
6
+ export const VideoPlayer = ({
7
+ src,
8
+ poster,
9
+ title,
10
+ autoPlay = false,
11
+ onPlay,
12
+ onPause,
13
+ onEnded,
14
+ onTimeUpdate,
15
+ readOnly = false,
16
+ aspectRatio = "16/9",
17
+ className,
18
+ style,
19
+ }: VideoPlayerProps) => {
20
+ const videoRef = useRef<HTMLVideoElement>(null);
21
+
22
+ const handleTimeUpdate = () => {
23
+ const video = videoRef.current;
24
+ if (video && onTimeUpdate) {
25
+ onTimeUpdate(video.currentTime, video.duration);
26
+ }
27
+ };
28
+
29
+ if (!src) {
30
+ return (
31
+ <div
32
+ className={cn("relative overflow-hidden rounded-lg border border-border bg-muted", className)}
33
+ style={{ aspectRatio, ...style }}
34
+ >
35
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
36
+ <div className="flex items-center justify-center size-12 rounded-full bg-muted-foreground/10">
37
+ <Video size={24} />
38
+ </div>
39
+ <span className="text-sm text-muted-foreground">
40
+ No video source provided
41
+ </span>
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ if (readOnly) {
48
+ return (
49
+ <div
50
+ className={cn("relative overflow-hidden rounded-lg", className)}
51
+ style={{ aspectRatio, ...style }}
52
+ >
53
+ {poster ? (
54
+ <img
55
+ src={poster}
56
+ alt={title || "Video poster"}
57
+ style={{ width: "100%", height: "100%", objectFit: "cover" }}
58
+ />
59
+ ) : (
60
+ <div className="absolute inset-0 flex items-center justify-center bg-muted">
61
+ <div className="flex items-center justify-center size-14 rounded-full bg-black/60">
62
+ <Play size={28} className="text-white ml-0.5" />
63
+ </div>
64
+ </div>
65
+ )}
66
+
67
+ {poster && (
68
+ <div className="absolute inset-0 flex items-center justify-center bg-black/30 transition-opacity">
69
+ <div className="flex items-center justify-center size-14 rounded-full bg-black/60">
70
+ <Play size={28} className="text-white ml-0.5" />
71
+ </div>
72
+ </div>
73
+ )}
74
+
75
+ {title && (
76
+ <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/60 to-transparent p-2">
77
+ <span className="text-sm text-white font-medium">{title}</span>
78
+ </div>
79
+ )}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ return (
85
+ <div className={className} style={style}>
86
+ {title && <p className="mb-2 text-sm font-medium">{title}</p>}
87
+ <div className="rounded-lg overflow-hidden">
88
+ <video
89
+ ref={videoRef}
90
+ src={src}
91
+ poster={poster}
92
+ controls
93
+ autoPlay={autoPlay}
94
+ onPlay={onPlay}
95
+ onPause={onPause}
96
+ onEnded={onEnded}
97
+ onTimeUpdate={handleTimeUpdate}
98
+ style={{ width: "100%", aspectRatio, display: "block" }}
99
+ />
100
+ </div>
101
+ </div>
102
+ );
103
+ };
@@ -0,0 +1,90 @@
1
+ import { Play, CheckCircle2, Circle } from "lucide-react";
2
+ import type { VideoPlaylistItemProps } from "./types";
3
+ import { cn } from "../lib/utils";
4
+ import { formatDuration } from "../utils/format-duration";
5
+
6
+ export const VideoPlaylistItem = ({
7
+ thumbnail,
8
+ title,
9
+ duration,
10
+ status = "unwatched",
11
+ isActive = false,
12
+ index,
13
+ onClick,
14
+ className,
15
+ style,
16
+ }: VideoPlaylistItemProps) => {
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "flex items-center gap-3 rounded-md px-3 py-2 transition-colors",
21
+ isActive && "bg-primary/10",
22
+ onClick && "cursor-pointer hover:bg-muted",
23
+ className,
24
+ )}
25
+ style={style}
26
+ onClick={onClick}
27
+ >
28
+ {/* Index / Status icon */}
29
+ <div className="flex shrink-0 items-center justify-center size-6">
30
+ {status === "completed" ? (
31
+ <span className="text-success">
32
+ <CheckCircle2 size={18} />
33
+ </span>
34
+ ) : isActive ? (
35
+ <span className="text-primary">
36
+ <Play size={16} className="ml-px" />
37
+ </span>
38
+ ) : index != null ? (
39
+ <span className="text-xs font-medium tabular-nums text-muted-foreground">
40
+ {index}
41
+ </span>
42
+ ) : (
43
+ <span className="text-muted-foreground">
44
+ <Circle size={16} />
45
+ </span>
46
+ )}
47
+ </div>
48
+
49
+ {/* Thumbnail */}
50
+ {thumbnail && (
51
+ <div
52
+ className="relative shrink-0 w-16 overflow-hidden rounded"
53
+ style={{ aspectRatio: "16/9" }}
54
+ >
55
+ <img
56
+ src={thumbnail}
57
+ alt={title}
58
+ className={cn(
59
+ "size-full object-cover",
60
+ status === "completed" && "opacity-60",
61
+ )}
62
+ />
63
+ {isActive && (
64
+ <div className="absolute inset-0 flex items-center justify-center bg-black/40">
65
+ <Play size={12} className="ml-px text-white" />
66
+ </div>
67
+ )}
68
+ </div>
69
+ )}
70
+
71
+ {/* Title + duration */}
72
+ <div className="min-w-0 flex-1">
73
+ <div
74
+ className={cn(
75
+ "truncate text-sm",
76
+ isActive ? "font-medium text-primary" : "text-foreground",
77
+ status === "completed" && !isActive && "text-muted-foreground",
78
+ )}
79
+ >
80
+ {title}
81
+ </div>
82
+ {duration != null && duration > 0 && (
83
+ <span className="text-xs text-muted-foreground">
84
+ {formatDuration(duration)}
85
+ </span>
86
+ )}
87
+ </div>
88
+ </div>
89
+ );
90
+ };
@@ -0,0 +1,74 @@
1
+ import { Play, Video } from "lucide-react";
2
+ import type { VideoThumbnailCardProps } from "./types";
3
+ import { cn } from "../lib/utils";
4
+ import { formatDuration } from "../utils/format-duration";
5
+ import { Progress } from "../ui/progress";
6
+
7
+ export const VideoThumbnailCard = ({
8
+ poster,
9
+ title,
10
+ duration,
11
+ progress,
12
+ onClick,
13
+ className,
14
+ style,
15
+ }: VideoThumbnailCardProps) => {
16
+ return (
17
+ <div
18
+ className={cn(
19
+ "group overflow-hidden rounded-lg border border-border transition-colors",
20
+ onClick && "cursor-pointer hover:border-primary/50 hover:shadow-sm",
21
+ className,
22
+ )}
23
+ style={style}
24
+ onClick={onClick}
25
+ >
26
+ {/* Poster area */}
27
+ <div className="relative overflow-hidden" style={{ aspectRatio: "16/9" }}>
28
+ {poster ? (
29
+ <img
30
+ src={poster}
31
+ alt={title}
32
+ className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
33
+ />
34
+ ) : (
35
+ <div className="flex size-full items-center justify-center bg-muted">
36
+ <Video size={32} className="text-muted-foreground" />
37
+ </div>
38
+ )}
39
+
40
+ {/* Play icon overlay */}
41
+ <div className="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-hover:bg-black/20">
42
+ <div className="flex size-10 items-center justify-center rounded-full bg-black/60 opacity-0 transition-opacity group-hover:opacity-100">
43
+ <Play size={20} className="ml-0.5 text-white" />
44
+ </div>
45
+ </div>
46
+
47
+ {/* Duration badge */}
48
+ {duration != null && duration > 0 && (
49
+ <span className="absolute bottom-2 right-2 rounded bg-black/75 px-1.5 py-0.5 text-xs font-medium tabular-nums text-white">
50
+ {formatDuration(duration)}
51
+ </span>
52
+ )}
53
+ </div>
54
+
55
+ {/* Title */}
56
+ <div className="p-3">
57
+ <h4 className="truncate text-sm font-medium text-foreground">
58
+ {title}
59
+ </h4>
60
+ </div>
61
+
62
+ {/* Optional progress bar */}
63
+ {progress != null && progress >= 0 && (
64
+ <div className="px-3 pb-3">
65
+ <Progress
66
+ value={progress}
67
+ size="sm"
68
+ variant={progress >= 100 ? "success" : "default"}
69
+ />
70
+ </div>
71
+ )}
72
+ </div>
73
+ );
74
+ };
@@ -0,0 +1,102 @@
1
+ import { useRef, useEffect } from "react";
2
+ import { Clock } from "lucide-react";
3
+ import type { VideoTranscriptProps } from "./types";
4
+ import { cn } from "../lib/utils";
5
+ import { formatTimer } from "../utils/format-duration";
6
+
7
+ export const VideoTranscript = ({
8
+ entries,
9
+ currentTime = 0,
10
+ onSeek,
11
+ readOnly = false,
12
+ maxHeight = "400px",
13
+ className,
14
+ style,
15
+ }: VideoTranscriptProps) => {
16
+ const containerRef = useRef<HTMLDivElement>(null);
17
+ const activeRef = useRef<HTMLDivElement>(null);
18
+
19
+ // Last entry whose time <= currentTime
20
+ const activeIndex = entries.reduce<number>(
21
+ (acc, entry, i) => (entry.time <= currentTime ? i : acc),
22
+ -1,
23
+ );
24
+
25
+ useEffect(() => {
26
+ if (activeRef.current && containerRef.current) {
27
+ activeRef.current.scrollIntoView({
28
+ behavior: "smooth",
29
+ block: "nearest",
30
+ });
31
+ }
32
+ }, [activeIndex]);
33
+
34
+ if (entries.length === 0) {
35
+ return (
36
+ <div
37
+ className={cn(
38
+ "flex flex-col items-center justify-center gap-2 rounded-md border border-border bg-muted p-6",
39
+ className,
40
+ )}
41
+ style={style}
42
+ >
43
+ <Clock size={20} className="text-muted-foreground" />
44
+ <span className="text-sm text-muted-foreground">
45
+ No transcript available
46
+ </span>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <div
53
+ ref={containerRef}
54
+ className={cn(
55
+ "overflow-y-auto rounded-md border border-border",
56
+ className,
57
+ )}
58
+ style={{ maxHeight, ...style }}
59
+ >
60
+ {entries.map((entry, i) => {
61
+ const isActive = i === activeIndex;
62
+ return (
63
+ <div
64
+ key={i}
65
+ ref={isActive ? activeRef : undefined}
66
+ className={cn(
67
+ "flex gap-3 px-3 py-2 text-sm transition-colors",
68
+ isActive && "bg-primary/10",
69
+ !readOnly && onSeek && "cursor-pointer hover:bg-muted",
70
+ )}
71
+ onClick={() => !readOnly && onSeek?.(entry.time)}
72
+ >
73
+ <span
74
+ className={cn(
75
+ "shrink-0 font-mono text-xs tabular-nums pt-0.5",
76
+ isActive
77
+ ? "text-primary font-medium"
78
+ : "text-muted-foreground",
79
+ )}
80
+ >
81
+ {formatTimer(Math.floor(entry.time))}
82
+ </span>
83
+ <div className="min-w-0">
84
+ {entry.speaker && (
85
+ <span className="mr-1 font-semibold text-foreground">
86
+ {entry.speaker}:
87
+ </span>
88
+ )}
89
+ <span
90
+ className={
91
+ isActive ? "text-foreground" : "text-muted-foreground"
92
+ }
93
+ >
94
+ {entry.text}
95
+ </span>
96
+ </div>
97
+ </div>
98
+ );
99
+ })}
100
+ </div>
101
+ );
102
+ };