@srcroot/ui 0.0.1 → 0.0.3
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/dist/index.js +91 -0
- package/package.json +9 -3
- package/registry/badge.tsx +9 -25
- package/registry/breadcrumb.tsx +1 -1
- package/registry/button-group.tsx +9 -29
- package/registry/button.tsx +20 -46
- package/registry/calendar.tsx +416 -142
- package/registry/card.tsx +21 -47
- package/registry/combobox.tsx +171 -0
- package/registry/command.tsx +300 -0
- package/registry/container.tsx +9 -25
- package/registry/context-menu.tsx +221 -0
- package/registry/date-picker.tsx +179 -0
- package/registry/drawer.tsx +241 -0
- package/registry/dropdown-menu.tsx +93 -74
- package/registry/file-upload.tsx +240 -0
- package/registry/hover-card.tsx +165 -0
- package/registry/image.tsx +2 -2
- package/registry/kbd.tsx +60 -0
- package/registry/menubar.tsx +246 -0
- package/registry/native-select.tsx +49 -0
- package/registry/pagination.tsx +3 -0
- package/registry/resizable.tsx +251 -0
- package/registry/scroll-area.tsx +119 -0
- package/registry/search.tsx +2 -1
- package/registry/sheet.tsx +63 -18
- package/registry/sidebar.tsx +512 -0
- package/registry/slider.tsx +133 -54
- package/registry/text.tsx +7 -16
- package/registry/toggle-group.tsx +129 -0
- package/registry/toggle.tsx +72 -0
- package/registry/tooltip.tsx +21 -3
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
// Context Menu Context
|
|
7
|
+
interface ContextMenuContextValue {
|
|
8
|
+
open: boolean
|
|
9
|
+
position: { x: number; y: number }
|
|
10
|
+
onOpenChange: (open: boolean) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ContextMenuContext = React.createContext<ContextMenuContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
function useContextMenu() {
|
|
16
|
+
const context = React.useContext(ContextMenuContext)
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error("useContextMenu must be used within a ContextMenu")
|
|
19
|
+
}
|
|
20
|
+
return context
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ContextMenu Root
|
|
24
|
+
interface ContextMenuProps {
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ContextMenu = ({ children }: ContextMenuProps) => {
|
|
29
|
+
const [open, setOpen] = React.useState(false)
|
|
30
|
+
const [position, setPosition] = React.useState({ x: 0, y: 0 })
|
|
31
|
+
|
|
32
|
+
const handleContextMenu = React.useCallback((e: React.MouseEvent) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
setPosition({ x: e.clientX, y: e.clientY })
|
|
35
|
+
setOpen(true)
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
// Close on outside click
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
if (!open) return
|
|
41
|
+
|
|
42
|
+
const handleClick = () => setOpen(false)
|
|
43
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
44
|
+
if (e.key === "Escape") setOpen(false)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
document.addEventListener("click", handleClick)
|
|
48
|
+
document.addEventListener("keydown", handleEscape)
|
|
49
|
+
return () => {
|
|
50
|
+
document.removeEventListener("click", handleClick)
|
|
51
|
+
document.removeEventListener("keydown", handleEscape)
|
|
52
|
+
}
|
|
53
|
+
}, [open])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<ContextMenuContext.Provider value={{ open, position, onOpenChange: setOpen }}>
|
|
57
|
+
<div onContextMenu={handleContextMenu}>
|
|
58
|
+
{children}
|
|
59
|
+
</div>
|
|
60
|
+
</ContextMenuContext.Provider>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ContextMenu Trigger (the area that can be right-clicked)
|
|
65
|
+
interface ContextMenuTriggerProps {
|
|
66
|
+
children: React.ReactNode
|
|
67
|
+
className?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ContextMenuTrigger = React.forwardRef<HTMLDivElement, ContextMenuTriggerProps>(
|
|
71
|
+
({ children, className, ...props }, ref) => {
|
|
72
|
+
return (
|
|
73
|
+
<div ref={ref} className={className} {...props}>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
ContextMenuTrigger.displayName = "ContextMenuTrigger"
|
|
80
|
+
|
|
81
|
+
// ContextMenu Content
|
|
82
|
+
interface ContextMenuContentProps {
|
|
83
|
+
children: React.ReactNode
|
|
84
|
+
className?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ContextMenuContent = React.forwardRef<HTMLDivElement, ContextMenuContentProps>(
|
|
88
|
+
({ children, className, ...props }, ref) => {
|
|
89
|
+
const { open, position } = useContextMenu()
|
|
90
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
91
|
+
const [adjustedPosition, setAdjustedPosition] = React.useState(position)
|
|
92
|
+
|
|
93
|
+
// Adjust position to keep menu in viewport
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
if (!open || !contentRef.current) return
|
|
96
|
+
|
|
97
|
+
const rect = contentRef.current.getBoundingClientRect()
|
|
98
|
+
const viewportWidth = window.innerWidth
|
|
99
|
+
const viewportHeight = window.innerHeight
|
|
100
|
+
|
|
101
|
+
let x = position.x
|
|
102
|
+
let y = position.y
|
|
103
|
+
|
|
104
|
+
// Adjust if overflowing right
|
|
105
|
+
if (x + rect.width > viewportWidth) {
|
|
106
|
+
x = viewportWidth - rect.width - 8
|
|
107
|
+
}
|
|
108
|
+
// Adjust if overflowing bottom
|
|
109
|
+
if (y + rect.height > viewportHeight) {
|
|
110
|
+
y = viewportHeight - rect.height - 8
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setAdjustedPosition({ x: Math.max(8, x), y: Math.max(8, y) })
|
|
114
|
+
}, [open, position])
|
|
115
|
+
|
|
116
|
+
if (!open) return null
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
ref={contentRef}
|
|
121
|
+
className={cn(
|
|
122
|
+
"fixed z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
123
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
124
|
+
className
|
|
125
|
+
)}
|
|
126
|
+
style={{
|
|
127
|
+
left: adjustedPosition.x,
|
|
128
|
+
top: adjustedPosition.y,
|
|
129
|
+
}}
|
|
130
|
+
onClick={(e) => e.stopPropagation()}
|
|
131
|
+
{...props}
|
|
132
|
+
>
|
|
133
|
+
{children}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
ContextMenuContent.displayName = "ContextMenuContent"
|
|
139
|
+
|
|
140
|
+
// ContextMenu Item
|
|
141
|
+
interface ContextMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
142
|
+
inset?: boolean
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const ContextMenuItem = React.forwardRef<HTMLButtonElement, ContextMenuItemProps>(
|
|
146
|
+
({ className, inset, children, ...props }, ref) => {
|
|
147
|
+
const { onOpenChange } = useContextMenu()
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<button
|
|
151
|
+
ref={ref}
|
|
152
|
+
className={cn(
|
|
153
|
+
"relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors",
|
|
154
|
+
"focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
155
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
156
|
+
inset && "pl-8",
|
|
157
|
+
className
|
|
158
|
+
)}
|
|
159
|
+
onClick={() => onOpenChange(false)}
|
|
160
|
+
{...props}
|
|
161
|
+
>
|
|
162
|
+
{children}
|
|
163
|
+
</button>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
ContextMenuItem.displayName = "ContextMenuItem"
|
|
168
|
+
|
|
169
|
+
// ContextMenu Separator
|
|
170
|
+
const ContextMenuSeparator = React.forwardRef<
|
|
171
|
+
HTMLDivElement,
|
|
172
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
173
|
+
>(({ className, ...props }, ref) => (
|
|
174
|
+
<div
|
|
175
|
+
ref={ref}
|
|
176
|
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
177
|
+
{...props}
|
|
178
|
+
/>
|
|
179
|
+
))
|
|
180
|
+
ContextMenuSeparator.displayName = "ContextMenuSeparator"
|
|
181
|
+
|
|
182
|
+
// ContextMenu Label
|
|
183
|
+
const ContextMenuLabel = React.forwardRef<
|
|
184
|
+
HTMLDivElement,
|
|
185
|
+
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }
|
|
186
|
+
>(({ className, inset, ...props }, ref) => (
|
|
187
|
+
<div
|
|
188
|
+
ref={ref}
|
|
189
|
+
className={cn(
|
|
190
|
+
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
191
|
+
inset && "pl-8",
|
|
192
|
+
className
|
|
193
|
+
)}
|
|
194
|
+
{...props}
|
|
195
|
+
/>
|
|
196
|
+
))
|
|
197
|
+
ContextMenuLabel.displayName = "ContextMenuLabel"
|
|
198
|
+
|
|
199
|
+
// ContextMenu Shortcut
|
|
200
|
+
const ContextMenuShortcut = ({
|
|
201
|
+
className,
|
|
202
|
+
...props
|
|
203
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
204
|
+
return (
|
|
205
|
+
<span
|
|
206
|
+
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
|
207
|
+
{...props}
|
|
208
|
+
/>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
|
212
|
+
|
|
213
|
+
export {
|
|
214
|
+
ContextMenu,
|
|
215
|
+
ContextMenuTrigger,
|
|
216
|
+
ContextMenuContent,
|
|
217
|
+
ContextMenuItem,
|
|
218
|
+
ContextMenuSeparator,
|
|
219
|
+
ContextMenuLabel,
|
|
220
|
+
ContextMenuShortcut,
|
|
221
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Calendar } from "./calendar"
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
|
|
6
|
+
import { Button } from "./button"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
// Calendar icon
|
|
10
|
+
const CalendarIcon = () => (
|
|
11
|
+
<svg
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
width="16"
|
|
14
|
+
height="16"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="2"
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeLinejoin="round"
|
|
21
|
+
className="mr-2 h-4 w-4 opacity-50"
|
|
22
|
+
>
|
|
23
|
+
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
|
|
24
|
+
<line x1="16" x2="16" y1="2" y2="6" />
|
|
25
|
+
<line x1="8" x2="8" y1="2" y2="6" />
|
|
26
|
+
<line x1="3" x2="21" y1="10" y2="10" />
|
|
27
|
+
</svg>
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// Format helpers
|
|
31
|
+
const formatDate = (date: Date) => {
|
|
32
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const formatRange = (dates: Date[]) => {
|
|
36
|
+
if (dates.length === 0) return null
|
|
37
|
+
if (dates.length === 1) return formatDate(dates[0])
|
|
38
|
+
return `${formatDate(dates[0])} → ${formatDate(dates[1])}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const formatMultiple = (dates: Date[]) => {
|
|
42
|
+
if (dates.length === 0) return null
|
|
43
|
+
return `${dates.length} date${dates.length > 1 ? 's' : ''} selected`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// DatePicker Props
|
|
47
|
+
interface DatePickerBaseProps {
|
|
48
|
+
/** Placeholder text when no date selected */
|
|
49
|
+
placeholder?: string
|
|
50
|
+
/** Whether the picker is disabled */
|
|
51
|
+
disabled?: boolean
|
|
52
|
+
/** Custom class name for the trigger button */
|
|
53
|
+
className?: string
|
|
54
|
+
/** Number of months to display */
|
|
55
|
+
numberOfMonths?: 1 | 2
|
|
56
|
+
/** Calendar size */
|
|
57
|
+
size?: "xs" | "sm" | "default" | "md" | "lg"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface DatePickerSingleProps extends DatePickerBaseProps {
|
|
61
|
+
mode?: "single"
|
|
62
|
+
selected?: Date
|
|
63
|
+
onSelect?: (date: Date | undefined) => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface DatePickerMultipleProps extends DatePickerBaseProps {
|
|
67
|
+
mode: "multiple"
|
|
68
|
+
selected?: Date[]
|
|
69
|
+
onSelect?: (dates: Date[]) => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface DatePickerRangeProps extends DatePickerBaseProps {
|
|
73
|
+
mode: "range"
|
|
74
|
+
selected?: Date[]
|
|
75
|
+
onSelect?: (dates: Date[]) => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type DatePickerProps = DatePickerSingleProps | DatePickerMultipleProps | DatePickerRangeProps
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* DatePicker - A complete date picker component
|
|
82
|
+
*
|
|
83
|
+
* Combines Calendar + Popover for a ready-to-use date selection experience.
|
|
84
|
+
* Supports single, multiple, and range selection modes.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // Single date
|
|
88
|
+
* <DatePicker selected={date} onSelect={setDate} />
|
|
89
|
+
*
|
|
90
|
+
* // Multiple dates
|
|
91
|
+
* <DatePicker mode="multiple" selected={dates} onSelect={setDates} />
|
|
92
|
+
*
|
|
93
|
+
* // Date range with dual months
|
|
94
|
+
* <DatePicker mode="range" numberOfMonths={2} selected={range} onSelect={setRange} />
|
|
95
|
+
*/
|
|
96
|
+
const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|
97
|
+
({
|
|
98
|
+
mode = "single",
|
|
99
|
+
selected,
|
|
100
|
+
onSelect,
|
|
101
|
+
placeholder,
|
|
102
|
+
disabled = false,
|
|
103
|
+
className,
|
|
104
|
+
numberOfMonths = 1,
|
|
105
|
+
size = "default",
|
|
106
|
+
...props
|
|
107
|
+
}, ref) => {
|
|
108
|
+
const [open, setOpen] = React.useState(false)
|
|
109
|
+
|
|
110
|
+
// Determine display text
|
|
111
|
+
const getDisplayText = () => {
|
|
112
|
+
if (mode === "single") {
|
|
113
|
+
return selected ? formatDate(selected as Date) : null
|
|
114
|
+
} else if (mode === "multiple") {
|
|
115
|
+
const dates = (selected as Date[]) || []
|
|
116
|
+
return dates.length > 0 ? formatMultiple(dates) : null
|
|
117
|
+
} else {
|
|
118
|
+
const dates = (selected as Date[]) || []
|
|
119
|
+
return dates.length > 0 ? formatRange(dates) : null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const displayText = getDisplayText()
|
|
124
|
+
const defaultPlaceholder = mode === "single" ? "Pick a date"
|
|
125
|
+
: mode === "multiple" ? "Pick dates"
|
|
126
|
+
: "Pick a date range"
|
|
127
|
+
|
|
128
|
+
// Handle selection
|
|
129
|
+
const handleSelect = (value: any) => {
|
|
130
|
+
if (mode === "single") {
|
|
131
|
+
(onSelect as ((date: Date | undefined) => void))?.(value)
|
|
132
|
+
setOpen(false) // Close on single selection
|
|
133
|
+
} else if (mode === "multiple") {
|
|
134
|
+
(onSelect as ((dates: Date[]) => void))?.(value || [])
|
|
135
|
+
} else {
|
|
136
|
+
const dates = value || []
|
|
137
|
+
; (onSelect as ((dates: Date[]) => void))?.(dates)
|
|
138
|
+
// Close when range is complete (2 dates)
|
|
139
|
+
if (dates.length === 2) {
|
|
140
|
+
setOpen(false)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
147
|
+
<PopoverTrigger asChild>
|
|
148
|
+
<Button
|
|
149
|
+
ref={ref}
|
|
150
|
+
variant="outline"
|
|
151
|
+
disabled={disabled}
|
|
152
|
+
className={cn(
|
|
153
|
+
"w-[280px] justify-start text-left font-normal",
|
|
154
|
+
!displayText && "text-muted-foreground",
|
|
155
|
+
numberOfMonths === 2 && "w-[320px]",
|
|
156
|
+
className
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
<CalendarIcon />
|
|
160
|
+
{displayText || <span>{placeholder || defaultPlaceholder}</span>}
|
|
161
|
+
</Button>
|
|
162
|
+
</PopoverTrigger>
|
|
163
|
+
<PopoverContent className="w-auto p-0">
|
|
164
|
+
<Calendar
|
|
165
|
+
mode={mode}
|
|
166
|
+
numberOfMonths={numberOfMonths}
|
|
167
|
+
size={size}
|
|
168
|
+
selected={selected}
|
|
169
|
+
onSelect={handleSelect}
|
|
170
|
+
className="rounded-md border-0 shadow-none"
|
|
171
|
+
/>
|
|
172
|
+
</PopoverContent>
|
|
173
|
+
</Popover>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
DatePicker.displayName = "DatePicker"
|
|
178
|
+
|
|
179
|
+
export { DatePicker, type DatePickerProps }
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
// Drawer Context
|
|
7
|
+
interface DrawerContextValue {
|
|
8
|
+
open: boolean
|
|
9
|
+
onOpenChange: (open: boolean) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DrawerContext = React.createContext<DrawerContextValue | null>(null)
|
|
13
|
+
|
|
14
|
+
function useDrawer() {
|
|
15
|
+
const context = React.useContext(DrawerContext)
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error("useDrawer must be used within a Drawer")
|
|
18
|
+
}
|
|
19
|
+
return context
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Drawer Root
|
|
23
|
+
interface DrawerProps {
|
|
24
|
+
children: React.ReactNode
|
|
25
|
+
open?: boolean
|
|
26
|
+
onOpenChange?: (open: boolean) => void
|
|
27
|
+
defaultOpen?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const Drawer = ({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DrawerProps) => {
|
|
31
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
32
|
+
const open = controlledOpen ?? uncontrolledOpen
|
|
33
|
+
const setOpen = onOpenChange ?? setUncontrolledOpen
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<DrawerContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
37
|
+
{children}
|
|
38
|
+
</DrawerContext.Provider>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Drawer Trigger
|
|
43
|
+
interface DrawerTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
44
|
+
asChild?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DrawerTrigger = React.forwardRef<HTMLButtonElement, DrawerTriggerProps>(
|
|
48
|
+
({ children, asChild, onClick, ...props }, ref) => {
|
|
49
|
+
const { onOpenChange } = useDrawer()
|
|
50
|
+
|
|
51
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
52
|
+
onClick?.(e)
|
|
53
|
+
onOpenChange(true)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (asChild && React.isValidElement(children)) {
|
|
57
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
58
|
+
onClick: handleClick,
|
|
59
|
+
ref,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<button ref={ref} onClick={handleClick} {...props}>
|
|
65
|
+
{children}
|
|
66
|
+
</button>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
DrawerTrigger.displayName = "DrawerTrigger"
|
|
71
|
+
|
|
72
|
+
// Drawer Close
|
|
73
|
+
const DrawerClose = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
74
|
+
({ children, onClick, ...props }, ref) => {
|
|
75
|
+
const { onOpenChange } = useDrawer()
|
|
76
|
+
|
|
77
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
78
|
+
onClick?.(e)
|
|
79
|
+
onOpenChange(false)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<button ref={ref} onClick={handleClick} {...props}>
|
|
84
|
+
{children}
|
|
85
|
+
</button>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
DrawerClose.displayName = "DrawerClose"
|
|
90
|
+
|
|
91
|
+
// Drawer Overlay (internal)
|
|
92
|
+
interface DrawerOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
93
|
+
isAnimating: boolean
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DrawerOverlay = React.forwardRef<HTMLDivElement, DrawerOverlayProps>(
|
|
97
|
+
({ className, isAnimating, ...props }, ref) => {
|
|
98
|
+
const { onOpenChange } = useDrawer()
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
ref={ref}
|
|
103
|
+
className={cn(
|
|
104
|
+
"fixed inset-0 z-50 bg-black/80 transition-opacity duration-300",
|
|
105
|
+
isAnimating ? "opacity-100" : "opacity-0",
|
|
106
|
+
className
|
|
107
|
+
)}
|
|
108
|
+
onClick={() => onOpenChange(false)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
DrawerOverlay.displayName = "DrawerOverlay"
|
|
115
|
+
|
|
116
|
+
// Drawer Content
|
|
117
|
+
interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
118
|
+
side?: "bottom" | "top"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
|
|
122
|
+
({ className, children, side = "bottom", ...props }, ref) => {
|
|
123
|
+
const { open, onOpenChange } = useDrawer()
|
|
124
|
+
const [isVisible, setIsVisible] = React.useState(false)
|
|
125
|
+
const [isAnimating, setIsAnimating] = React.useState(false)
|
|
126
|
+
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
if (open) {
|
|
129
|
+
// First make visible (off-screen)
|
|
130
|
+
setIsVisible(true)
|
|
131
|
+
// Use a small timeout to ensure the browser has painted the initial state
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
setIsAnimating(true)
|
|
134
|
+
}, 10)
|
|
135
|
+
return () => clearTimeout(timer)
|
|
136
|
+
} else {
|
|
137
|
+
// Start close animation
|
|
138
|
+
setIsAnimating(false)
|
|
139
|
+
// Wait for animation to complete before hiding
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
setIsVisible(false)
|
|
142
|
+
}, 300)
|
|
143
|
+
return () => clearTimeout(timer)
|
|
144
|
+
}
|
|
145
|
+
}, [open])
|
|
146
|
+
|
|
147
|
+
// Close on Escape
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (!open) return
|
|
150
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
151
|
+
if (e.key === "Escape") onOpenChange(false)
|
|
152
|
+
}
|
|
153
|
+
document.addEventListener("keydown", handleEscape)
|
|
154
|
+
return () => document.removeEventListener("keydown", handleEscape)
|
|
155
|
+
}, [open, onOpenChange])
|
|
156
|
+
|
|
157
|
+
// Prevent body scroll when open
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
if (open) {
|
|
160
|
+
document.body.style.overflow = "hidden"
|
|
161
|
+
} else {
|
|
162
|
+
document.body.style.overflow = ""
|
|
163
|
+
}
|
|
164
|
+
return () => {
|
|
165
|
+
document.body.style.overflow = ""
|
|
166
|
+
}
|
|
167
|
+
}, [open])
|
|
168
|
+
|
|
169
|
+
if (!isVisible) return null
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<>
|
|
173
|
+
<DrawerOverlay isAnimating={isAnimating} />
|
|
174
|
+
<div
|
|
175
|
+
ref={ref}
|
|
176
|
+
className={cn(
|
|
177
|
+
"fixed z-50 flex flex-col bg-background shadow-lg",
|
|
178
|
+
"transition-transform duration-300 ease-out",
|
|
179
|
+
side === "bottom" && "inset-x-0 bottom-0 rounded-t-xl border-t",
|
|
180
|
+
side === "top" && "inset-x-0 top-0 rounded-b-xl border-b",
|
|
181
|
+
// Animation states
|
|
182
|
+
side === "bottom" && (isAnimating ? "translate-y-0" : "translate-y-full"),
|
|
183
|
+
side === "top" && (isAnimating ? "translate-y-0" : "-translate-y-full"),
|
|
184
|
+
className
|
|
185
|
+
)}
|
|
186
|
+
{...props}
|
|
187
|
+
>
|
|
188
|
+
{/* Handle indicator */}
|
|
189
|
+
{side === "bottom" && (
|
|
190
|
+
<div className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
|
|
191
|
+
)}
|
|
192
|
+
{children}
|
|
193
|
+
{side === "top" && (
|
|
194
|
+
<div className="mx-auto mb-4 h-1.5 w-12 rounded-full bg-muted" />
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
DrawerContent.displayName = "DrawerContent"
|
|
202
|
+
|
|
203
|
+
// Drawer Header
|
|
204
|
+
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
205
|
+
<div className={cn("flex flex-col space-y-1.5 p-4 text-center sm:text-left", className)} {...props} />
|
|
206
|
+
)
|
|
207
|
+
DrawerHeader.displayName = "DrawerHeader"
|
|
208
|
+
|
|
209
|
+
// Drawer Footer
|
|
210
|
+
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
211
|
+
<div className={cn("flex flex-col-reverse gap-2 p-4 sm:flex-row sm:justify-end", className)} {...props} />
|
|
212
|
+
)
|
|
213
|
+
DrawerFooter.displayName = "DrawerFooter"
|
|
214
|
+
|
|
215
|
+
// Drawer Title
|
|
216
|
+
const DrawerTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
217
|
+
({ className, ...props }, ref) => (
|
|
218
|
+
<h2 ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
DrawerTitle.displayName = "DrawerTitle"
|
|
222
|
+
|
|
223
|
+
// Drawer Description
|
|
224
|
+
const DrawerDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
225
|
+
({ className, ...props }, ref) => (
|
|
226
|
+
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
DrawerDescription.displayName = "DrawerDescription"
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
Drawer,
|
|
233
|
+
DrawerTrigger,
|
|
234
|
+
DrawerClose,
|
|
235
|
+
DrawerOverlay,
|
|
236
|
+
DrawerContent,
|
|
237
|
+
DrawerHeader,
|
|
238
|
+
DrawerFooter,
|
|
239
|
+
DrawerTitle,
|
|
240
|
+
DrawerDescription,
|
|
241
|
+
}
|