@srcroot/ui 0.0.1 → 0.0.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.
@@ -0,0 +1,179 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Calendar } from "./calendar"
5
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover"
6
+ import { Button } from "./button"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ // Calendar icon
10
+ const CalendarIcon = () => (
11
+ <svg
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ width="16"
14
+ height="16"
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ strokeWidth="2"
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ className="mr-2 h-4 w-4 opacity-50"
22
+ >
23
+ <rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
24
+ <line x1="16" x2="16" y1="2" y2="6" />
25
+ <line x1="8" x2="8" y1="2" y2="6" />
26
+ <line x1="3" x2="21" y1="10" y2="10" />
27
+ </svg>
28
+ )
29
+
30
+ // Format helpers
31
+ const formatDate = (date: Date) => {
32
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
33
+ }
34
+
35
+ const formatRange = (dates: Date[]) => {
36
+ if (dates.length === 0) return null
37
+ if (dates.length === 1) return formatDate(dates[0])
38
+ return `${formatDate(dates[0])} → ${formatDate(dates[1])}`
39
+ }
40
+
41
+ const formatMultiple = (dates: Date[]) => {
42
+ if (dates.length === 0) return null
43
+ return `${dates.length} date${dates.length > 1 ? 's' : ''} selected`
44
+ }
45
+
46
+ // DatePicker Props
47
+ interface DatePickerBaseProps {
48
+ /** Placeholder text when no date selected */
49
+ placeholder?: string
50
+ /** Whether the picker is disabled */
51
+ disabled?: boolean
52
+ /** Custom class name for the trigger button */
53
+ className?: string
54
+ /** Number of months to display */
55
+ numberOfMonths?: 1 | 2
56
+ /** Calendar size */
57
+ size?: "xs" | "sm" | "default" | "md" | "lg"
58
+ }
59
+
60
+ interface DatePickerSingleProps extends DatePickerBaseProps {
61
+ mode?: "single"
62
+ selected?: Date
63
+ onSelect?: (date: Date | undefined) => void
64
+ }
65
+
66
+ interface DatePickerMultipleProps extends DatePickerBaseProps {
67
+ mode: "multiple"
68
+ selected?: Date[]
69
+ onSelect?: (dates: Date[]) => void
70
+ }
71
+
72
+ interface DatePickerRangeProps extends DatePickerBaseProps {
73
+ mode: "range"
74
+ selected?: Date[]
75
+ onSelect?: (dates: Date[]) => void
76
+ }
77
+
78
+ type DatePickerProps = DatePickerSingleProps | DatePickerMultipleProps | DatePickerRangeProps
79
+
80
+ /**
81
+ * DatePicker - A complete date picker component
82
+ *
83
+ * Combines Calendar + Popover for a ready-to-use date selection experience.
84
+ * Supports single, multiple, and range selection modes.
85
+ *
86
+ * @example
87
+ * // Single date
88
+ * <DatePicker selected={date} onSelect={setDate} />
89
+ *
90
+ * // Multiple dates
91
+ * <DatePicker mode="multiple" selected={dates} onSelect={setDates} />
92
+ *
93
+ * // Date range with dual months
94
+ * <DatePicker mode="range" numberOfMonths={2} selected={range} onSelect={setRange} />
95
+ */
96
+ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
97
+ ({
98
+ mode = "single",
99
+ selected,
100
+ onSelect,
101
+ placeholder,
102
+ disabled = false,
103
+ className,
104
+ numberOfMonths = 1,
105
+ size = "default",
106
+ ...props
107
+ }, ref) => {
108
+ const [open, setOpen] = React.useState(false)
109
+
110
+ // Determine display text
111
+ const getDisplayText = () => {
112
+ if (mode === "single") {
113
+ return selected ? formatDate(selected as Date) : null
114
+ } else if (mode === "multiple") {
115
+ const dates = (selected as Date[]) || []
116
+ return dates.length > 0 ? formatMultiple(dates) : null
117
+ } else {
118
+ const dates = (selected as Date[]) || []
119
+ return dates.length > 0 ? formatRange(dates) : null
120
+ }
121
+ }
122
+
123
+ const displayText = getDisplayText()
124
+ const defaultPlaceholder = mode === "single" ? "Pick a date"
125
+ : mode === "multiple" ? "Pick dates"
126
+ : "Pick a date range"
127
+
128
+ // Handle selection
129
+ const handleSelect = (value: any) => {
130
+ if (mode === "single") {
131
+ (onSelect as ((date: Date | undefined) => void))?.(value)
132
+ setOpen(false) // Close on single selection
133
+ } else if (mode === "multiple") {
134
+ (onSelect as ((dates: Date[]) => void))?.(value || [])
135
+ } else {
136
+ const dates = value || []
137
+ ; (onSelect as ((dates: Date[]) => void))?.(dates)
138
+ // Close when range is complete (2 dates)
139
+ if (dates.length === 2) {
140
+ setOpen(false)
141
+ }
142
+ }
143
+ }
144
+
145
+ return (
146
+ <Popover open={open} onOpenChange={setOpen}>
147
+ <PopoverTrigger asChild>
148
+ <Button
149
+ ref={ref}
150
+ variant="outline"
151
+ disabled={disabled}
152
+ className={cn(
153
+ "w-[280px] justify-start text-left font-normal",
154
+ !displayText && "text-muted-foreground",
155
+ numberOfMonths === 2 && "w-[320px]",
156
+ className
157
+ )}
158
+ >
159
+ <CalendarIcon />
160
+ {displayText || <span>{placeholder || defaultPlaceholder}</span>}
161
+ </Button>
162
+ </PopoverTrigger>
163
+ <PopoverContent className="w-auto p-0">
164
+ <Calendar
165
+ mode={mode}
166
+ numberOfMonths={numberOfMonths}
167
+ size={size}
168
+ selected={selected}
169
+ onSelect={handleSelect}
170
+ className="rounded-md border-0 shadow-none"
171
+ />
172
+ </PopoverContent>
173
+ </Popover>
174
+ )
175
+ }
176
+ )
177
+ DatePicker.displayName = "DatePicker"
178
+
179
+ export { DatePicker, type DatePickerProps }
@@ -0,0 +1,217 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Drawer Context
7
+ interface DrawerContextValue {
8
+ open: boolean
9
+ onOpenChange: (open: boolean) => void
10
+ }
11
+
12
+ const DrawerContext = React.createContext<DrawerContextValue | null>(null)
13
+
14
+ function useDrawer() {
15
+ const context = React.useContext(DrawerContext)
16
+ if (!context) {
17
+ throw new Error("useDrawer must be used within a Drawer")
18
+ }
19
+ return context
20
+ }
21
+
22
+ // Drawer Root
23
+ interface DrawerProps {
24
+ children: React.ReactNode
25
+ open?: boolean
26
+ onOpenChange?: (open: boolean) => void
27
+ defaultOpen?: boolean
28
+ }
29
+
30
+ const Drawer = ({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DrawerProps) => {
31
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
32
+ const open = controlledOpen ?? uncontrolledOpen
33
+ const setOpen = onOpenChange ?? setUncontrolledOpen
34
+
35
+ return (
36
+ <DrawerContext.Provider value={{ open, onOpenChange: setOpen }}>
37
+ {children}
38
+ </DrawerContext.Provider>
39
+ )
40
+ }
41
+
42
+ // Drawer Trigger
43
+ interface DrawerTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
44
+ asChild?: boolean
45
+ }
46
+
47
+ const DrawerTrigger = React.forwardRef<HTMLButtonElement, DrawerTriggerProps>(
48
+ ({ children, asChild, onClick, ...props }, ref) => {
49
+ const { onOpenChange } = useDrawer()
50
+
51
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
52
+ onClick?.(e)
53
+ onOpenChange(true)
54
+ }
55
+
56
+ if (asChild && React.isValidElement(children)) {
57
+ return React.cloneElement(children as React.ReactElement<any>, {
58
+ onClick: handleClick,
59
+ ref,
60
+ })
61
+ }
62
+
63
+ return (
64
+ <button ref={ref} onClick={handleClick} {...props}>
65
+ {children}
66
+ </button>
67
+ )
68
+ }
69
+ )
70
+ DrawerTrigger.displayName = "DrawerTrigger"
71
+
72
+ // Drawer Close
73
+ const DrawerClose = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
74
+ ({ children, onClick, ...props }, ref) => {
75
+ const { onOpenChange } = useDrawer()
76
+
77
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
78
+ onClick?.(e)
79
+ onOpenChange(false)
80
+ }
81
+
82
+ return (
83
+ <button ref={ref} onClick={handleClick} {...props}>
84
+ {children}
85
+ </button>
86
+ )
87
+ }
88
+ )
89
+ DrawerClose.displayName = "DrawerClose"
90
+
91
+ // Drawer Overlay
92
+ const DrawerOverlay = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
93
+ ({ className, ...props }, ref) => {
94
+ const { open, onOpenChange } = useDrawer()
95
+
96
+ if (!open) return null
97
+
98
+ return (
99
+ <div
100
+ ref={ref}
101
+ className={cn(
102
+ "fixed inset-0 z-50 bg-black/80",
103
+ "animate-in fade-in-0",
104
+ className
105
+ )}
106
+ onClick={() => onOpenChange(false)}
107
+ {...props}
108
+ />
109
+ )
110
+ }
111
+ )
112
+ DrawerOverlay.displayName = "DrawerOverlay"
113
+
114
+ // Drawer Content
115
+ interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {
116
+ side?: "bottom" | "top"
117
+ }
118
+
119
+ const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
120
+ ({ className, children, side = "bottom", ...props }, ref) => {
121
+ const { open, onOpenChange } = useDrawer()
122
+
123
+ // Close on Escape
124
+ React.useEffect(() => {
125
+ if (!open) return
126
+ const handleEscape = (e: KeyboardEvent) => {
127
+ if (e.key === "Escape") onOpenChange(false)
128
+ }
129
+ document.addEventListener("keydown", handleEscape)
130
+ return () => document.removeEventListener("keydown", handleEscape)
131
+ }, [open, onOpenChange])
132
+
133
+ // Prevent body scroll when open
134
+ React.useEffect(() => {
135
+ if (open) {
136
+ document.body.style.overflow = "hidden"
137
+ } else {
138
+ document.body.style.overflow = ""
139
+ }
140
+ return () => {
141
+ document.body.style.overflow = ""
142
+ }
143
+ }, [open])
144
+
145
+ if (!open) return null
146
+
147
+ return (
148
+ <>
149
+ <DrawerOverlay />
150
+ <div
151
+ ref={ref}
152
+ className={cn(
153
+ "fixed z-50 flex flex-col bg-background shadow-lg",
154
+ "transition-transform duration-300 ease-out",
155
+ side === "bottom" && "inset-x-0 bottom-0 rounded-t-xl border-t",
156
+ side === "top" && "inset-x-0 top-0 rounded-b-xl border-b",
157
+ // Animation
158
+ side === "bottom" && "animate-in slide-in-from-bottom",
159
+ side === "top" && "animate-in slide-in-from-top",
160
+ className
161
+ )}
162
+ {...props}
163
+ >
164
+ {/* Handle indicator */}
165
+ {side === "bottom" && (
166
+ <div className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
167
+ )}
168
+ {children}
169
+ {side === "top" && (
170
+ <div className="mx-auto mb-4 h-1.5 w-12 rounded-full bg-muted" />
171
+ )}
172
+ </div>
173
+ </>
174
+ )
175
+ }
176
+ )
177
+ DrawerContent.displayName = "DrawerContent"
178
+
179
+ // Drawer Header
180
+ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
181
+ <div className={cn("flex flex-col space-y-1.5 p-4 text-center sm:text-left", className)} {...props} />
182
+ )
183
+ DrawerHeader.displayName = "DrawerHeader"
184
+
185
+ // Drawer Footer
186
+ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
187
+ <div className={cn("flex flex-col-reverse gap-2 p-4 sm:flex-row sm:justify-end", className)} {...props} />
188
+ )
189
+ DrawerFooter.displayName = "DrawerFooter"
190
+
191
+ // Drawer Title
192
+ const DrawerTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
193
+ ({ className, ...props }, ref) => (
194
+ <h2 ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
195
+ )
196
+ )
197
+ DrawerTitle.displayName = "DrawerTitle"
198
+
199
+ // Drawer Description
200
+ const DrawerDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
201
+ ({ className, ...props }, ref) => (
202
+ <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
203
+ )
204
+ )
205
+ DrawerDescription.displayName = "DrawerDescription"
206
+
207
+ export {
208
+ Drawer,
209
+ DrawerTrigger,
210
+ DrawerClose,
211
+ DrawerOverlay,
212
+ DrawerContent,
213
+ DrawerHeader,
214
+ DrawerFooter,
215
+ DrawerTitle,
216
+ DrawerDescription,
217
+ }
@@ -193,18 +193,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
193
193
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
194
194
  if (disabled) return
