@srcroot/ui 0.0.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.
Files changed (44) hide show
  1. package/README.md +151 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +640 -0
  4. package/package.json +43 -0
  5. package/registry/accordion.tsx +158 -0
  6. package/registry/alert-dialog.tsx +206 -0
  7. package/registry/alert.tsx +73 -0
  8. package/registry/aspect-ratio.tsx +44 -0
  9. package/registry/avatar.tsx +94 -0
  10. package/registry/badge.tsx +68 -0
  11. package/registry/breadcrumb.tsx +151 -0
  12. package/registry/button-group.tsx +84 -0
  13. package/registry/button.tsx +102 -0
  14. package/registry/calendar.tsx +238 -0
  15. package/registry/card.tsx +114 -0
  16. package/registry/carousel.tsx +169 -0
  17. package/registry/checkbox.tsx +79 -0
  18. package/registry/collapsible.tsx +110 -0
  19. package/registry/container.tsx +60 -0
  20. package/registry/dialog.tsx +264 -0
  21. package/registry/dropdown-menu.tsx +387 -0
  22. package/registry/image.tsx +144 -0
  23. package/registry/input.tsx +44 -0
  24. package/registry/label.tsx +34 -0
  25. package/registry/loading-spinner.tsx +108 -0
  26. package/registry/otp-input.tsx +152 -0
  27. package/registry/pagination.tsx +146 -0
  28. package/registry/popover.tsx +135 -0
  29. package/registry/progress.tsx +49 -0
  30. package/registry/radio.tsx +99 -0
  31. package/registry/search.tsx +146 -0
  32. package/registry/select.tsx +190 -0
  33. package/registry/separator.tsx +44 -0
  34. package/registry/sheet.tsx +180 -0
  35. package/registry/skeleton.tsx +26 -0
  36. package/registry/slider.tsx +115 -0
  37. package/registry/star-rating.tsx +131 -0
  38. package/registry/switch.tsx +70 -0
  39. package/registry/table.tsx +136 -0
  40. package/registry/tabs.tsx +122 -0
  41. package/registry/text.tsx +70 -0
  42. package/registry/textarea.tsx +39 -0
  43. package/registry/toast.tsx +95 -0
  44. package/registry/tooltip.tsx +122 -0
