@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.
- package/README.md +151 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +640 -0
- package/package.json +43 -0
- package/registry/accordion.tsx +158 -0
- package/registry/alert-dialog.tsx +206 -0
- package/registry/alert.tsx +73 -0
- package/registry/aspect-ratio.tsx +44 -0
- package/registry/avatar.tsx +94 -0
- package/registry/badge.tsx +68 -0
- package/registry/breadcrumb.tsx +151 -0
- package/registry/button-group.tsx +84 -0
- package/registry/button.tsx +102 -0
- package/registry/calendar.tsx +238 -0
- package/registry/card.tsx +114 -0
- package/registry/carousel.tsx +169 -0
- package/registry/checkbox.tsx +79 -0
- package/registry/collapsible.tsx +110 -0
- package/registry/container.tsx +60 -0
- package/registry/dialog.tsx +264 -0
- package/registry/dropdown-menu.tsx +387 -0
- package/registry/image.tsx +144 -0
- package/registry/input.tsx +44 -0
- package/registry/label.tsx +34 -0
- package/registry/loading-spinner.tsx +108 -0
- package/registry/otp-input.tsx +152 -0
- package/registry/pagination.tsx +146 -0
- package/registry/popover.tsx +135 -0
- package/registry/progress.tsx +49 -0
- package/registry/radio.tsx +99 -0
- package/registry/search.tsx +146 -0
- package/registry/select.tsx +190 -0
- package/registry/separator.tsx +44 -0
- package/registry/sheet.tsx +180 -0
- package/registry/skeleton.tsx +26 -0
- package/registry/slider.tsx +115 -0
- package/registry/star-rating.tsx +131 -0
- package/registry/switch.tsx +70 -0
- package/registry/table.tsx +136 -0
- package/registry/tabs.tsx +122 -0
- package/registry/text.tsx +70 -0
- package/registry/textarea.tsx +39 -0
- package/registry/toast.tsx +95 -0
- 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 }
|