195
195
  onClick?.(e)
196
- // Checkbox items usually don't close the menu, or maybe they do?
197
- // Standard behavior is often to keep open for multiple selections,
198
- // but Radix primitives usually don't close.
199
- // Let's assume user wants to toggle and keep open or close depending on UX.
200
- // For this simple implementation, let's NOT close it automatically.
201
- // Actually, for a "native-like" feel, single click usually toggles and keeps open?
202
- // No, standard non-native dropdowns usually close.
203
- // But for checkboxes, you might want to select multiple.
204
- // Let's stick to closing for now to be safe, or check standard behavior.
205
- // Radix UI defaults to NOT closing on selection for CheckboxItem.
206
- // But here we are building a custom one.
207
- // Let's NOT close it.
196
+ // Checkbox items don't close the menu to allow multiple selections
208
197
  e.preventDefault()
209
198
  e.stopPropagation()
210
199
  }
@@ -263,13 +252,6 @@ const DropdownMenuRadioItem = React.forwardRef<
263
252
  React.HTMLAttributes<HTMLDivElement> & { value: string; disabled?: boolean }
264
253
  >(({ className, children, value, disabled, onClick, ...props }, ref) => {
265
254
  const context = React.useContext(DropdownMenuContext)
266
- // We strictly don't have a RadioGroup context here in this simple implementation,
267
- // so we rely on the parent RadioGroup to handle state via context if we were using Radix.
268
- // However, since this is "copy/paste" simple code, we might just style it.
269
- // Realistically, to support `onValueChange` properly, we need a Context for RadioGroup.
270
- // Let's just implement the UI part for now as requested, assuming controlled state is handled by parent.
271
- // Wait, the playground likely expects it to work.
272
- // The request is about "exported member", implying it just needs to exist.
273
255
 
274
256
  return (
275
257
  <div
@@ -290,17 +272,6 @@ const DropdownMenuRadioItem = React.forwardRef<
290
272
  {...props}
291
273
  >
292
274
  <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
293
- {/* We don't have 'checked' state passed here easily without context.
294
- Consumers usually pass `checked={value === itemValue}` etc if using primitive.
295
- But look at the error: "no exported member".
296
- It seems they just want the component definitions.
297
- Standard Radix RadioItem has a `checked` prop too?
298
- Actually, let's assume the user passes a `checked` prop or handles logic.
299
- But wait, `value` is passed.
300
- Let's update the signature to accept `checked` for visual indicator if needed,
301
- or just render a circle.
302
- The previous error log didn't complain about props, just missing export.
303
- */}
304
275
  <svg
305
276
  xmlns="http://www.w3.org/2000/svg"
306
277
  viewBox="0 0 24 24"
@@ -0,0 +1,240 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Upload, X, File, Image, FileText, Film, Music } from "lucide-react"
5
+ import { cn } from "@/lib/utils"
6
+ import { Button } from "@/components/ui/button"
7
+
8
+ interface FileUploadProps {
9
+ onChange?: (files: File[]) => void
10
+ accept?: string
11
+ multiple?: boolean
12
+ maxSize?: number // in bytes
13
+ maxFiles?: number
14
+ className?: string
15
+ disabled?: boolean
16
+ }
17
+
18
+ interface UploadedFile {
19
+ file: File
20
+ preview?: string
21
+ }
22
+
23
+ const getFileIcon = (type: string) => {
24
+ if (type.startsWith("image/")) return Image
25
+ if (type.startsWith("video/")) return Film
26
+ if (type.startsWith("audio/")) return Music
27
+ if (type.includes("pdf") || type.includes("document")) return FileText
28
+ return File
29
+ }
30
+
31
+ const formatFileSize = (bytes: number): string => {
32
+ if (bytes === 0) return "0 Bytes"
33
+ const k = 1024
34
+ const sizes = ["Bytes", "KB", "MB", "GB"]
35
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
36
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
37
+ }
38
+
39
+ export function FileUpload({
40
+ onChange,
41
+ accept,
42
+ multiple = false,
43
+ maxSize = 10 * 1024 * 1024, // 10MB default
44
+ maxFiles = 5,
45
+ className,
46
+ disabled = false,
47
+ }: FileUploadProps) {
48
+ const [files, setFiles] = React.useState<UploadedFile[]>([])
49
+ const [isDragging, setIsDragging] = React.useState(false)
50
+ const [error, setError] = React.useState<string | null>(null)
51
+ const inputRef = React.useRef<HTMLInputElement>(null)
52
+
53
+ const handleFiles = React.useCallback(
54
+ (newFiles: FileList | null) => {
55
+ if (!newFiles) return
56
+ setError(null)
57
+
58
+ const fileArray = Array.from(newFiles)
59
+
60
+ // Validate file count
61
+ if (!multiple && fileArray.length > 1) {
62
+ setError("Only one file allowed")
63
+ return
64
+ }
65
+
66
+ if (multiple && files.length + fileArray.length > maxFiles) {
67
+ setError(`Maximum ${maxFiles} files allowed`)
68
+ return
69
+ }
70
+
71
+ // Validate file sizes
72
+ const oversizedFiles = fileArray.filter(f => f.size > maxSize)
73
+ if (oversizedFiles.length > 0) {
74
+ setError(`File(s) exceed maximum size of ${formatFileSize(maxSize)}`)
75
+ return
76
+ }
77
+
78
+ const uploadedFiles: UploadedFile[] = fileArray.map(file => ({
79
+ file,
80
+ preview: file.type.startsWith("image/") ? URL.createObjectURL(file) : undefined
81
+ }))
82
+
83
+ const newFileList = multiple ? [...files, ...uploadedFiles] : uploadedFiles
84
+ setFiles(newFileList)
85
+ onChange?.(newFileList.map(f => f.file))
86
+ },
87
+ [files, multiple, maxFiles, maxSize, onChange]
88
+ )
89
+
90
+ const handleDragOver = (e: React.DragEvent) => {
91
+ e.preventDefault()
92
+ if (!disabled) {
93
+ setIsDragging(true)
94
+ }
95
+ }
96
+
97
+ const handleDragLeave = (e: React.DragEvent) => {
98
+ e.preventDefault()
99
+ setIsDragging(false)
100
+ }
101
+
102
+ const handleDrop = (e: React.DragEvent) => {
103
+ e.preventDefault()
104
+ setIsDragging(false)
105
+ if (!disabled) {
106
+ handleFiles(e.dataTransfer.files)
107
+ }
108
+ }
109
+
110
+ const removeFile = (index: number) => {
111
+ const newFiles = files.filter((_, i) => i !== index)
112
+ // Revoke object URL to prevent memory leaks
113
+ if (files[index].preview) {
114
+ URL.revokeObjectURL(files[index].preview!)
115
+ }
116
+ setFiles(newFiles)
117
+ onChange?.(newFiles.map(f => f.file))
118
+ }
119
+
120
+ const handleClick = () => {
121
+ if (!disabled) {
122
+ inputRef.current?.click()
123
+ }
124
+ }
125
+
126
+ // Cleanup previews on unmount
127
+ React.useEffect(() => {
128
+ return () => {
129
+ files.forEach(f => {
130
+ if (f.preview) URL.revokeObjectURL(f.preview)
131
+ })
132
+ }
133
+ }, [])
134
+
135
+ return (
136
+ <div className={cn("space-y-4", className)}>
137
+ {/* Drop Zone */}
138
+ <div
139
+ onClick={handleClick}
140
+ onDragOver={handleDragOver}
141
+ onDragLeave={handleDragLeave}
142
+ onDrop={handleDrop}
143
+ className={cn(
144
+ "relative flex flex-col items-center justify-center gap-4 rounded-lg border-2 border-dashed p-8 transition-colors cursor-pointer",
145
+ isDragging
146
+ ? "border-primary bg-primary/5"
147
+ : "border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50",
148
+ disabled && "opacity-50 cursor-not-allowed",
149
+ error && "border-destructive"
150
+ )}
151
+ >
152
+ <input
153
+ ref={inputRef}
154
+ type="file"
155
+ accept={accept}
156
+ multiple={multiple}
157
+ onChange={(e) => handleFiles(e.target.files)}
158
+ className="hidden"
159
+ disabled={disabled}
160
+ />
161
+
162
+ <div className={cn(
163
+ "flex h-14 w-14 items-center justify-center rounded-full bg-muted transition-colors",
164
+ isDragging && "bg-primary/10"
165
+ )}>
166
+ <Upload className={cn(
167
+ "h-6 w-6 text-muted-foreground",
168
+ isDragging && "text-primary"
169
+ )} />
170
+ </div>
171
+
172
+ <div className="text-center">
173
+ <p className="text-sm font-medium">
174
+ {isDragging ? "Drop files here" : "Drag & drop files here"}
175
+ </p>
176
+ <p className="text-xs text-muted-foreground mt-1">
177
+ or click to browse
178
+ </p>
179
+ </div>
180
+
181
+ <p className="text-xs text-muted-foreground">
182
+ {accept ? `Accepted: ${accept}` : "All file types accepted"} • Max {formatFileSize(maxSize)}
183
+ </p>
184
+ </div>
185
+
186
+ {/* Error Message */}
187
+ {error && (
188
+ <p className="text-sm text-destructive">{error}</p>
189
+ )}
190
+
191
+ {/* File List */}
192
+ {files.length > 0 && (
193
+ <div className="space-y-2">
194
+ {files.map((uploadedFile, index) => {
195
+ const FileIcon = getFileIcon(uploadedFile.file.type)
196
+ return (
197
+ <div
198
+ key={index}
199
+ className="flex items-center gap-3 rounded-lg border bg-card p-3"
200
+ >
201
+ {uploadedFile.preview ? (
202
+ <img
203
+ src={uploadedFile.preview}
204
+ alt={uploadedFile.file.name}
205
+ className="h-10 w-10 rounded object-cover"
206
+ />
207
+ ) : (
208
+ <div className="flex h-10 w-10 items-center justify-center rounded bg-muted">
209
+ <FileIcon className="h-5 w-5 text-muted-foreground" />
210
+ </div>
211
+ )}
212
+
213
+ <div className="flex-1 min-w-0">
214
+ <p className="text-sm font-medium truncate">
215
+ {uploadedFile.file.name}
216
+ </p>
217
+ <p className="text-xs text-muted-foreground">
218
+ {formatFileSize(uploadedFile.file.size)}
219
+ </p>
220
+ </div>
221
+
222
+ <Button
223
+ variant="ghost"
224
+ size="icon"
225
+ className="h-8 w-8 text-muted-foreground hover:text-destructive"
226
+ onClick={(e: React.MouseEvent) => {
227
+ e.stopPropagation()
228
+ removeFile(index)
229
+ }}
230
+ >
231
+ <X className="h-4 w-4" />
232
+ </Button>
233
+ </div>
234
+ )
235
+ })}
236
+ </div>
237
+ )}
238
+ </div>
239
+ )
240
+ }