@@ -0,0 +1,151 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ /**
5
+ * Breadcrumb navigation component
6
+ *
7
+ * @example
8
+ * <Breadcrumb>
9
+ * <BreadcrumbList>
10
+ * <BreadcrumbItem>
11
+ * <BreadcrumbLink href="/">Home</BreadcrumbLink>
12
+ * </BreadcrumbItem>
13
+ * <BreadcrumbSeparator />
14
+ * <BreadcrumbItem>
15
+ * <BreadcrumbPage>Current Page</BreadcrumbPage>
16
+ * </BreadcrumbItem>
17
+ * </BreadcrumbList>
18
+ * </Breadcrumb>
19
+ */
20
+
21
+ const Breadcrumb = React.forwardRef<
22
+ HTMLElement,
23
+ React.HTMLAttributes<HTMLElement>
24
+ >(({ ...props }, ref) => (
25
+ <nav ref={ref} aria-label="breadcrumb" {...props} />
26
+ ))
27
+ Breadcrumb.displayName = "Breadcrumb"
28
+
29
+ const BreadcrumbList = React.forwardRef<
30
+ HTMLOListElement,
31
+ React.OListHTMLAttributes<HTMLOListElement>
32
+ >(({ className, ...props }, ref) => (
33
+ <ol
34
+ ref={ref}
35
+ className={cn(
36
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ ))
42
+ BreadcrumbList.displayName = "BreadcrumbList"
43
+
44
+ const BreadcrumbItem = React.forwardRef<
45
+ HTMLLIElement,
46
+ React.LiHTMLAttributes<HTMLLIElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <li
49
+ ref={ref}
50
+ className={cn("inline-flex items-center gap-1.5", className)}
51
+ {...props}
52
+ />
53
+ ))
54
+ BreadcrumbItem.displayName = "BreadcrumbItem"
55
+
56
+ interface BreadcrumbLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
57
+ asChild?: boolean
58
+ }
59
+
60
+ const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
61
+ ({ asChild, className, children, ...props }, ref) => {
62
+ if (asChild && React.isValidElement(children)) {
63
+ return React.cloneElement(children as React.ReactElement<any>, {
64
+ ref,
65
+ className: cn("transition-colors hover:text-foreground", className),
66
+ })
67
+ }
68
+
69
+ return (
70
+ <a
71
+ ref={ref}
72
+ className={cn("transition-colors hover:text-foreground", className)}
73
+ {...props}
74
+ >
75
+ {children}
76
+ </a>
77
+ )
78
+ }
79
+ )
80
+ BreadcrumbLink.displayName = "BreadcrumbLink"
81
+
82
+ const BreadcrumbPage = React.forwardRef<
83
+ HTMLSpanElement,
84
+ React.HTMLAttributes<HTMLSpanElement>
85
+ >(({ className, ...props }, ref) => (
86
+ <span
87
+ ref={ref}
88
+ role="link"
89
+ aria-disabled="true"
90
+ aria-current="page"
91
+ className={cn("font-normal text-foreground", className)}
92
+ {...props}
93
+ />
94
+ ))
95
+ BreadcrumbPage.displayName = "BreadcrumbPage"
96
+
97
+ const BreadcrumbSeparator = ({
98
+ children,
99
+ className,
100
+ ...props
101
+ }: React.LiHTMLAttributes<HTMLLIElement>) => (
102
+ <li
103
+ role="presentation"
104
+ aria-hidden="true"
105
+ className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
106
+ {...props}
107
+ >
108
+ {children || (
109
+ <svg
110
+ className="h-4 w-4"
111
+ fill="none"
112
+ viewBox="0 0 24 24"
113
+ stroke="currentColor"
114
+ strokeWidth={2}
115
+ >
116
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
117
+ </svg>
118
+ )}
119
+ </li>
120
+ )
121
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
122
+
123
+ const BreadcrumbEllipsis = ({
124
+ className,
125
+ ...props
126
+ }: React.HTMLAttributes<HTMLSpanElement>) => (
127
+ <span
128
+ role="presentation"
129
+ aria-hidden="true"
130
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
131
+ {...props}
132
+ >
133
+ <svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
134
+ <circle cx="12" cy="12" r="1" />
135
+ <circle cx="19" cy="12" r="1" />
136
+ <circle cx="5" cy="12" r="1" />
137
+ </svg>
138
+ <span className="sr-only">More</span>
139
+ </span>
140
+ )
141
+ BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis"
142
+
143
+ export {
144
+ Breadcrumb,
145
+ BreadcrumbList,
146
+ BreadcrumbItem,
147
+ BreadcrumbLink,
148
+ BreadcrumbPage,
149
+ BreadcrumbSeparator,
150
+ BreadcrumbEllipsis,
151
+ }
@@ -0,0 +1,84 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const buttonGroupVariants = cva("inline-flex", {
6
+ variants: {
7
+ orientation: {
8
+ horizontal: "flex-row",
9
+ vertical: "flex-col",
10
+ },
11
+ attached: {
12
+ true: "",
13
+ false: "gap-2",
14
+ },
15
+ },
16
+ compoundVariants: [
17
+ {
18
+ orientation: "horizontal",
19
+ attached: true,
20
+ className:
21
+ "[&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:first-child]:rounded-r-none [&>*:last-child]:rounded-l-none [&>*:not(:first-child)]:-ml-px",
22
+ },
23
+ {
24
+ orientation: "vertical",
25
+ attached: true,
26
+ className:
27
+ "[&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:first-child]:rounded-b-none [&>*:last-child]:rounded-t-none [&>*:not(:first-child)]:-mt-px",
28
+ },
29
+ ],
30
+ defaultVariants: {
31
+ orientation: "horizontal",
32
+ attached: true,
33
+ },
34
+ })
35
+
36
+ type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>
37
+
38
+ interface ButtonGroupBaseProps extends ButtonGroupVariants {
39
+ className?: string
40
+ children?: React.ReactNode
41
+ }
42
+
43
+ /**
44
+ * Polymorphic ButtonGroup to group buttons together
45
+ *
46
+ * @example
47
+ * <ButtonGroup>
48
+ * <Button>Left</Button>
49
+ * <Button>Center</Button>
50
+ * <Button>Right</Button>
51
+ * </ButtonGroup>
52
+ *
53
+ * @example
54
+ * <ButtonGroup attached={false}>
55
+ * <Button>Spaced</Button>
56
+ * <Button>Buttons</Button>
57
+ * </ButtonGroup>
58
+ */
59
+ const ButtonGroup = React.forwardRef(
60
+ <T extends React.ElementType = "div">(
61
+ {
62
+ as,
63
+ className,
64
+ orientation,
65
+ attached,
66
+ ...props
67
+ }: ButtonGroupBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonGroupBaseProps | "as">,
68
+ ref: React.ForwardedRef<React.ElementRef<T>>
69
+ ) => {
70
+ const Comp = as || "div"
71
+
72
+ return (
73
+ <Comp
74
+ ref={ref as any}
75
+ role="group"
76
+ className={cn(buttonGroupVariants({ orientation, attached, className }))}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+ )
82
+ ButtonGroup.displayName = "ButtonGroup"
83
+
84
+ export { ButtonGroup, buttonGroupVariants }
@@ -0,0 +1,102 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const buttonVariants = cva(
6
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
12
+ destructive:
13
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
14
+ outline:
15
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
18
+ ghost: "hover:bg-accent hover:text-accent-foreground",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default: "h-9 px-4 py-2",
23
+ sm: "h-8 rounded-md px-3 text-xs",
24
+ lg: "h-10 rounded-md px-8",
25
+ icon: "h-9 w-9",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ size: "default",
31
+ },
32
+ }
33
+ )
34
+
35
+ type ButtonVariants = VariantProps<typeof buttonVariants>
36
+
37
+ interface ButtonBaseProps extends ButtonVariants {
38
+ className?: string
39
+ children?: React.ReactNode
40
+ }
41
+
42
+ /**
43
+ * Polymorphic Button component
44
+ *
45
+ * @example
46
+ * // As a button (default)
47
+ * <Button variant="outline">Click me</Button>
48
+ *
49
+ * // As a link
50
+ * <Button as="a" href="/home" variant="link">Go Home</Button>
51
+ *
52
+ * // With loading state
53
+ * <Button disabled>
54
+ * <LoadingSpinner /> Processing...
55
+ * </Button>
56
+ */
57
+ const Button = React.forwardRef(
58
+ <T extends React.ElementType = "button">(
59
+ {
60
+ as,
61
+ className,
62
+ variant,
63
+ size,
64
+ ...props
65
+ }: ButtonBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonBaseProps | "as">,
66
+ ref: React.ForwardedRef<React.ElementRef<T>>
67
+ ) => {
68
+ const Comp = as || "button"
69
+
70
+ // Ensure proper keyboard handling for non-button elements
71
+ const handleKeyDown = (e: React.KeyboardEvent) => {
72
+ if (Comp !== "button" && (e.key === "Enter" || e.key === " ")) {
73
+ e.preventDefault()
74
+ ; (e.currentTarget as HTMLElement).click()
75
+ }
76
+ // Call original onKeyDown if provided
77
+ const originalOnKeyDown = (props as any).onKeyDown
78
+ if (originalOnKeyDown) {
79
+ originalOnKeyDown(e)
80
+ }
81
+ }
82
+
83
+ // Add role="button" for non-button elements
84
+ const accessibilityProps = Comp !== "button" ? {
85
+ role: "button",
86
+ tabIndex: 0,
87
+ onKeyDown: handleKeyDown,
88
+ } : {}
89
+
90
+ return (
91
+ <Comp
92
+ ref={ref as any}
93
+ className={cn(buttonVariants({ variant, size, className }))}
94
+ {...accessibilityProps}
95
+ {...props}
96
+ />
97
+ )
98
+ }
99
+ )
100
+ Button.displayName = "Button"
101
+
102
+ export { Button, buttonVariants }
@@ -0,0 +1,238 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface CalendarContextValue {
5
+ currentMonth: Date
6
+ setCurrentMonth: (date: Date) => void
7
+ selectedDates: Date[]
8
+ onSelect: (date: Date) => void
9
+ mode: "single" | "multiple" | "range"
10
+ rangeStart: Date | null
11
+ }
12
+
13
+ const CalendarContext = React.createContext<CalendarContextValue | null>(null)
14
+
15
+ interface CalendarProps {
16
+ /** Selection mode */
17
+ mode?: "single" | "multiple" | "range"
18
+ /** Selected date(s) */
19
+ selected?: Date | Date[]
20
+ /** Callback when date is selected */
21
+ onSelect?: (date: Date | Date[] | undefined) => void
22
+ /** Default month to display */
23
+ defaultMonth?: Date
24
+ /** Minimum selectable date */
25
+ minDate?: Date
26
+ /** Maximum selectable date */
27
+ maxDate?: Date
28
+ /** Whether calendar is disabled */
29
+ disabled?: boolean
30
+ className?: string
31
+ }
32
+
33
+ const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
34
+ const MONTHS = [
35
+ "January", "February", "March", "April", "May", "June",
36
+ "July", "August", "September", "October", "November", "December"
37
+ ]
38
+
39
+ function getDaysInMonth(year: number, month: number): Date[] {
40
+ const days: Date[] = []
41
+ const firstDay = new Date(year, month, 1)
42
+ const lastDay = new Date(year, month + 1, 0)
43
+
44
+ // Add days from previous month to fill first week
45
+ const startPadding = firstDay.getDay()
46
+ for (let i = startPadding - 1; i >= 0; i--) {
47
+ days.push(new Date(year, month, -i))
48
+ }
49
+
50
+ // Add days of current month
51
+ for (let d = 1; d <= lastDay.getDate(); d++) {
52
+ days.push(new Date(year, month, d))
53
+ }
54
+
55
+ // Add days from next month to fill last week
56
+ const endPadding = 42 - days.length // 6 rows * 7 days
57
+ for (let i = 1; i <= endPadding; i++) {
58
+ days.push(new Date(year, month + 1, i))
59
+ }
60
+
61
+ return days
62
+ }
63
+
64
+ function isSameDay(d1: Date, d2: Date): boolean {
65
+ return d1.getDate() === d2.getDate() &&
66
+ d1.getMonth() === d2.getMonth() &&
67
+ d1.getFullYear() === d2.getFullYear()
68
+ }
69
+
70
+ function isInRange(date: Date, start: Date | null, end: Date | null): boolean {
71
+ if (!start || !end) return false
72
+ const d = date.getTime()
73
+ return d >= start.getTime() && d <= end.getTime()
74
+ }
75
+
76
+ /**
77
+ * Calendar date picker component
78
+ *
79
+ * @example
80
+ * // Single date
81
+ * const [date, setDate] = useState<Date>()
82
+ * <Calendar mode="single" selected={date} onSelect={setDate} />
83
+ *
84
+ * @example
85
+ * // Date range
86
+ * const [range, setRange] = useState<Date[]>([])
87
+ * <Calendar mode="range" selected={range} onSelect={setRange} />
88
+ */
89
+ const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
90
+ (
91
+ {
92
+ mode = "single",
93
+ selected,
94
+ onSelect,
95
+ defaultMonth = new Date(),
96
+ minDate,
97
+ maxDate,
98
+ disabled,
99
+ className,
100
+ },
101
+ ref
102
+ ) => {
103
+ const [currentMonth, setCurrentMonth] = React.useState(defaultMonth)
104
+ const [rangeStart, setRangeStart] = React.useState<Date | null>(null)
105
+
106
+ const selectedDates = React.useMemo(() => {
107
+ if (!selected) return []
108
+ return Array.isArray(selected) ? selected : [selected]
109
+ }, [selected])
110
+
111
+ const handleSelect = (date: Date) => {
112
+ if (disabled) return
113
+ if (minDate && date < minDate) return
114
+ if (maxDate && date > maxDate) return
115
+
116
+ if (mode === "single") {
117
+ onSelect?.(date)
118
+ } else if (mode === "multiple") {
119
+ const exists = selectedDates.some(d => isSameDay(d, date))
120
+ if (exists) {
121
+ onSelect?.(selectedDates.filter(d => !isSameDay(d, date)))
122
+ } else {
123
+ onSelect?.([...selectedDates, date])
124
+ }
125
+ } else if (mode === "range") {
126
+ if (!rangeStart) {
127
+ setRangeStart(date)
128
+ onSelect?.([date])
129
+ } else {
130
+ const start = rangeStart < date ? rangeStart : date
131
+ const end = rangeStart < date ? date : rangeStart
132
+ onSelect?.([start, end])
133
+ setRangeStart(null)
134
+ }
135
+ }
136
+ }
137
+
138
+ const navigatePrev = () => {
139
+ setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))
140
+ }
141
+
142
+ const navigateNext = () => {
143
+ setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))
144
+ }
145
+
146
+ const days = getDaysInMonth(currentMonth.getFullYear(), currentMonth.getMonth())
147
+ const rangeEnd = mode === "range" && selectedDates.length === 2 ? selectedDates[1] : null
148
+
149
+ return (
150
+ <div
151
+ ref={ref}
152
+ className={cn("p-3 bg-background border rounded-lg shadow-md w-fit", className)}
153
+ role="application"
154
+ aria-label="Calendar"
155
+ >
156
+ {/* Header */}
157
+ <div className="flex items-center justify-between mb-4">
158
+ <button
159
+ type="button"
160
+ onClick={navigatePrev}
161
+ className="p-1 rounded hover:bg-accent"
162
+ aria-label="Previous month"
163
+ >
164
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
165
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
166
+ </svg>
167
+ </button>
168
+ <span className="font-semibold">
169
+ {MONTHS[currentMonth.getMonth()]} {currentMonth.getFullYear()}
170
+ </span>
171
+ <button
172
+ type="button"
173
+ onClick={navigateNext}
174
+ className="p-1 rounded hover:bg-accent"
175
+ aria-label="Next month"
176
+ >
177
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
178
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
179
+ </svg>
180
+ </button>
181
+ </div>
182
+
183
+ {/* Day names */}
184
+ <div className="grid grid-cols-7 gap-1 mb-1">
185
+ {DAYS.map(day => (
186
+ <div key={day} className="text-center text-xs text-muted-foreground font-medium py-1">
187
+ {day}
188
+ </div>
189
+ ))}
190
+ </div>
191
+
192
+ {/* Days grid */}
193
+ <div className="grid grid-cols-7 gap-1" role="grid">
194
+ {days.map((date, index) => {
195
+ const isCurrentMonth = date.getMonth() === currentMonth.getMonth()
196
+ const isSelected = selectedDates.some(d => isSameDay(d, date))
197
+ const isRangeStart = rangeStart && isSameDay(date, rangeStart)
198
+ const isRangeEnd = rangeEnd && isSameDay(date, rangeEnd)
199
+ const inRange = isInRange(date, rangeStart || selectedDates[0], rangeEnd)
200
+ const isToday = isSameDay(date, new Date())
201
+ const isDisabled = disabled ||
202
+ (minDate && date < minDate) ||
203
+ (maxDate && date > maxDate)
204
+
205
+ return (
206
+ <button
207
+ key={index}
208
+ type="button"
209
+ role="gridcell"
210
+ aria-selected={isSelected}
211
+ aria-disabled={isDisabled}
212
+ disabled={isDisabled}
213
+ onClick={() => handleSelect(date)}
214
+ className={cn(
215
+ "h-8 w-8 rounded text-sm font-medium transition-colors",
216
+ "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1",
217
+ !isCurrentMonth && "text-muted-foreground/50",
218
+ isCurrentMonth && "text-foreground",
219
+ isToday && "border border-primary",
220
+ isSelected && "bg-primary text-primary-foreground",
221
+ (isRangeStart || isRangeEnd) && "bg-primary text-primary-foreground",
222
+ inRange && !isSelected && "bg-accent",
223
+ !isSelected && !inRange && "hover:bg-accent",
224
+ isDisabled && "opacity-50 cursor-not-allowed"
225
+ )}
226
+ >
227
+ {date.getDate()}
228
+ </button>
229
+ )
230
+ })}
231
+ </div>
232
+ </div>
233
+ )
234
+ }
235
+ )
236
+ Calendar.displayName = "Calendar"
237
+
238
+ export { Calendar }
@@ -0,0 +1,114 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ /**
5
+ * Card component - Container with header, content, and footer
6
+ *
7
+ * @example
8
+ * <Card>
9
+ * <CardHeader>
10
+ * <CardTitle>Title</CardTitle>
11
+ * <CardDescription>Description</CardDescription>
12
+ * </CardHeader>
13
+ * <CardContent>Content goes here</CardContent>
14
+ * <CardFooter>Footer actions</CardFooter>
15
+ * </Card>
16
+ */
17
+
18
+ interface CardBaseProps {
19
+ className?: string
20
+ children?: React.ReactNode
21
+ }
22
+
23
+ const Card = React.forwardRef(
24
+ <T extends React.ElementType = "div">(
25
+ {
26
+ as,
27
+ className,
28
+ ...props
29
+ }: CardBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof CardBaseProps | "as">,
30
+ ref: React.ForwardedRef<React.ElementRef<T>>
31
+ ) => {
32
+ const Comp = as || "div"
33
+
34
+ return (
35
+ <Comp
36
+ ref={ref as any}
37
+ className={cn(
38
+ "rounded-xl border bg-card text-card-foreground shadow",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+ )
46
+ Card.displayName = "Card"
47
+
48
+ const CardHeader = React.forwardRef<
49
+ HTMLDivElement,
50
+ React.HTMLAttributes<HTMLDivElement>
51
+ >(({ className, ...props }, ref) => (
52
+ <div
53
+ ref={ref}
54
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
55
+ {...props}
56
+ />
57
+ ))
58
+ CardHeader.displayName = "CardHeader"
59
+
60
+ const CardTitle = React.forwardRef(
61
+ <T extends React.ElementType = "h3">(
62
+ {
63
+ as,
64
+ className,
65
+ ...props
66
+ }: CardBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof CardBaseProps | "as">,
67
+ ref: React.ForwardedRef<React.ElementRef<T>>
68
+ ) => {
69
+ const Comp = as || "h3"
70
+
71
+ return (
72
+ <Comp
73
+ ref={ref as any}
74
+ className={cn("font-semibold leading-none tracking-tight", className)}
75
+ {...props}
76
+ />
77
+ )
78
+ }
79
+ )
80
+ CardTitle.displayName = "CardTitle"
81
+
82
+ const CardDescription = React.forwardRef<
83
+ HTMLParagraphElement,
84
+ React.HTMLAttributes<HTMLParagraphElement>
85
+ >(({ className, ...props }, ref) => (
86
+ <p
87
+ ref={ref}
88
+ className={cn("text-sm text-muted-foreground", className)}
89
+ {...props}
90
+ />
91
+ ))
92
+ CardDescription.displayName = "CardDescription"
93
+
94
+ const CardContent = React.forwardRef<
95
+ HTMLDivElement,
96
+ React.HTMLAttributes<HTMLDivElement>
97
+ >(({ className, ...props }, ref) => (
98
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
99
+ ))
100
+ CardContent.displayName = "CardContent"
101
+
102
+ const CardFooter = React.forwardRef<
103
+ HTMLDivElement,
104
+ React.HTMLAttributes<HTMLDivElement>
105
+ >(({ className, ...props }, ref) => (
106
+ <div
107
+ ref={ref}
108
+ className={cn("flex items-center p-6 pt-0", className)}
109
+ {...props}
110
+ />
111
+ ))
112
+ CardFooter.displayName = "CardFooter"
113
+
114
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }