@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,115 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
5
+ value?: number[]
6
+ onValueChange?: (value: number[]) => void
7
+ defaultValue?: number[]
8
+ min?: number
9
+ max?: number
10
+ step?: number
11
+ disabled?: boolean
12
+ }
13
+
14
+ /**
15
+ * Slider component with keyboard support
16
+ *
17
+ * @example
18
+ * const [value, setValue] = useState([50])
19
+ * <Slider value={value} onValueChange={setValue} max={100} step={1} />
20
+ */
21
+ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
22
+ ({
23
+ className,
24
+ value: controlledValue,
25
+ onValueChange,
26
+ defaultValue = [0],
27
+ min = 0,
28
+ max = 100,
29
+ step = 1,
30
+ disabled,
31
+ ...props
32
+ }, ref) => {
33
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
34
+
35
+ const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
36
+ const setValue = onValueChange || setUncontrolledValue
37
+ const currentValue = value[0] || 0
38
+
39
+ const percentage = ((currentValue - min) / (max - min)) * 100
40
+
41
+ const handleKeyDown = (e: React.KeyboardEvent) => {
42
+ if (disabled) return
43
+
44
+ let newValue = currentValue
45
+
46
+ switch (e.key) {
47
+ case "ArrowRight":
48
+ case "ArrowUp":
49
+ newValue = Math.min(currentValue + step, max)
50
+ break
51
+ case "ArrowLeft":
52
+ case "ArrowDown":
53
+ newValue = Math.max(currentValue - step, min)
54
+ break
55
+ case "Home":
56
+ newValue = min
57
+ break
58
+ case "End":
59
+ newValue = max
60
+ break
61
+ default:
62
+ return
63
+ }
64
+
65
+ e.preventDefault()
66
+ setValue([newValue])
67
+ }
68
+
69
+ const handleTrackClick = (e: React.MouseEvent<HTMLDivElement>) => {
70
+ if (disabled) return
71
+
72
+ const rect = e.currentTarget.getBoundingClientRect()
73
+ const clickPosition = (e.clientX - rect.left) / rect.width
74
+ const newValue = min + clickPosition * (max - min)
75
+ const steppedValue = Math.round(newValue / step) * step
76
+ setValue([Math.min(Math.max(steppedValue, min), max)])
77
+ }
78
+
79
+ return (
80
+ <div
81
+ ref={ref}
82
+ role="slider"
83
+ aria-valuenow={currentValue}
84
+ aria-valuemin={min}
85
+ aria-valuemax={max}
86
+ aria-disabled={disabled}
87
+ tabIndex={disabled ? -1 : 0}
88
+ className={cn(
89
+ "relative flex w-full touch-none select-none items-center",
90
+ disabled && "opacity-50 cursor-not-allowed",
91
+ className
92
+ )}
93
+ onKeyDown={handleKeyDown}
94
+ {...props}
95
+ >
96
+ <div
97
+ className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20 cursor-pointer"
98
+ onClick={handleTrackClick}
99
+ >
100
+ <div
101
+ className="absolute h-full bg-primary"
102
+ style={{ width: `${percentage}%` }}
103
+ />
104
+ </div>
105
+ <div
106
+ className="absolute block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
107
+ style={{ left: `calc(${percentage}% - 8px)` }}
108
+ />
109
+ </div>
110
+ )
111
+ }
112
+ )
113
+ Slider.displayName = "Slider"
114
+
115
+ export { Slider }
@@ -0,0 +1,131 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface StarRatingProps {
5
+ /** Current rating value */
6
+ value?: number
7
+ /** Max number of stars */
8
+ max?: number
9
+ /** Callback when rating changes */
10
+ onValueChange?: (value: number) => void
11
+ /** Whether rating is readonly */
12
+ readonly?: boolean
13
+ /** Size of stars */
14
+ size?: "sm" | "default" | "lg"
15
+ className?: string
16
+ }
17
+
18
+ const sizeClasses = {
19
+ sm: "h-4 w-4",
20
+ default: "h-5 w-5",
21
+ lg: "h-6 w-6",
22
+ }
23
+
24
+ /**
25
+ * Star rating component
26
+ *
27
+ * @example
28
+ * const [rating, setRating] = useState(0)
29
+ * <StarRating value={rating} onValueChange={setRating} />
30
+ *
31
+ * @example
32
+ * <StarRating value={4.5} readonly />
33
+ */
34
+ const StarRating = React.forwardRef<HTMLDivElement, StarRatingProps>(
35
+ (
36
+ {
37
+ value = 0,
38
+ max = 5,
39
+ onValueChange,
40
+ readonly = false,
41
+ size = "default",
42
+ className,
43
+ },
44
+ ref
45
+ ) => {
46
+ const [hoverValue, setHoverValue] = React.useState<number | null>(null)
47
+
48
+ const displayValue = hoverValue !== null ? hoverValue : value
49
+
50
+ const handleClick = (starValue: number) => {
51
+ if (!readonly) {
52
+ onValueChange?.(starValue)
53
+ }
54
+ }
55
+
56
+ const handleKeyDown = (e: React.KeyboardEvent, starValue: number) => {
57
+ if (readonly) return
58
+
59
+ if (e.key === "Enter" || e.key === " ") {
60
+ e.preventDefault()
61
+ onValueChange?.(starValue)
62
+ } else if (e.key === "ArrowRight") {
63
+ e.preventDefault()
64
+ onValueChange?.(Math.min(value + 1, max))
65
+ } else if (e.key === "ArrowLeft") {
66
+ e.preventDefault()
67
+ onValueChange?.(Math.max(value - 1, 0))
68
+ }
69
+ }
70
+
71
+ return (
72
+ <div
73
+ ref={ref}
74
+ role="radiogroup"
75
+ aria-label={`Rating: ${value} out of ${max} stars`}
76
+ className={cn("inline-flex gap-0.5", className)}
77
+ onMouseLeave={() => setHoverValue(null)}
78
+ >
79
+ {Array.from({ length: max }).map((_, index) => {
80
+ const starValue = index + 1
81
+ const isFilled = displayValue >= starValue
82
+ const isHalfFilled = !isFilled && displayValue > index && displayValue < starValue
83
+
84
+ return (
85
+ <button
86
+ key={index}
87
+ type="button"
88
+ role="radio"
89
+ aria-checked={value >= starValue}
90
+ aria-label={`${starValue} star${starValue > 1 ? "s" : ""}`}
91
+ disabled={readonly}
92
+ tabIndex={readonly ? -1 : starValue === Math.ceil(value) || (value === 0 && starValue === 1) ? 0 : -1}
93
+ className={cn(
94
+ "relative focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded",
95
+ !readonly && "cursor-pointer hover:scale-110 transition-transform"
96
+ )}
97
+ onClick={() => handleClick(starValue)}
98
+ onMouseEnter={() => !readonly && setHoverValue(starValue)}
99
+ onKeyDown={(e) => handleKeyDown(e, starValue)}
100
+ >
101
+ {/* Empty star */}
102
+ <svg
103
+ className={cn(sizeClasses[size], "text-muted-foreground/30")}
104
+ fill="currentColor"
105
+ viewBox="0 0 24 24"
106
+ >
107
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
108
+ </svg>
109
+
110
+ {/* Filled star overlay */}
111
+ <svg
112
+ className={cn(
113
+ sizeClasses[size],
114
+ "absolute inset-0 text-yellow-400 transition-opacity",
115
+ isFilled ? "opacity-100" : isHalfFilled ? "opacity-50" : "opacity-0"
116
+ )}
117
+ fill="currentColor"
118
+ viewBox="0 0 24 24"
119
+ >
120
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
121
+ </svg>
122
+ </button>
123
+ )
124
+ })}
125
+ </div>
126
+ )
127
+ }
128
+ )
129
+ StarRating.displayName = "StarRating"
130
+
131
+ export { StarRating }
@@ -0,0 +1,70 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
5
+ /**
6
+ * Whether the switch is on
7
+ */
8
+ checked?: boolean
9
+ /**
10
+ * Callback when the switch state changes
11
+ */
12
+ onCheckedChange?: (checked: boolean) => void
13
+ /**
14
+ * Whether the switch is disabled
15
+ */
16
+ disabled?: boolean
17
+ }
18
+
19
+ /**
20
+ * Switch/Toggle component with keyboard accessibility
21
+ *
22
+ * @example
23
+ * const [enabled, setEnabled] = useState(false)
24
+ * <Switch checked={enabled} onCheckedChange={setEnabled} />
25
+ */
26
+ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
27
+ ({ className, checked = false, onCheckedChange, disabled, ...props }, ref) => {
28
+ const handleClick = () => {
29
+ if (!disabled) {
30
+ onCheckedChange?.(!checked)
31
+ }
32
+ }
33
+
34
+ const handleKeyDown = (e: React.KeyboardEvent) => {
35
+ if (e.key === " " || e.key === "Enter") {
36
+ e.preventDefault()
37
+ handleClick()
38
+ }
39
+ }
40
+
41
+ return (
42
+ <button
43
+ type="button"
44
+ role="switch"
45
+ aria-checked={checked}
46
+ aria-disabled={disabled}
47
+ disabled={disabled}
48
+ ref={ref}
49
+ className={cn(
50
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
51
+ checked ? "bg-primary" : "bg-input",
52
+ className
53
+ )}
54
+ onClick={handleClick}
55
+ onKeyDown={handleKeyDown}
56
+ {...props}
57
+ >
58
+ <span
59
+ className={cn(
60
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform",
61
+ checked ? "translate-x-4" : "translate-x-0"
62
+ )}
63
+ />
64
+ </button>
65
+ )
66
+ }
67
+ )
68
+ Switch.displayName = "Switch"
69
+
70
+ export { Switch }
@@ -0,0 +1,136 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ /**
5
+ * Table components for data display
6
+ *
7
+ * @example
8
+ * <Table>
9
+ * <TableHeader>
10
+ * <TableRow>
11
+ * <TableHead>Name</TableHead>
12
+ * <TableHead>Email</TableHead>
13
+ * </TableRow>
14
+ * </TableHeader>
15
+ * <TableBody>
16
+ * <TableRow>
17
+ * <TableCell>John Doe</TableCell>
18
+ * <TableCell>john@example.com</TableCell>
19
+ * </TableRow>
20
+ * </TableBody>
21
+ * </Table>
22
+ */
23
+
24
+ const Table = React.forwardRef<
25
+ HTMLTableElement,
26
+ React.HTMLAttributes<HTMLTableElement>
27
+ >(({ className, ...props }, ref) => (
28
+ <div className="relative w-full overflow-auto">
29
+ <table
30
+ ref={ref}
31
+ className={cn("w-full caption-bottom text-sm", className)}
32
+ {...props}
33
+ />
34
+ </div>
35
+ ))
36
+ Table.displayName = "Table"
37
+
38
+ const TableHeader = React.forwardRef<
39
+ HTMLTableSectionElement,
40
+ React.HTMLAttributes<HTMLTableSectionElement>
41
+ >(({ className, ...props }, ref) => (
42
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
43
+ ))
44
+ TableHeader.displayName = "TableHeader"
45
+
46
+ const TableBody = React.forwardRef<
47
+ HTMLTableSectionElement,
48
+ React.HTMLAttributes<HTMLTableSectionElement>
49
+ >(({ className, ...props }, ref) => (
50
+ <tbody
51
+ ref={ref}
52
+ className={cn("[&_tr:last-child]:border-0", className)}
53
+ {...props}
54
+ />
55
+ ))
56
+ TableBody.displayName = "TableBody"
57
+
58
+ const TableFooter = React.forwardRef<
59
+ HTMLTableSectionElement,
60
+ React.HTMLAttributes<HTMLTableSectionElement>
61
+ >(({ className, ...props }, ref) => (
62
+ <tfoot
63
+ ref={ref}
64
+ className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
65
+ {...props}
66
+ />
67
+ ))
68
+ TableFooter.displayName = "TableFooter"
69
+
70
+ const TableRow = React.forwardRef<
71
+ HTMLTableRowElement,
72
+ React.HTMLAttributes<HTMLTableRowElement>
73
+ >(({ className, ...props }, ref) => (
74
+ <tr
75
+ ref={ref}
76
+ className={cn(
77
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ ))
83
+ TableRow.displayName = "TableRow"
84
+
85
+ const TableHead = React.forwardRef<
86
+ HTMLTableCellElement,
87
+ React.ThHTMLAttributes<HTMLTableCellElement>
88
+ >(({ className, ...props }, ref) => (
89
+ <th
90
+ ref={ref}
91
+ className={cn(
92
+ "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
93
+ className
94
+ )}
95
+ {...props}
96
+ />
97
+ ))
98
+ TableHead.displayName = "TableHead"
99
+
100
+ const TableCell = React.forwardRef<
101
+ HTMLTableCellElement,
102
+ React.TdHTMLAttributes<HTMLTableCellElement>
103
+ >(({ className, ...props }, ref) => (
104
+ <td
105
+ ref={ref}
106
+ className={cn(
107
+ "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
108
+ className
109
+ )}
110
+ {...props}
111
+ />
112
+ ))
113
+ TableCell.displayName = "TableCell"
114
+
115
+ const TableCaption = React.forwardRef<
116
+ HTMLTableCaptionElement,
117
+ React.HTMLAttributes<HTMLTableCaptionElement>
118
+ >(({ className, ...props }, ref) => (
119
+ <caption
120
+ ref={ref}
121
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
122
+ {...props}
123
+ />
124
+ ))
125
+ TableCaption.displayName = "TableCaption"
126
+
127
+ export {
128
+ Table,
129
+ TableHeader,
130
+ TableBody,
131
+ TableFooter,
132
+ TableHead,
133
+ TableRow,
134
+ TableCell,
135
+ TableCaption,
136
+ }
@@ -0,0 +1,122 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface TabsContextValue {
5
+ value: string
6
+ onValueChange: (value: string) => void
7
+ }
8
+
9
+ const TabsContext = React.createContext<TabsContextValue | null>(null)
10
+
11
+ interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ value?: string
13
+ onValueChange?: (value: string) => void
14
+ defaultValue?: string
15
+ }
16
+
17
+ /**
18
+ * Tabs component with keyboard navigation
19
+ *
20
+ * @example
21
+ * <Tabs defaultValue="tab1">
22
+ * <TabsList>
23
+ * <TabsTrigger value="tab1">Tab 1</TabsTrigger>
24
+ * <TabsTrigger value="tab2">Tab 2</TabsTrigger>
25
+ * </TabsList>
26
+ * <TabsContent value="tab1">Content 1</TabsContent>
27
+ * <TabsContent value="tab2">Content 2</TabsContent>
28
+ * </Tabs>
29
+ */
30
+ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
31
+ ({ className, value: controlledValue, onValueChange, defaultValue = "", children, ...props }, ref) => {
32
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
33
+
34
+ const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
35
+ const setValue = onValueChange || setUncontrolledValue
36
+
37
+ return (
38
+ <TabsContext.Provider value={{ value, onValueChange: setValue }}>
39
+ <div ref={ref} className={cn("", className)} {...props}>
40
+ {children}
41
+ </div>
42
+ </TabsContext.Provider>
43
+ )
44
+ }
45
+ )
46
+ Tabs.displayName = "Tabs"
47
+
48
+ const TabsList = React.forwardRef<
49
+ HTMLDivElement,
50
+ React.HTMLAttributes<HTMLDivElement>
51
+ >(({ className, ...props }, ref) => (
52
+ <div
53
+ ref={ref}
54
+ role="tablist"
55
+ className={cn(
56
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ ))
62
+ TabsList.displayName = "TabsList"
63
+
64
+ interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
65
+ value: string
66
+ }
67
+
68
+ const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
69
+ ({ className, value, ...props }, ref) => {
70
+ const context = React.useContext(TabsContext)
71
+ if (!context) throw new Error("TabsTrigger must be used within Tabs")
72
+
73
+ const isSelected = context.value === value
74
+
75
+ return (
76
+ <button
77
+ ref={ref}
78
+ type="button"
79
+ role="tab"
80
+ aria-selected={isSelected}
81
+ tabIndex={isSelected ? 0 : -1}
82
+ className={cn(
83
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
84
+ isSelected && "bg-background text-foreground shadow",
85
+ className
86
+ )}
87
+ onClick={() => context.onValueChange(value)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+ )
93
+ TabsTrigger.displayName = "TabsTrigger"
94
+
95
+ interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
96
+ value: string
97
+ }
98
+
99
+ const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(
100
+ ({ className, value, ...props }, ref) => {
101
+ const context = React.useContext(TabsContext)
102
+ if (!context) throw new Error("TabsContent must be used within Tabs")
103
+
104
+ if (context.value !== value) return null
105
+
106
+ return (
107
+ <div
108
+ ref={ref}
109
+ role="tabpanel"
110
+ tabIndex={0}
111
+ className={cn(
112
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ )
118
+ }
119
+ )
120
+ TabsContent.displayName = "TabsContent"
121
+
122
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,70 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const textVariants = cva("", {
6
+ variants: {
7
+ variant: {
8
+ h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
9
+ h2: "scroll-m-20 text-3xl font-semibold tracking-tight",
10
+ h3: "scroll-m-20 text-2xl font-semibold tracking-tight",
11
+ h4: "scroll-m-20 text-xl font-semibold tracking-tight",
12
+ h5: "scroll-m-20 text-lg font-semibold tracking-tight",
13
+ h6: "scroll-m-20 text-base font-semibold tracking-tight",
14
+ p: "leading-7 [&:not(:first-child)]:mt-6",
15
+ lead: "text-xl text-muted-foreground",
16
+ large: "text-lg font-semibold",
17
+ small: "text-sm font-medium leading-none",
18
+ muted: "text-sm text-muted-foreground",
19
+ code: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "p",
24
+ },
25
+ })
26
+
27
+ type TextVariants = VariantProps<typeof textVariants>
28
+
29
+ interface TextBaseProps extends TextVariants {
30
+ className?: string
31
+ children?: React.ReactNode
32
+ }
33
+
34
+ /**
35
+ * Polymorphic Text component for typography
36
+ *
37
+ * @example
38
+ * // As a heading
39
+ * <Text as="h1" variant="h1">Page Title</Text>
40
+ *
41
+ * // Using heading styles on a different element
42
+ * <Text as="span" variant="h2">Styled as H2</Text>
43
+ *
44
+ * // Muted text
45
+ * <Text variant="muted">Secondary information</Text>
46
+ */
47
+ const Text = React.forwardRef(
48
+ <T extends React.ElementType = "p">(
49
+ {
50
+ as,
51
+ className,
52
+ variant,
53
+ ...props
54
+ }: TextBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof TextBaseProps | "as">,
55
+ ref: React.ForwardedRef<React.ElementRef<T>>
56
+ ) => {
57
+ const Comp = as || "p"
58
+
59
+ return (
60
+ <Comp
61
+ ref={ref as any}
62
+ className={cn(textVariants({ variant, className }))}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+ )
68
+ Text.displayName = "Text"
69
+
70
+ export { Text, textVariants }
@@ -0,0 +1,39 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ export interface TextareaProps
5
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
6
+ /**
7
+ * Whether the textarea is in an error state
8
+ */
9
+ error?: boolean
10
+ }
11
+
12
+ /**
13
+ * Textarea component for multi-line text input
14
+ *
15
+ * @example
16
+ * <Textarea placeholder="Enter your message" />
17
+ *
18
+ * @example
19
+ * <Textarea error placeholder="Required field" />
20
+ */
21
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
22
+ ({ className, error, ...props }, ref) => {
23
+ return (
24
+ <textarea
25
+ className={cn(
26
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
27
+ error && "border-destructive focus-visible:ring-destructive",
28
+ className
29
+ )}
30
+ ref={ref}
31
+ aria-invalid={error ? "true" : undefined}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+ )
37
+ Textarea.displayName = "Textarea"
38
+
39
+ export { Textarea }