@hydralms/components 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/dist/ForumBoard-CHXU3mjC.js +2207 -0
  2. package/dist/ForumBoard-d1w5-r6n.cjs +1 -0
  3. package/dist/assessment-toolbar/assessment-toolbar.d.ts +1 -1
  4. package/dist/assessment-toolbar/index.d.ts +5 -1
  5. package/dist/assessment-toolbar/question-header-bar.d.ts +2 -0
  6. package/dist/assessment-toolbar/question-materials-drawer.d.ts +2 -0
  7. package/dist/assessment-toolbar/question-navigator.d.ts +1 -1
  8. package/dist/assessment-toolbar/types.d.ts +52 -4
  9. package/dist/assessment-toolbar/use-countdown.d.ts +43 -0
  10. package/dist/common/index.d.ts +2 -1
  11. package/dist/common/stepper.d.ts +6 -0
  12. package/dist/common/types.d.ts +37 -0
  13. package/dist/components.css +1 -1
  14. package/dist/content/attachment-list.d.ts +6 -0
  15. package/dist/content/content-block.d.ts +1 -1
  16. package/dist/content/index.d.ts +2 -1
  17. package/dist/content/types.d.ts +39 -0
  18. package/dist/curriculum/curriculum-item.d.ts +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.js +551 -312
  21. package/dist/modules/AssignmentModule/AssignmentModule.d.ts +8 -0
  22. package/dist/modules/AssignmentModule/types.d.ts +65 -0
  23. package/dist/modules/CertificateModule/CertificateModule.d.ts +9 -0
  24. package/dist/modules/CertificateModule/types.d.ts +49 -0
  25. package/dist/modules/DiscussionModule/DiscussionModule.d.ts +8 -0
  26. package/dist/modules/DiscussionModule/types.d.ts +47 -0
  27. package/dist/modules/ExamModule/ExamModule.d.ts +8 -0
  28. package/dist/modules/ExamModule/types.d.ts +64 -0
  29. package/dist/modules/GradeCenterModule/GradeCenterModule.d.ts +9 -0
  30. package/dist/modules/GradeCenterModule/types.d.ts +54 -0
  31. package/dist/modules/QuizModule/QuizModule.d.ts +1 -1
  32. package/dist/modules/QuizModule/types.d.ts +6 -1
  33. package/dist/modules/SurveyModule/SurveyModule.d.ts +7 -0
  34. package/dist/modules/SurveyModule/types.d.ts +49 -0
  35. package/dist/modules/index.d.ts +12 -0
  36. package/dist/modules.cjs +1 -0
  37. package/dist/modules.js +1422 -0
  38. package/dist/progress/achievement-badge.d.ts +6 -0
  39. package/dist/progress/activity-timeline.d.ts +6 -0
  40. package/dist/progress/index.d.ts +4 -1
  41. package/dist/progress/stat-card.d.ts +1 -1
  42. package/dist/progress/streak-badge.d.ts +6 -0
  43. package/dist/progress/types.d.ts +97 -0
  44. package/dist/questions/essay.d.ts +1 -1
  45. package/dist/questions/hotspot.d.ts +21 -0
  46. package/dist/questions/index.d.ts +9 -1
  47. package/dist/questions/inline-choice.d.ts +21 -0
  48. package/dist/questions/matching.d.ts +22 -0
  49. package/dist/questions/numeric.d.ts +11 -0
  50. package/dist/questions/ordering.d.ts +12 -0
  51. package/dist/questions/scenario.d.ts +23 -0
  52. package/dist/questions/scoring.d.ts +22 -0
  53. package/dist/questions/spreadsheet.d.ts +29 -0
  54. package/dist/questions/types.d.ts +106 -1
  55. package/dist/questions/use-drag-reorder.d.ts +17 -0
  56. package/dist/sections/CertificateViewer/types.d.ts +7 -5
  57. package/dist/sections/ExamSession/ExamSession.d.ts +1 -1
  58. package/dist/sections/ExamSession/types.d.ts +6 -1
  59. package/dist/sections/ForumBoard/ForumBoard.d.ts +8 -0
  60. package/dist/sections/ForumBoard/types.d.ts +64 -0
  61. package/dist/sections/QuizSession/QuizSession.d.ts +1 -1
  62. package/dist/sections/QuizSession/types.d.ts +6 -1
  63. package/dist/sections/RequirementsChecklist/RequirementsChecklist.d.ts +8 -0
  64. package/dist/sections/RequirementsChecklist/types.d.ts +37 -0
  65. package/dist/sections/RubricView/RubricView.d.ts +9 -0
  66. package/dist/sections/RubricView/types.d.ts +50 -0
  67. package/dist/sections/index.d.ts +7 -1
  68. package/dist/sections.cjs +1 -1
  69. package/dist/sections.js +250 -1715
  70. package/dist/social/post-card.d.ts +1 -1
  71. package/dist/tabs-DRM2Iq_J.cjs +172 -0
  72. package/dist/tabs-Wf3h_Cx3.js +21580 -0
  73. package/dist/ui/alert.d.ts +1 -1
  74. package/dist/ui/badge.d.ts +1 -1
  75. package/dist/ui/button.d.ts +1 -1
  76. package/dist/ui/drawer.d.ts +84 -0
  77. package/dist/ui/index.d.ts +3 -0
  78. package/dist/ui/progress.d.ts +1 -1
  79. package/dist/ui/rich-text-editor.d.ts +30 -0
  80. package/dist/ui/rich-text-toolbar.d.ts +8 -0
  81. package/dist/utils/array-utils.d.ts +4 -0
  82. package/dist/utils/flatten-leaves.d.ts +6 -0
  83. package/dist/utils/format-file-size.d.ts +1 -0
  84. package/dist/utils/format-timestamp.d.ts +1 -0
  85. package/dist/utils/is-empty-html.d.ts +5 -0
  86. package/dist/utils/shuffle.d.ts +1 -0
  87. package/dist/utils/string-utils.d.ts +12 -0
  88. package/dist/video/video-bookmark.d.ts +1 -1
  89. package/dist/video/video-playlist-item.d.ts +1 -1
  90. package/package.json +92 -3
  91. package/src/assessment-toolbar/assessment-toolbar.tsx +54 -49
  92. package/src/assessment-toolbar/index.ts +6 -0
  93. package/src/assessment-toolbar/question-header-bar.tsx +61 -0
  94. package/src/assessment-toolbar/question-materials-drawer.tsx +55 -0
  95. package/src/assessment-toolbar/question-navigator.tsx +3 -31
  96. package/src/assessment-toolbar/timer-display.tsx +2 -2
  97. package/src/assessment-toolbar/types.ts +54 -4
  98. package/src/assessment-toolbar/use-countdown.ts +153 -0
  99. package/src/common/index.ts +3 -0
  100. package/src/common/search-input.tsx +7 -6
  101. package/src/common/stepper.tsx +100 -0
  102. package/src/common/types.ts +39 -0
  103. package/src/content/attachment-list.tsx +90 -0
  104. package/src/content/content-block.tsx +4 -2
  105. package/src/content/file-upload-zone.tsx +1 -6
  106. package/src/content/index.ts +3 -0
  107. package/src/content/types.ts +41 -0
  108. package/src/curriculum/curriculum-item.tsx +7 -3
  109. package/src/feedback/feedback-banner.tsx +12 -14
  110. package/src/flashcards/flashcard-deck.tsx +1 -9
  111. package/src/flashcards/flashcard.tsx +1 -1
  112. package/src/modules/AssignmentModule/AssignmentModule.tsx +305 -0
  113. package/src/modules/AssignmentModule/types.ts +73 -0
  114. package/src/modules/CertificateModule/CertificateModule.tsx +161 -0
  115. package/src/modules/CertificateModule/types.ts +47 -0
  116. package/src/modules/CoursePlayer/CoursePlayer.tsx +44 -48
  117. package/src/modules/DiscussionModule/DiscussionModule.tsx +110 -0
  118. package/src/modules/DiscussionModule/types.ts +54 -0
  119. package/src/modules/ExamModule/ExamModule.tsx +285 -0
  120. package/src/modules/ExamModule/types.ts +66 -0
  121. package/src/modules/FlashcardLab/FlashcardLab.tsx +29 -16
  122. package/src/modules/GradeCenterModule/GradeCenterModule.tsx +169 -0
  123. package/src/modules/GradeCenterModule/types.ts +63 -0
  124. package/src/modules/QuizModule/QuizModule.tsx +88 -88
  125. package/src/modules/QuizModule/types.ts +6 -1
  126. package/src/modules/SurveyModule/SurveyModule.tsx +180 -0
  127. package/src/modules/SurveyModule/types.ts +51 -0
  128. package/src/modules/index.ts +24 -0
  129. package/src/progress/achievement-badge.tsx +52 -0
  130. package/src/progress/activity-timeline.tsx +84 -0
  131. package/src/progress/index.ts +7 -0
  132. package/src/progress/stat-card.tsx +30 -18
  133. package/src/progress/streak-badge.tsx +35 -0
  134. package/src/progress/types.ts +101 -0
  135. package/src/questions/choice.tsx +7 -9
  136. package/src/questions/essay.tsx +23 -25
  137. package/src/questions/fill-in-the-blank.tsx +13 -16
  138. package/src/questions/hotspot.tsx +154 -0
  139. package/src/questions/index.ts +16 -0
  140. package/src/questions/inline-choice.tsx +151 -0
  141. package/src/questions/matching.tsx +228 -0
  142. package/src/questions/multiple-choice.tsx +7 -9
  143. package/src/questions/numeric.tsx +102 -0
  144. package/src/questions/ordering.tsx +159 -0
  145. package/src/questions/question-renderer.tsx +21 -0
  146. package/src/questions/scenario.tsx +140 -0
  147. package/src/questions/scoring.ts +201 -0
  148. package/src/questions/spreadsheet.tsx +259 -0
  149. package/src/questions/true-false.tsx +7 -9
  150. package/src/questions/types.ts +123 -1
  151. package/src/questions/use-drag-reorder.ts +80 -0
  152. package/src/sections/AnnouncementFeed/AnnouncementFeed.tsx +2 -15
  153. package/src/sections/AssessmentReview/AssessmentReview.tsx +13 -2
  154. package/src/sections/AssignmentSubmission/AssignmentSubmission.tsx +7 -5
  155. package/src/sections/CertificateViewer/CertificateViewer.tsx +409 -56
  156. package/src/sections/CertificateViewer/types.ts +13 -5
  157. package/src/sections/CourseOutline/CourseOutline.tsx +4 -14
  158. package/src/sections/DiscussionThread/DiscussionThread.tsx +13 -10
  159. package/src/sections/ExamSession/ExamSession.tsx +44 -7
  160. package/src/sections/ExamSession/types.ts +6 -1
  161. package/src/sections/ForumBoard/ForumBoard.tsx +284 -0
  162. package/src/sections/ForumBoard/types.ts +67 -0
  163. package/src/sections/GradebookTable/GradebookTable.tsx +1 -1
  164. package/src/sections/LecturePlayer/LecturePlayer.tsx +1 -1
  165. package/src/sections/LessonPage/LessonPage.tsx +5 -9
  166. package/src/sections/PracticeQuiz/PracticeQuiz.tsx +15 -26
  167. package/src/sections/ProgressDashboard/ProgressDashboard.tsx +65 -65
  168. package/src/sections/QuizSession/QuizSession.tsx +67 -8
  169. package/src/sections/QuizSession/types.ts +6 -1
  170. package/src/sections/RequirementsChecklist/RequirementsChecklist.tsx +107 -0
  171. package/src/sections/RequirementsChecklist/types.ts +38 -0
  172. package/src/sections/ResourceLibrary/ResourceLibrary.tsx +4 -9
  173. package/src/sections/RubricView/RubricView.tsx +138 -0
  174. package/src/sections/RubricView/types.ts +52 -0
  175. package/src/sections/ScrollableQuiz/ScrollableQuiz.tsx +23 -9
  176. package/src/sections/SurveyForm/SurveyForm.tsx +8 -5
  177. package/src/sections/index.ts +20 -1
  178. package/src/social/post-card.tsx +8 -19
  179. package/src/social/user-avatar.tsx +1 -0
  180. package/src/styles/globals.css +13 -0
  181. package/src/ui/drawer.tsx +600 -0
  182. package/src/ui/index.ts +19 -0
  183. package/src/ui/rich-text-editor.tsx +109 -0
  184. package/src/ui/rich-text-toolbar.tsx +156 -0
  185. package/src/utils/array-utils.ts +17 -0
  186. package/src/utils/flatten-leaves.ts +17 -0
  187. package/src/utils/format-file-size.ts +5 -0
  188. package/src/utils/format-timestamp.ts +13 -0
  189. package/src/utils/is-empty-html.ts +7 -0
  190. package/src/utils/shuffle.ts +8 -0
  191. package/src/utils/string-utils.ts +30 -0
  192. package/src/video/video-bookmark.tsx +4 -3
  193. package/src/video/video-chapter-list.tsx +9 -4
  194. package/src/video/video-player.tsx +11 -4
  195. package/src/video/video-playlist-item.tsx +8 -3
  196. package/src/video/video-thumbnail-card.tsx +4 -0
  197. package/src/video/video-transcript.tsx +8 -5
  198. package/dist/table-BrS5cDQu.js +0 -2510
  199. package/dist/table-D6AkBBEo.cjs +0 -1
