@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,146 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
interface SearchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
|
|
5
|
+
/** Callback when search value changes */
|
|
6
|
+
onSearch?: (value: string) => void
|
|
7
|
+
/** Debounce delay in ms */
|
|
8
|
+
debounceMs?: number
|
|
9
|
+
/** Show clear button */
|
|
10
|
+
showClear?: boolean
|
|
11
|
+
/** Loading state */
|
|
12
|
+
loading?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Search input with optional debounce and clear button
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <Search
|
|
20
|
+
* placeholder="Search..."
|
|
21
|
+
* onSearch={(value) => fetchResults(value)}
|
|
22
|
+
* debounceMs={300}
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
const Search = React.forwardRef<HTMLInputElement, SearchProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
className,
|
|
29
|
+
onSearch,
|
|
30
|
+
debounceMs = 0,
|
|
31
|
+
showClear = true,
|
|
32
|
+
loading,
|
|
33
|
+
defaultValue = "",
|
|
34
|
+
...props
|
|
35
|
+
},
|
|
36
|
+
ref
|
|
37
|
+
) => {
|
|
38
|
+
const [value, setValue] = React.useState(String(defaultValue))
|
|
39
|
+
const debounceRef = React.useRef<NodeJS.Timeout>()
|
|
40
|
+
|
|
41
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
42
|
+
const newValue = e.target.value
|
|
43
|
+
setValue(newValue)
|
|
44
|
+
|
|
45
|
+
if (debounceMs > 0) {
|
|
46
|
+
clearTimeout(debounceRef.current)
|
|
47
|
+
debounceRef.current = setTimeout(() => {
|
|
48
|
+
onSearch?.(newValue)
|
|
49
|
+
}, debounceMs)
|
|
50
|
+
} else {
|
|
51
|
+
onSearch?.(newValue)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleClear = () => {
|
|
56
|
+
setValue("")
|
|
57
|
+
onSearch?.("")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
61
|
+
if (e.key === "Escape") {
|
|
62
|
+
handleClear()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
return () => {
|
|
68
|
+
clearTimeout(debounceRef.current)
|
|
69
|
+
}
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={cn("relative", className)}>
|
|
74
|
+
{/* Search Icon */}
|
|
75
|
+
<svg
|
|
76
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
|
|
77
|
+
fill="none"
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
strokeWidth={2}
|
|
81
|
+
>
|
|
82
|
+
<path
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
strokeLinejoin="round"
|
|
85
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
86
|
+
/>
|
|
87
|
+
</svg>
|
|
88
|
+
|
|
89
|
+
<input
|
|
90
|
+
ref={ref}
|
|
91
|
+
type="search"
|
|
92
|
+
role="searchbox"
|
|
93
|
+
value={value}
|
|
94
|
+
onChange={handleChange}
|
|
95
|
+
onKeyDown={handleKeyDown}
|
|
96
|
+
className={cn(
|
|
97
|
+
"flex h-10 w-full rounded-md border border-input bg-transparent pl-10 pr-10 py-2 text-sm shadow-sm transition-colors",
|
|
98
|
+
"placeholder:text-muted-foreground",
|
|
99
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
100
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
101
|
+
)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
{/* Loading or Clear */}
|
|
106
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
107
|
+
{loading ? (
|
|
108
|
+
<svg
|
|
109
|
+
className="h-4 w-4 animate-spin text-muted-foreground"
|
|
110
|
+
fill="none"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
>
|
|
113
|
+
<circle
|
|
114
|
+
className="opacity-25"
|
|
115
|
+
cx="12"
|
|
116
|
+
cy="12"
|
|
117
|
+
r="10"
|
|
118
|
+
stroke="currentColor"
|
|
119
|
+
strokeWidth="4"
|
|
120
|
+
/>
|
|
121
|
+
<path
|
|
122
|
+
className="opacity-75"
|
|
123
|
+
fill="currentColor"
|
|
124
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
125
|
+
/>
|
|
126
|
+
</svg>
|
|
127
|
+
) : showClear && value ? (
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={handleClear}
|
|
131
|
+
className="text-muted-foreground hover:text-foreground"
|
|
132
|
+
aria-label="Clear search"
|
|
133
|
+
>
|
|
134
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
135
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
136
|
+
</svg>
|
|
137
|
+
</button>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
Search.displayName = "Search"
|
|
145
|
+
|
|
146
|
+
export { Search }
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
interface SelectContextValue {
|
|
5
|
+
value: string
|
|
6
|
+
onValueChange: (value: string) => void
|
|
7
|
+
open: boolean
|
|
8
|
+
setOpen: (open: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SelectContext = React.createContext<SelectContextValue | null>(null)
|
|
12
|
+
|
|
13
|
+
interface SelectProps {
|
|
14
|
+
children: React.ReactNode
|
|
15
|
+
value?: string
|
|
16
|
+
onValueChange?: (value: string) => void
|
|
17
|
+
defaultValue?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom Select dropdown with keyboard navigation
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <Select value={value} onValueChange={setValue}>
|
|
25
|
+
* <SelectTrigger>
|
|
26
|
+
* <SelectValue placeholder="Select option" />
|
|
27
|
+
* </SelectTrigger>
|
|
28
|
+
* <SelectContent>
|
|
29
|
+
* <SelectItem value="option1">Option 1</SelectItem>
|
|
30
|
+
* <SelectItem value="option2">Option 2</SelectItem>
|
|
31
|
+
* </SelectContent>
|
|
32
|
+
* </Select>
|
|
33
|
+
*/
|
|
34
|
+
function Select({ children, value: controlledValue, onValueChange, defaultValue = "" }: SelectProps) {
|
|
35
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
|
|
36
|
+
const [open, setOpen] = React.useState(false)
|
|
37
|
+
|
|
38
|
+
const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
|
|
39
|
+
const setValue = onValueChange || setUncontrolledValue
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<SelectContext.Provider value={{ value, onValueChange: setValue, open, setOpen }}>
|
|
43
|
+
<div className="relative">
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
</SelectContext.Provider>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SelectTrigger = React.forwardRef<
|
|
51
|
+
HTMLButtonElement,
|
|
52
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
53
|
+
>(({ className, children, ...props }, ref) => {
|
|
54
|
+
const context = React.useContext(SelectContext)
|
|
55
|
+
if (!context) throw new Error("SelectTrigger must be used within Select")
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
ref={ref}
|
|
60
|
+
type="button"
|
|
61
|
+
role="combobox"
|
|
62
|
+
aria-expanded={context.open}
|
|
63
|
+
aria-haspopup="listbox"
|
|
64
|
+
className={cn(
|
|
65
|
+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
onClick={() => context.setOpen(!context.open)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
<svg
|
|
73
|
+
className={cn("h-4 w-4 opacity-50 transition-transform", context.open && "rotate-180")}
|
|
74
|
+
fill="none"
|
|
75
|
+
viewBox="0 0 24 24"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
strokeWidth={2}
|
|
78
|
+
>
|
|
79
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
SelectTrigger.displayName = "SelectTrigger"
|
|
85
|
+
|
|
86
|
+
interface SelectValueProps {
|
|
87
|
+
placeholder?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function SelectValue({ placeholder }: SelectValueProps) {
|
|
91
|
+
const context = React.useContext(SelectContext)
|
|
92
|
+
if (!context) throw new Error("SelectValue must be used within Select")
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<span className={cn(!context.value && "text-muted-foreground")}>
|
|
96
|
+
{context.value || placeholder}
|
|
97
|
+
</span>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const SelectContent = React.forwardRef<
|
|
102
|
+
HTMLDivElement,
|
|
103
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
104
|
+
>(({ className, children, ...props }, ref) => {
|
|
105
|
+
const context = React.useContext(SelectContext)
|
|
106
|
+
if (!context) throw new Error("SelectContent must be used within Select")
|
|
107
|
+
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
110
|
+
if (context.open) {
|
|
111
|
+
context.setOpen(false)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
116
|
+
if (e.key === "Escape" && context.open) {
|
|
117
|
+
context.setOpen(false)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
document.addEventListener("mousedown", handleClickOutside)
|
|
122
|
+
document.addEventListener("keydown", handleEscape)
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
document.removeEventListener("mousedown", handleClickOutside)
|
|
126
|
+
document.removeEventListener("keydown", handleEscape)
|
|
127
|
+
}
|
|
128
|
+
}, [context.open, context])
|
|
129
|
+
|
|
130
|
+
if (!context.open) return null
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
ref={ref}
|
|
135
|
+
role="listbox"
|
|
136
|
+
className={cn(
|
|
137
|
+
"absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
138
|
+
className
|
|
139
|
+
)}
|
|
140
|
+
onClick={(e) => e.stopPropagation()}
|
|
141
|
+
{...props}
|
|
142
|
+
>
|
|
143
|
+
{children}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
SelectContent.displayName = "SelectContent"
|
|
148
|
+
|
|
149
|
+
interface SelectItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
150
|
+
value: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const SelectItem = React.forwardRef<HTMLDivElement, SelectItemProps>(
|
|
154
|
+
({ className, children, value, ...props }, ref) => {
|
|
155
|
+
const context = React.useContext(SelectContext)
|
|
156
|
+
if (!context) throw new Error("SelectItem must be used within Select")
|
|
157
|
+
|
|
158
|
+
const isSelected = context.value === value
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
ref={ref}
|
|
163
|
+
role="option"
|
|
164
|
+
aria-selected={isSelected}
|
|
165
|
+
className={cn(
|
|
166
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
|
167
|
+
isSelected && "bg-accent text-accent-foreground",
|
|
168
|
+
className
|
|
169
|
+
)}
|
|
170
|
+
onClick={() => {
|
|
171
|
+
context.onValueChange(value)
|
|
172
|
+
context.setOpen(false)
|
|
173
|
+
}}
|
|
174
|
+
{...props}
|
|
175
|
+
>
|
|
176
|
+
{children}
|
|
177
|
+
{isSelected && (
|
|
178
|
+
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
179
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
180
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
181
|
+
</svg>
|
|
182
|
+
</span>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
SelectItem.displayName = "SelectItem"
|
|
189
|
+
|
|
190
|
+
export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/**
|
|
6
|
+
* Orientation of the separator
|
|
7
|
+
* @default "horizontal"
|
|
8
|
+
*/
|
|
9
|
+
orientation?: "horizontal" | "vertical"
|
|
10
|
+
/**
|
|
11
|
+
* Whether the separator is decorative (no semantic meaning)
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
decorative?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Separator component for visual division
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Horizontal separator
|
|
22
|
+
* <Separator />
|
|
23
|
+
*
|
|
24
|
+
* // Vertical separator
|
|
25
|
+
* <Separator orientation="vertical" className="h-4" />
|
|
26
|
+
*/
|
|
27
|
+
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
28
|
+
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
|
29
|
+
<div
|
|
30
|
+
ref={ref}
|
|
31
|
+
role={decorative ? "none" : "separator"}
|
|
32
|
+
aria-orientation={decorative ? undefined : orientation}
|
|
33
|
+
className={cn(
|
|
34
|
+
"shrink-0 bg-border",
|
|
35
|
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
Separator.displayName = "Separator"
|
|
43
|
+
|
|
44
|
+
export { Separator }
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
interface SheetContextValue {
|
|
6
|
+
open: boolean
|
|
7
|
+
onOpenChange: (open: boolean) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SheetContext = React.createContext<SheetContextValue | null>(null)
|
|
11
|
+
|
|
12
|
+
interface SheetProps {
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
open?: boolean
|
|
15
|
+
onOpenChange?: (open: boolean) => void
|
|
16
|
+
defaultOpen?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sheet (slide-in panel) component
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <Sheet>
|
|
24
|
+
* <SheetTrigger asChild>
|
|
25
|
+
* <Button>Open Sheet</Button>
|
|
26
|
+
* </SheetTrigger>
|
|
27
|
+
* <SheetContent side="right">
|
|
28
|
+
* <SheetHeader>
|
|
29
|
+
* <SheetTitle>Title</SheetTitle>
|
|
30
|
+
* <SheetDescription>Description</SheetDescription>
|
|
31
|
+
* </SheetHeader>
|
|
32
|
+
* </SheetContent>
|
|
33
|
+
* </Sheet>
|
|
34
|
+
*/
|
|
35
|
+
function Sheet({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: SheetProps) {
|
|
36
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
37
|
+
|
|
38
|
+
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
39
|
+
const setOpen = onOpenChange || setUncontrolledOpen
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<SheetContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
43
|
+
{children}
|
|
44
|
+
</SheetContext.Provider>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SheetTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
49
|
+
asChild?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SheetTrigger = React.forwardRef<HTMLButtonElement, SheetTriggerProps>(
|
|
53
|
+
({ onClick, asChild, children, ...props }, ref) => {
|
|
54
|
+
const context = React.useContext(SheetContext)
|
|
55
|
+
if (!context) throw new Error("SheetTrigger must be used within Sheet")
|
|
56
|
+
|
|
57
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
58
|
+
onClick?.(e)
|
|
59
|
+
context.onOpenChange(true)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (asChild && React.isValidElement(children)) {
|
|
63
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
64
|
+
onClick: handleClick,
|
|
65
|
+
ref,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<button ref={ref} onClick={handleClick} {...props}>
|
|
71
|
+
{children}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
SheetTrigger.displayName = "SheetTrigger"
|
|
77
|
+
|
|
78
|
+
const sheetVariants = cva(
|
|
79
|
+
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out",
|
|
80
|
+
{
|
|
81
|
+
variants: {
|
|
82
|
+
side: {
|
|
83
|
+
top: "inset-x-0 top-0 border-b",
|
|
84
|
+
bottom: "inset-x-0 bottom-0 border-t",
|
|
85
|
+
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
|
86
|
+
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
defaultVariants: {
|
|
90
|
+
side: "right",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
interface SheetContentProps
|
|
96
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
97
|
+
VariantProps<typeof sheetVariants> { }
|
|
98
|
+
|
|
99
|
+
const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
|
|
100
|
+
({ side = "right", className, children, ...props }, ref) => {
|
|
101
|
+
const context = React.useContext(SheetContext)
|
|
102
|
+
if (!context) throw new Error("SheetContent must be used within Sheet")
|
|
103
|
+
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
106
|
+
if (e.key === "Escape") {
|
|
107
|
+
context.onOpenChange(false)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (context.open) {
|
|
112
|
+
document.addEventListener("keydown", handleEscape)
|
|
113
|
+
document.body.style.overflow = "hidden"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
document.removeEventListener("keydown", handleEscape)
|
|
118
|
+
document.body.style.overflow = ""
|
|
119
|
+
}
|
|
120
|
+
}, [context.open, context])
|
|
121
|
+
|
|
122
|
+
if (!context.open) return null
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<div
|
|
127
|
+
className="fixed inset-0 z-50 bg-black/80"
|
|
128
|
+
onClick={() => context.onOpenChange(false)}
|
|
129
|
+
/>
|
|
130
|
+
<div
|
|
131
|
+
ref={ref}
|
|
132
|
+
role="dialog"
|
|
133
|
+
aria-modal="true"
|
|
134
|
+
className={cn(sheetVariants({ side }), className)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
<button
|
|
139
|
+
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
140
|
+
onClick={() => context.onOpenChange(false)}
|
|
141
|
+
>
|
|
142
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
143
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
144
|
+
</svg>
|
|
145
|
+
<span className="sr-only">Close</span>
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
</>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
SheetContent.displayName = "SheetContent"
|
|
153
|
+
|
|
154
|
+
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
155
|
+
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
|
156
|
+
)
|
|
157
|
+
SheetHeader.displayName = "SheetHeader"
|
|
158
|
+
|
|
159
|
+
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
160
|
+
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
|
161
|
+
)
|
|
162
|
+
SheetFooter.displayName = "SheetFooter"
|
|
163
|
+
|
|
164
|
+
const SheetTitle = React.forwardRef<
|
|
165
|
+
HTMLHeadingElement,
|
|
166
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
167
|
+
>(({ className, ...props }, ref) => (
|
|
168
|
+
<h2 ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
|
|
169
|
+
))
|
|
170
|
+
SheetTitle.displayName = "SheetTitle"
|
|
171
|
+
|
|
172
|
+
const SheetDescription = React.forwardRef<
|
|
173
|
+
HTMLParagraphElement,
|
|
174
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
175
|
+
>(({ className, ...props }, ref) => (
|
|
176
|
+
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
177
|
+
))
|
|
178
|
+
SheetDescription.displayName = "SheetDescription"
|
|
179
|
+
|
|
180
|
+
export { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Skeleton loading placeholder
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Skeleton className="h-4 w-[200px]" />
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Circle skeleton for avatars
|
|
12
|
+
* <Skeleton className="h-12 w-12 rounded-full" />
|
|
13
|
+
*/
|
|
14
|
+
const Skeleton = React.forwardRef<
|
|
15
|
+
HTMLDivElement,
|
|
16
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
17
|
+
>(({ className, ...props }, ref) => (
|
|
18
|
+
<div
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
))
|
|
24
|
+
Skeleton.displayName = "Skeleton"
|
|
25
|
+
|
|
26
|
+
export { Skeleton }
|