@@ -0,0 +1,600 @@
1
+ import * as React from "react";
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ useId,
9
+ useCallback,
10
+ } from "react";
11
+ import { createPortal } from "react-dom";
12
+ import { cva } from "class-variance-authority";
13
+ import { X } from "lucide-react";
14
+
15
+ import { cn } from "../lib/utils";
16
+ import { buttonVariants } from "./button";
17
+ import { Tooltip, TooltipTrigger, TooltipContent } from "./tooltip";
18
+
19
+ /* --------------------------------- Context -------------------------------- */
20
+
21
+ interface DrawerContextValue {
22
+ open: boolean;
23
+ onOpenChange: (open: boolean) => void;
24
+ side: "left" | "right";
25
+ titleId: string;
26
+ descriptionId: string;
27
+ hasNav: boolean;
28
+ setHasNav: (v: boolean) => void;
29
+ }
30
+
31
+ /* ------------------------------ Nav Context ------------------------------- */
32
+
33
+ interface DrawerNavContextValue {
34
+ value: string;
35
+ onValueChange: (value: string) => void;
36
+ }
37
+
38
+ const DrawerNavContext = createContext<DrawerNavContextValue | null>(null);
39
+
40
+ function useDrawerNavContext() {
41
+ const ctx = useContext(DrawerNavContext);
42
+ if (!ctx)
43
+ throw new Error(
44
+ "DrawerNavItem must be used within <DrawerNav>",
45
+ );
46
+ return ctx;
47
+ }
48
+
49
+ const DrawerContext = createContext<DrawerContextValue | null>(null);
50
+
51
+ function useDrawerContext() {
52
+ const ctx = useContext(DrawerContext);
53
+ if (!ctx)
54
+ throw new Error("Drawer compound components must be used within <Drawer>");
55
+ return ctx;
56
+ }
57
+
58
+ /* ------------------------------- Focus Trap ------------------------------- */
59
+
60
+ const FOCUSABLE_SELECTOR = [
61
+ "a[href]",
62
+ "button:not([disabled])",
63
+ "input:not([disabled])",
64
+ "select:not([disabled])",
65
+ "textarea:not([disabled])",
66
+ '[tabindex]:not([tabindex="-1"])',
67
+ ].join(", ");
68
+
69
+ function useFocusTrap(
70
+ containerRef: React.RefObject<HTMLElement | null>,
71
+ active: boolean,
72
+ ) {
73
+ const previousFocusRef = useRef<Element | null>(null);
74
+
75
+ useEffect(() => {
76
+ if (!active || !containerRef.current) return;
77
+
78
+ previousFocusRef.current = document.activeElement;
79
+
80
+ const focusable =
81
+ containerRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
82
+ if (focusable.length > 0) {
83
+ focusable[0].focus();
84
+ }
85
+
86
+ return () => {
87
+ if (previousFocusRef.current instanceof HTMLElement) {
88
+ previousFocusRef.current.focus();
89
+ }
90
+ };
91
+ }, [active, containerRef]);
92
+
93
+ const handleKeyDown = useCallback(
94
+ (e: React.KeyboardEvent) => {
95
+ if (e.key !== "Tab" || !containerRef.current) return;
96
+
97
+ const focusable = Array.from(
98
+ containerRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
99
+ );
100
+ if (focusable.length === 0) return;
101
+
102
+ const first = focusable[0];
103
+ const last = focusable[focusable.length - 1];
104
+
105
+ if (e.shiftKey && document.activeElement === first) {
106
+ e.preventDefault();
107
+ last.focus();
108
+ } else if (!e.shiftKey && document.activeElement === last) {
109
+ e.preventDefault();
110
+ first.focus();
111
+ }
112
+ },
113
+ [containerRef],
114
+ );
115
+
116
+ return handleKeyDown;
117
+ }
118
+
119
+ /* ------------------------------- Scroll Lock ------------------------------ */
120
+
121
+ function useScrollLock(active: boolean) {
122
+ useEffect(() => {
123
+ if (!active) return;
124
+
125
+ const original = document.body.style.overflow;
126
+ document.body.style.overflow = "hidden";
127
+
128
+ return () => {
129
+ document.body.style.overflow = original;
130
+ };
131
+ }, [active]);
132
+ }
133
+
134
+ /* ------------------------------ Variants --------------------------------- */
135
+
136
+ const drawerContentVariants = cva(
137
+ "bg-background fixed inset-y-0 z-50 flex flex-col shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out",
138
+ {
139
+ variants: {
140
+ side: {
141
+ right:
142
+ "right-0 border-l data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right",
143
+ left: "left-0 border-r data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",
144
+ },
145
+ size: {
146
+ sm: "w-80",
147
+ default: "w-100",
148
+ lg: "w-128",
149
+ xl: "w-160",
150
+ full: "w-full",
151
+ },
152
+ },
153
+ defaultVariants: {
154
+ side: "right",
155
+ size: "default",
156
+ },
157
+ },
158
+ );
159
+
160
+ /* -------------------------------- Drawer --------------------------------- */
161
+
162
+ /**
163
+ * A slide-in side panel for contextual content like navigation, settings, or detail views.
164
+ *
165
+ * @example
166
+ * ```tsx
167
+ * <Drawer open={open} onOpenChange={setOpen} side="right">
168
+ * <DrawerContent>
169
+ * <DrawerHeader><DrawerTitle>Panel</DrawerTitle></DrawerHeader>
170
+ * <DrawerBody>Content</DrawerBody>
171
+ * </DrawerContent>
172
+ * </Drawer>
173
+ * ```
174
+ */
175
+ interface DrawerProps {
176
+ /** Compound component children (DrawerContent, DrawerTrigger, etc.) */
177
+ children: React.ReactNode;
178
+ /** Whether the drawer is open. */
179
+ open: boolean;
180
+ /** Callback fired when the drawer should open or close. */
181
+ onOpenChange: (open: boolean) => void;
182
+ /** Which edge of the viewport the drawer slides in from. */
183
+ side?: "left" | "right";
184
+ }
185
+
186
+ function Drawer({ children, open, onOpenChange, side = "right" }: DrawerProps) {
187
+ const titleId = useId();
188
+ const descriptionId = useId();
189
+ const [hasNav, setHasNav] = useState(false);
190
+
191
+ return (
192
+ <DrawerContext.Provider
193
+ value={{ open, onOpenChange, side, titleId, descriptionId, hasNav, setHasNav }}
194
+ >
195
+ {children}
196
+ </DrawerContext.Provider>
197
+ );
198
+ }
199
+
200
+ /* ------------------------------ DrawerTrigger ----------------------------- */
201
+
202
+ function DrawerTrigger({
203
+ className,
204
+ onClick,
205
+ ...props
206
+ }: React.ComponentProps<"button">) {
207
+ const { onOpenChange } = useDrawerContext();
208
+
209
+ return (
210
+ <button
211
+ type="button"
212
+ data-slot="drawer-trigger"
213
+ className={className}
214
+ onClick={(e) => {
215
+ onOpenChange(true);
216
+ onClick?.(e);
217
+ }}
218
+ {...props}
219
+ />
220
+ );
221
+ }
222
+
223
+ /* ------------------------------- DrawerPortal ----------------------------- */
224
+
225
+ function DrawerPortal({ children }: { children: React.ReactNode }) {
226
+ return createPortal(children, document.body);
227
+ }
228
+
229
+ /* ----------------------------- DrawerBackdrop ----------------------------- */
230
+
231
+ function DrawerBackdrop({
232
+ className,
233
+ ...props
234
+ }: React.ComponentProps<"div">) {
235
+ const { onOpenChange } = useDrawerContext();
236
+
237
+ return (
238
+ <div
239
+ data-slot="drawer-backdrop"
240
+ className={cn(
241
+ "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
242
+ className,
243
+ )}
244
+ data-state="open"
245
+ onClick={() => onOpenChange(false)}
246
+ {...props}
247
+ />
248
+ );
249
+ }
250
+
251
+ /* ----------------------------- DrawerContent ------------------------------ */
252
+
253
+ interface DrawerContentProps
254
+ extends Omit<React.ComponentProps<"div">, "size"> {
255
+ /** Width preset for the drawer panel. */
256
+ size?: "sm" | "default" | "lg" | "xl" | "full";
257
+ /** Whether to lock body scroll when the drawer is open. */
258
+ scrollLock?: boolean;
259
+ }
260
+
261
+ function DrawerContent({
262
+ className,
263
+ children,
264
+ size = "default",
265
+ scrollLock: scrollLockProp = true,
266
+ ...props
267
+ }: DrawerContentProps) {
268
+ const { open, onOpenChange, side, titleId, descriptionId, hasNav } =
269
+ useDrawerContext();
270
+ const contentRef = useRef<HTMLDivElement>(null);
271
+
272
+ const handleFocusTrap = useFocusTrap(contentRef, open);
273
+ useScrollLock(open && scrollLockProp);
274
+
275
+ useEffect(() => {
276
+ if (!open) return;
277
+ const handler = (e: KeyboardEvent) => {
278
+ if (e.key === "Escape") onOpenChange(false);
279
+ };
280
+ document.addEventListener("keydown", handler);
281
+ return () => document.removeEventListener("keydown", handler);
282
+ }, [open, onOpenChange]);
283
+
284
+ if (!open) return null;
285
+
286
+ return (
287
+ <DrawerPortal>
288
+ <DrawerBackdrop />
289
+ <div
290
+ ref={contentRef}
291
+ role="dialog"
292
+ aria-modal="true"
293
+ aria-labelledby={titleId}
294
+ aria-describedby={descriptionId}
295
+ data-slot="drawer-content"
296
+ data-state="open"
297
+ className={cn(drawerContentVariants({ side, size }), hasNav && "flex-row", className)}
298
+ onKeyDown={handleFocusTrap}
299
+ {...props}
300
+ >
301
+ {children}
302
+ </div>
303
+ </DrawerPortal>
304
+ );
305
+ }
306
+
307
+ /* -------------------------------- Layout --------------------------------- */
308
+
309
+ function DrawerHeader({
310
+ className,
311
+ ...props
312
+ }: React.ComponentProps<"div">) {
313
+ return (
314
+ <div
315
+ data-slot="drawer-header"
316
+ className={cn("flex flex-col gap-1.5 border-b px-6 py-4 shrink-0", className)}
317
+ {...props}
318
+ />
319
+ );
320
+ }
321
+
322
+ function DrawerBody({
323
+ className,
324
+ ...props
325
+ }: React.ComponentProps<"div">) {
326
+ return (
327
+ <div
328
+ data-slot="drawer-body"
329
+ className={cn("flex-1 overflow-y-auto px-6 py-4", className)}
330
+ {...props}
331
+ />
332
+ );
333
+ }
334
+
335
+ function DrawerFooter({
336
+ className,
337
+ ...props
338
+ }: React.ComponentProps<"div">) {
339
+ return (
340
+ <div
341
+ data-slot="drawer-footer"
342
+ className={cn(
343
+ "flex items-center gap-2 border-t px-6 py-4 shrink-0",
344
+ className,
345
+ )}
346
+ {...props}
347
+ />
348
+ );
349
+ }
350
+
351
+ /* ------------------------------- Title/Desc ------------------------------ */
352
+
353
+ function DrawerTitle({
354
+ className,
355
+ ...props
356
+ }: React.ComponentProps<"h2">) {
357
+ const { titleId } = useDrawerContext();
358
+
359
+ return (
360
+ <h2
361
+ id={titleId}
362
+ data-slot="drawer-title"
363
+ className={cn("text-lg font-semibold", className)}
364
+ {...props}
365
+ />
366
+ );
367
+ }
368
+
369
+ function DrawerDescription({
370
+ className,
371
+ ...props
372
+ }: React.ComponentProps<"p">) {
373
+ const { descriptionId } = useDrawerContext();
374
+
375
+ return (
376
+ <p
377
+ id={descriptionId}
378
+ data-slot="drawer-description"
379
+ className={cn("text-muted-foreground text-sm", className)}
380
+ {...props}
381
+ />
382
+ );
383
+ }
384
+
385
+ /* -------------------------------- Close ---------------------------------- */
386
+
387
+ function DrawerClose({
388
+ className,
389
+ children,
390
+ onClick,
391
+ ...props
392
+ }: React.ComponentProps<"button">) {
393
+ const { onOpenChange } = useDrawerContext();
394
+
395
+ return (
396
+ <button
397
+ type="button"
398
+ data-slot="drawer-close"
399
+ className={cn(buttonVariants({ variant: "ghost", size: "sm" }), className)}
400
+ onClick={(e) => {
401
+ onOpenChange(false);
402
+ onClick?.(e);
403
+ }}
404
+ {...props}
405
+ >
406
+ {children ?? <X className="h-4 w-4" />}
407
+ </button>
408
+ );
409
+ }
410
+
411
+ /* --------------------------------- Nav ----------------------------------- */
412
+
413
+ /**
414
+ * A vertical icon rail for switching between views inside a Drawer.
415
+ * Provides controlled navigation state to DrawerNavItem children.
416
+ *
417
+ * @example
418
+ * ```tsx
419
+ * <DrawerNav value={activeView} onValueChange={setActiveView}>
420
+ * <DrawerNavItem value="notes" icon={<FileText />} label="Notes" />
421
+ * <DrawerNavItem value="bookmarks" icon={<Bookmark />} label="Bookmarks" />
422
+ * </DrawerNav>
423
+ * ```
424
+ */
425
+ interface DrawerNavProps extends React.ComponentProps<"nav"> {
426
+ /** The value of the currently active nav item. */
427
+ value: string;
428
+ /** Callback fired when the active nav item changes. */
429
+ onValueChange: (value: string) => void;
430
+ }
431
+
432
+ function DrawerNav({
433
+ value,
434
+ onValueChange,
435
+ className,
436
+ children,
437
+ ...props
438
+ }: DrawerNavProps) {
439
+ const { setHasNav } = useDrawerContext();
440
+ const navRef = useRef<HTMLElement>(null);
441
+
442
+ useEffect(() => {
443
+ setHasNav(true);
444
+ return () => setHasNav(false);
445
+ }, [setHasNav]);
446
+
447
+ const handleKeyDown = (e: React.KeyboardEvent) => {
448
+ if (!["ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) return;
449
+
450
+ const nav = navRef.current;
451
+ if (!nav) return;
452
+
453
+ const items = Array.from(
454
+ nav.querySelectorAll<HTMLButtonElement>('[role="tab"]:not([disabled])'),
455
+ );
456
+ const current = items.indexOf(document.activeElement as HTMLButtonElement);
457
+ if (current === -1) return;
458
+
459
+ e.preventDefault();
460
+ let next: number;
461
+ switch (e.key) {
462
+ case "ArrowDown":
463
+ next = (current + 1) % items.length;
464
+ break;
465
+ case "ArrowUp":
466
+ next = (current - 1 + items.length) % items.length;
467
+ break;
468
+ case "Home":
469
+ next = 0;
470
+ break;
471
+ case "End":
472
+ next = items.length - 1;
473
+ break;
474
+ default:
475
+ return;
476
+ }
477
+ items[next].focus();
478
+ };
479
+
480
+ return (
481
+ <DrawerNavContext.Provider value={{ value, onValueChange }}>
482
+ <nav
483
+ ref={navRef}
484
+ role="tablist"
485
+ aria-orientation="vertical"
486
+ data-slot="drawer-nav"
487
+ className={cn(
488
+ "flex shrink-0 flex-col items-center gap-1 border-r bg-muted/30 px-1.5 py-2",
489
+ className,
490
+ )}
491
+ onKeyDown={handleKeyDown}
492
+ {...props}
493
+ >
494
+ {children}
495
+ </nav>
496
+ </DrawerNavContext.Provider>
497
+ );
498
+ }
499
+
500
+ /* ------------------------------ DrawerNavItem ----------------------------- */
501
+
502
+ /**
503
+ * An icon button inside a DrawerNav rail. Renders a tooltip on hover.
504
+ */
505
+ interface DrawerNavItemProps
506
+ extends Omit<React.ComponentProps<"button">, "value"> {
507
+ /** Unique value identifying this view. Must match a DrawerViewport's conditional content. */
508
+ value: string;
509
+ /** Icon element to render (e.g., `<FileText />`). */
510
+ icon: React.ReactNode;
511
+ /** Accessible label shown in the tooltip. */
512
+ label: string;
513
+ }
514
+
515
+ function DrawerNavItem({
516
+ value,
517
+ icon,
518
+ label,
519
+ className,
520
+ ...props
521
+ }: DrawerNavItemProps) {
522
+ const { value: activeValue, onValueChange } = useDrawerNavContext();
523
+ const { side } = useDrawerContext();
524
+ const isActive = activeValue === value;
525
+
526
+ return (
527
+ <Tooltip>
528
+ <TooltipTrigger>
529
+ <button
530
+ role="tab"
531
+ type="button"
532
+ aria-selected={isActive}
533
+ aria-label={label}
534
+ data-slot="drawer-nav-item"
535
+ {...(isActive ? { "data-active": "" } : {})}
536
+ className={cn(
537
+ "relative flex cursor-pointer items-center justify-center rounded-md size-9 transition-colors",
538
+ "text-muted-foreground hover:text-foreground hover:bg-accent",
539
+ isActive && "text-foreground bg-accent/50",
540
+ className,
541
+ )}
542
+ onClick={() => onValueChange(value)}
543
+ {...props}
544
+ >
545
+ {isActive && (
546
+ <span
547
+ aria-hidden="true"
548
+ className={cn(
549
+ "absolute top-1 bottom-1 w-0.5 rounded-full bg-primary",
550
+ side === "left" ? "right-0 -mr-1.5" : "left-0 -ml-1.5",
551
+ )}
552
+ />
553
+ )}
554
+ {icon}
555
+ </button>
556
+ </TooltipTrigger>
557
+ <TooltipContent>{label}</TooltipContent>
558
+ </Tooltip>
559
+ );
560
+ }
561
+
562
+ /* ----------------------------- DrawerViewport ----------------------------- */
563
+
564
+ /**
565
+ * Content panel paired with a DrawerNav rail. Wraps header/body/footer in the
566
+ * remaining space beside the icon rail.
567
+ */
568
+ function DrawerViewport({
569
+ className,
570
+ ...props
571
+ }: React.ComponentProps<"div">) {
572
+ return (
573
+ <div
574
+ role="tabpanel"
575
+ data-slot="drawer-viewport"
576
+ className={cn("flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", className)}
577
+ {...props}
578
+ />
579
+ );
580
+ }
581
+
582
+ /* -------------------------------- Exports -------------------------------- */
583
+
584
+ export {
585
+ Drawer,
586
+ DrawerTrigger,
587
+ DrawerPortal,
588
+ DrawerBackdrop,
589
+ DrawerContent,
590
+ DrawerHeader,
591
+ DrawerBody,
592
+ DrawerFooter,
593
+ DrawerTitle,
594
+ DrawerDescription,
595
+ DrawerClose,
596
+ DrawerNav,
597
+ DrawerNavItem,
598
+ DrawerViewport,
599
+ drawerContentVariants,
600
+ };
package/src/ui/index.ts CHANGED
@@ -18,6 +18,23 @@ export {
18
18
  AlertDialogCancel,
19
19
  } from "./alert-dialog";
20
20
  export { Avatar, AvatarImage, AvatarFallback } from "./avatar";
21
+ export {
22
+ Drawer,
23
+ DrawerTrigger,
24
+ DrawerPortal,
25
+ DrawerBackdrop,
26
+ DrawerContent,
27
+ DrawerHeader,
28
+ DrawerBody,
29
+ DrawerFooter,
30
+ DrawerTitle,
31
+ DrawerDescription,
32
+ DrawerClose,
33
+ DrawerNav,
34
+ DrawerNavItem,
35
+ DrawerViewport,
36
+ drawerContentVariants,
37
+ } from "./drawer";
21
38
  export {
22
39
  Card,
23
40
  CardHeader,
@@ -42,3 +59,5 @@ export {
42
59
  } from "./table";
43
60
  export { Skeleton } from "./skeleton";
44
61
  export { Tooltip, TooltipTrigger, TooltipContent } from "./tooltip";
62
+ export { RichTextEditor } from "./rich-text-editor";
63
+ export type { RichTextEditorProps, RichTextEditorVariant } from "./rich-text-editor";