@srcroot/ui 0.0.54 → 0.0.56
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 -151
- package/dist/index.d.ts +0 -0
- package/dist/index.js +55 -1
- package/package.json +7 -2
- package/src/registry/analytics/google-analytics.tsx +36 -39
- package/src/registry/analytics/google-tag-manager.tsx +62 -65
- package/src/registry/analytics/meta-pixel.tsx +44 -47
- package/src/registry/analytics/microsoft-clarity.tsx +31 -34
- package/src/registry/analytics/tiktok-pixel.tsx +34 -37
- package/src/registry/lib/utils.ts +0 -0
- package/src/registry/themes/v3/blue.css +157 -157
- package/src/registry/themes/v3/glass.css +153 -153
- package/src/registry/themes/v3/gray.css +157 -157
- package/src/registry/themes/v3/green.css +157 -157
- package/src/registry/themes/v3/neutral.css +157 -157
- package/src/registry/themes/v3/orange.css +157 -157
- package/src/registry/themes/v3/rose.css +157 -157
- package/src/registry/themes/v3/slate.css +157 -157
- package/src/registry/themes/v3/stone.css +157 -157
- package/src/registry/themes/v3/violet.css +186 -186
- package/src/registry/themes/v3/zinc.css +157 -157
- package/src/registry/themes/v4/blue.css +184 -184
- package/src/registry/themes/v4/glass.css +180 -180
- package/src/registry/themes/v4/gray.css +184 -184
- package/src/registry/themes/v4/green.css +184 -184
- package/src/registry/themes/v4/neutral.css +184 -184
- package/src/registry/themes/v4/orange.css +184 -184
- package/src/registry/themes/v4/rose.css +184 -184
- package/src/registry/themes/v4/slate.css +184 -184
- package/src/registry/themes/v4/stone.css +184 -184
- package/src/registry/themes/v4/violet.css +184 -184
- package/src/registry/themes/v4/zinc.css +184 -184
- package/src/registry/ui/accordion.tsx +164 -165
- package/src/registry/ui/alert-dialog.tsx +213 -214
- package/src/registry/ui/alert.tsx +73 -76
- package/src/registry/ui/aspect-ratio.tsx +44 -47
- package/src/registry/ui/avatar.tsx +96 -97
- package/src/registry/ui/badge.tsx +52 -55
- package/src/registry/ui/breadcrumb.tsx +147 -150
- package/src/registry/ui/button-group.tsx +64 -67
- package/src/registry/ui/button.tsx +71 -72
- package/src/registry/ui/calendar.tsx +514 -515
- package/src/registry/ui/card.tsx +88 -91
- package/src/registry/ui/carousel.tsx +214 -214
- package/src/registry/ui/chart.tsx +373 -373
- package/src/registry/ui/chatbot.tsx +86 -13
- package/src/registry/ui/checkbox.tsx +93 -94
- package/src/registry/ui/collapsible.tsx +107 -108
- package/src/registry/ui/combobox.tsx +171 -171
- package/src/registry/ui/command.tsx +300 -300
- package/src/registry/ui/container.tsx +44 -47
- package/src/registry/ui/context-menu.tsx +221 -221
- package/src/registry/ui/date-picker.tsx +228 -228
- package/src/registry/ui/dialog.tsx +269 -270
- package/src/registry/ui/drawer.tsx +10 -4
- package/src/registry/ui/dropdown-menu.tsx +529 -530
- package/src/registry/ui/empty-state.tsx +0 -2
- package/src/registry/ui/file-upload.tsx +0 -0
- package/src/registry/ui/floating-dock.tsx +0 -0
- package/src/registry/ui/form-field.tsx +91 -94
- package/src/registry/ui/google-analytics.tsx +38 -0
- package/src/registry/ui/google-tag-manager.tsx +64 -0
- package/src/registry/ui/hover-card.tsx +223 -223
- package/src/registry/ui/image.tsx +144 -147
- package/src/registry/ui/input-group.tsx +82 -85
- package/src/registry/ui/input.tsx +125 -125
- package/src/registry/ui/kbd.tsx +60 -63
- package/src/registry/ui/label.tsx +36 -37
- package/src/registry/ui/loading-spinner.tsx +108 -111
- package/src/registry/ui/map.tsx +0 -0
- package/src/registry/ui/marquee.tsx +2 -0
- package/src/registry/ui/menubar.tsx +246 -246
- package/src/registry/ui/meta-pixel.tsx +46 -0
- package/src/registry/ui/microsoft-clarity.tsx +33 -0
- package/src/registry/ui/native-select.tsx +49 -52
- package/src/registry/ui/otp-input.tsx +152 -155
- package/src/registry/ui/pagination.tsx +149 -152
- package/src/registry/ui/patterns.tsx +28 -0
- package/src/registry/ui/popover.tsx +226 -227
- package/src/registry/ui/progress.tsx +51 -52
- package/src/registry/ui/radio.tsx +99 -102
- package/src/registry/ui/resizable.tsx +314 -314
- package/src/registry/ui/scroll-animation.tsx +45 -0
- package/src/registry/ui/scroll-area.tsx +121 -122
- package/src/registry/ui/scroll-to-top.tsx +0 -0
- package/src/registry/ui/search.tsx +147 -150
- package/src/registry/ui/select.tsx +292 -293
- package/src/registry/ui/separator.tsx +46 -47
- package/src/registry/ui/sheet.tsx +6 -3
- package/src/registry/ui/sidebar.tsx +628 -628
- package/src/registry/ui/skeleton.tsx +26 -29
- package/src/registry/ui/slider.tsx +196 -197
- package/src/registry/ui/slot.tsx +69 -72
- package/src/registry/ui/star-rating.tsx +131 -134
- package/src/registry/ui/switch.tsx +72 -73
- package/src/registry/ui/table-of-contents.tsx +96 -96
- package/src/registry/ui/table.tsx +138 -139
- package/src/registry/ui/tabs.tsx +124 -125
- package/src/registry/ui/text.tsx +61 -64
- package/src/registry/ui/textarea.tsx +41 -42
- package/src/registry/ui/theme-switcher.tsx +66 -66
- package/src/registry/ui/tiktok-pixel.tsx +36 -0
- package/src/registry/ui/toast.tsx +97 -98
- package/src/registry/ui/toggle-group.tsx +129 -129
- package/src/registry/ui/toggle.tsx +72 -72
- package/src/registry/ui/tooltip.tsx +143 -144
- package/src/registry/ui/whatsapp.tsx +0 -0
|
@@ -1,270 +1,269 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { createPortal } from "react-dom"
|
|
5
|
-
import { cn } from "@/lib/utils"
|
|
6
|
-
import { Slot } from "@/components/ui/slot"
|
|
7
|
-
|
|
8
|
-
interface DialogContextValue {
|
|
9
|
-
open: boolean
|
|
10
|
-
onOpenChange: (open: boolean) => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const DialogContext = React.createContext<DialogContextValue | null>(null)
|
|
14
|
-
|
|
15
|
-
function useDialogContext() {
|
|
16
|
-
const context = React.useContext(DialogContext)
|
|
17
|
-
if (!context) {
|
|
18
|
-
throw new Error("Dialog components must be used within a Dialog")
|
|
19
|
-
}
|
|
20
|
-
return context
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface DialogProps {
|
|
24
|
-
children: React.ReactNode
|
|
25
|
-
open?: boolean
|
|
26
|
-
onOpenChange?: (open: boolean) => void
|
|
27
|
-
defaultOpen?: boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Dialog component with focus trap and keyboard handling
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* const [open, setOpen] = useState(false)
|
|
35
|
-
*
|
|
36
|
-
* <Dialog open={open} onOpenChange={setOpen}>
|
|
37
|
-
* <DialogTrigger asChild>
|
|
38
|
-
* <Button>Open Dialog</Button>
|
|
39
|
-
* </DialogTrigger>
|
|
40
|
-
* <DialogContent>
|
|
41
|
-
* <DialogHeader>
|
|
42
|
-
* <DialogTitle>Title</DialogTitle>
|
|
43
|
-
* <DialogDescription>Description</DialogDescription>
|
|
44
|
-
* </DialogHeader>
|
|
45
|
-
* <div>Content</div>
|
|
46
|
-
* <DialogFooter>
|
|
47
|
-
* <Button>Save</Button>
|
|
48
|
-
* </DialogFooter>
|
|
49
|
-
* </DialogContent>
|
|
50
|
-
* </Dialog>
|
|
51
|
-
*/
|
|
52
|
-
function Dialog({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DialogProps) {
|
|
53
|
-
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
54
|
-
|
|
55
|
-
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
56
|
-
const setOpen = onOpenChange || setUncontrolledOpen
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<DialogContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
60
|
-
{children}
|
|
61
|
-
</DialogContext.Provider>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface DialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
66
|
-
asChild?: boolean
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const DialogTrigger = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
|
|
70
|
-
({ onClick, asChild, children, ...props }, ref) => {
|
|
71
|
-
const { onOpenChange } = useDialogContext()
|
|
72
|
-
|
|
73
|
-
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
74
|
-
onClick?.(e)
|
|
75
|
-
onOpenChange(true)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const Comp = asChild ? Slot : "button"
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<Comp ref={ref} onClick={handleClick} {...props}>
|
|
82
|
-
{children}
|
|
83
|
-
</Comp>
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
)
|
|
87
|
-
DialogTrigger.displayName = "DialogTrigger"
|
|
88
|
-
|
|
89
|
-
const DialogPortal = ({ children }: { children: React.ReactNode }) => {
|
|
90
|
-
const { open } = useDialogContext()
|
|
91
|
-
const [mounted, setMounted] = React.useState(false)
|
|
92
|
-
|
|
93
|
-
React.useEffect(() => {
|
|
94
|
-
setMounted(true)
|
|
95
|
-
}, [])
|
|
96
|
-
|
|
97
|
-
if (!open) return null
|
|
98
|
-
if (!mounted) return null
|
|
99
|
-
|
|
100
|
-
return createPortal(children, document.body)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const DialogOverlay = React.forwardRef<
|
|
104
|
-
HTMLDivElement,
|
|
105
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
106
|
-
>(({ className, ...props }, ref) => (
|
|
107
|
-
<div
|
|
108
|
-
ref={ref}
|
|
109
|
-
className={cn(
|
|
110
|
-
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
111
|
-
className
|
|
112
|
-
)}
|
|
113
|
-
{...props}
|
|
114
|
-
/>
|
|
115
|
-
))
|
|
116
|
-
DialogOverlay.displayName = "DialogOverlay"
|
|
117
|
-
|
|
118
|
-
const DialogContent = React.forwardRef<
|
|
119
|
-
HTMLDivElement,
|
|
120
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
121
|
-
>(({ className, children, ...props }, ref) => {
|
|
122
|
-
const { open, onOpenChange } = useDialogContext()
|
|
123
|
-
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
124
|
-
|
|
125
|
-
// Handle escape key
|
|
126
|
-
React.useEffect(() => {
|
|
127
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
128
|
-
if (e.key === "Escape") {
|
|
129
|
-
onOpenChange(false)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (open) {
|
|
134
|
-
document.addEventListener("keydown", handleEscape)
|
|
135
|
-
document.body.style.overflow = "hidden"
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return () => {
|
|
139
|
-
document.removeEventListener("keydown", handleEscape)
|
|
140
|
-
document.body.style.overflow = ""
|
|
141
|
-
}
|
|
142
|
-
}, [open, onOpenChange])
|
|
143
|
-
|
|
144
|
-
// Focus trap
|
|
145
|
-
React.useEffect(() => {
|
|
146
|
-
if (open && contentRef.current) {
|
|
147
|
-
const focusableElements = contentRef.current.querySelectorAll(
|
|
148
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
149
|
-
)
|
|
150
|
-
const firstElement = focusableElements[0] as HTMLElement
|
|
151
|
-
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement
|
|
152
|
-
|
|
153
|
-
const handleTab = (e: KeyboardEvent) => {
|
|
154
|
-
if (e.key === "Tab") {
|
|
155
|
-
if (e.shiftKey && document.activeElement === firstElement) {
|
|
156
|
-
e.preventDefault()
|
|
157
|
-
lastElement?.focus()
|
|
158
|
-
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
159
|
-
e.preventDefault()
|
|
160
|
-
firstElement?.focus()
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
document.addEventListener("keydown", handleTab)
|
|
166
|
-
firstElement?.focus()
|
|
167
|
-
|
|
168
|
-
return () => document.removeEventListener("keydown", handleTab)
|
|
169
|
-
}
|
|
170
|
-
}, [open])
|
|
171
|
-
|
|
172
|
-
if (!open) return null
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<DialogPortal>
|
|
176
|
-
<DialogOverlay onClick={() => onOpenChange(false)} />
|
|
177
|
-
<div
|
|
178
|
-
ref={(node) => {
|
|
179
|
-
(contentRef as any).current = node
|
|
180
|
-
if (typeof ref === "function") ref(node)
|
|
181
|
-
else if (ref) ref.current = node
|
|
182
|
-
}}
|
|
183
|
-
role="dialog"
|
|
184
|
-
aria-modal="true"
|
|
185
|
-
className={cn(
|
|
186
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg",
|
|
187
|
-
className
|
|
188
|
-
)}
|
|
189
|
-
{...props}
|
|
190
|
-
>
|
|
191
|
-
{children}
|
|
192
|
-
<button
|
|
193
|
-
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 disabled:pointer-events-none"
|
|
194
|
-
onClick={() => onOpenChange(false)}
|
|
195
|
-
>
|
|
196
|
-
<svg
|
|
197
|
-
className="h-4 w-4"
|
|
198
|
-
fill="none"
|
|
199
|
-
viewBox="0 0 24 24"
|
|
200
|
-
stroke="currentColor"
|
|
201
|
-
strokeWidth={2}
|
|
202
|
-
>
|
|
203
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
204
|
-
</svg>
|
|
205
|
-
<span className="sr-only">Close</span>
|
|
206
|
-
</button>
|
|
207
|
-
</div>
|
|
208
|
-
</DialogPortal>
|
|
209
|
-
)
|
|
210
|
-
})
|
|
211
|
-
DialogContent.displayName = "DialogContent"
|
|
212
|
-
|
|
213
|
-
const DialogHeader = ({
|
|
214
|
-
className,
|
|
215
|
-
...props
|
|
216
|
-
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
217
|
-
<div
|
|
218
|
-
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
|
219
|
-
{...props}
|
|
220
|
-
/>
|
|
221
|
-
)
|
|
222
|
-
DialogHeader.displayName = "DialogHeader"
|
|
223
|
-
|
|
224
|
-
const DialogFooter = ({
|
|
225
|
-
className,
|
|
226
|
-
...props
|
|
227
|
-
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
228
|
-
<div
|
|
229
|
-
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
230
|
-
{...props}
|
|
231
|
-
/>
|
|
232
|
-
)
|
|
233
|
-
DialogFooter.displayName = "DialogFooter"
|
|
234
|
-
|
|
235
|
-
const DialogTitle = React.forwardRef<
|
|
236
|
-
HTMLHeadingElement,
|
|
237
|
-
React.HTMLAttributes<HTMLHeadingElement>
|
|
238
|
-
>(({ className, ...props }, ref) => (
|
|
239
|
-
<h2
|
|
240
|
-
ref={ref}
|
|
241
|
-
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
|
242
|
-
{...props}
|
|
243
|
-
/>
|
|
244
|
-
))
|
|
245
|
-
DialogTitle.displayName = "DialogTitle"
|
|
246
|
-
|
|
247
|
-
const DialogDescription = React.forwardRef<
|
|
248
|
-
HTMLParagraphElement,
|
|
249
|
-
React.HTMLAttributes<HTMLParagraphElement>
|
|
250
|
-
>(({ className, ...props }, ref) => (
|
|
251
|
-
<p
|
|
252
|
-
ref={ref}
|
|
253
|
-
className={cn("text-sm text-muted-foreground", className)}
|
|
254
|
-
{...props}
|
|
255
|
-
/>
|
|
256
|
-
))
|
|
257
|
-
DialogDescription.displayName = "DialogDescription"
|
|
258
|
-
|
|
259
|
-
export {
|
|
260
|
-
Dialog,
|
|
261
|
-
DialogPortal,
|
|
262
|
-
DialogOverlay,
|
|
263
|
-
DialogTrigger,
|
|
264
|
-
DialogContent,
|
|
265
|
-
DialogHeader,
|
|
266
|
-
DialogFooter,
|
|
267
|
-
DialogTitle,
|
|
268
|
-
DialogDescription,
|
|
269
|
-
}
|
|
270
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { createPortal } from "react-dom"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Slot } from "@/components/ui/slot"
|
|
7
|
+
|
|
8
|
+
interface DialogContextValue {
|
|
9
|
+
open: boolean
|
|
10
|
+
onOpenChange: (open: boolean) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DialogContext = React.createContext<DialogContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
function useDialogContext() {
|
|
16
|
+
const context = React.useContext(DialogContext)
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error("Dialog components must be used within a Dialog")
|
|
19
|
+
}
|
|
20
|
+
return context
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DialogProps {
|
|
24
|
+
children: React.ReactNode
|
|
25
|
+
open?: boolean
|
|
26
|
+
onOpenChange?: (open: boolean) => void
|
|
27
|
+
defaultOpen?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Dialog component with focus trap and keyboard handling
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const [open, setOpen] = useState(false)
|
|
35
|
+
*
|
|
36
|
+
* <Dialog open={open} onOpenChange={setOpen}>
|
|
37
|
+
* <DialogTrigger asChild>
|
|
38
|
+
* <Button>Open Dialog</Button>
|
|
39
|
+
* </DialogTrigger>
|
|
40
|
+
* <DialogContent>
|
|
41
|
+
* <DialogHeader>
|
|
42
|
+
* <DialogTitle>Title</DialogTitle>
|
|
43
|
+
* <DialogDescription>Description</DialogDescription>
|
|
44
|
+
* </DialogHeader>
|
|
45
|
+
* <div>Content</div>
|
|
46
|
+
* <DialogFooter>
|
|
47
|
+
* <Button>Save</Button>
|
|
48
|
+
* </DialogFooter>
|
|
49
|
+
* </DialogContent>
|
|
50
|
+
* </Dialog>
|
|
51
|
+
*/
|
|
52
|
+
function Dialog({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DialogProps) {
|
|
53
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
54
|
+
|
|
55
|
+
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
56
|
+
const setOpen = onOpenChange || setUncontrolledOpen
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<DialogContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
60
|
+
{children}
|
|
61
|
+
</DialogContext.Provider>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface DialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
66
|
+
asChild?: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DialogTrigger = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
|
|
70
|
+
({ onClick, asChild, children, ...props }, ref) => {
|
|
71
|
+
const { onOpenChange } = useDialogContext()
|
|
72
|
+
|
|
73
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
74
|
+
onClick?.(e)
|
|
75
|
+
onOpenChange(true)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const Comp = asChild ? Slot : "button"
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Comp ref={ref} onClick={handleClick} {...props}>
|
|
82
|
+
{children}
|
|
83
|
+
</Comp>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
DialogTrigger.displayName = "DialogTrigger"
|
|
88
|
+
|
|
89
|
+
const DialogPortal = ({ children }: { children: React.ReactNode }) => {
|
|
90
|
+
const { open } = useDialogContext()
|
|
91
|
+
const [mounted, setMounted] = React.useState(false)
|
|
92
|
+
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
setMounted(true)
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
if (!open) return null
|
|
98
|
+
if (!mounted) return null
|
|
99
|
+
|
|
100
|
+
return createPortal(children, document.body)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const DialogOverlay = React.forwardRef<
|
|
104
|
+
HTMLDivElement,
|
|
105
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
106
|
+
>(({ className, ...props }, ref) => (
|
|
107
|
+
<div
|
|
108
|
+
ref={ref}
|
|
109
|
+
className={cn(
|
|
110
|
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
111
|
+
className
|
|
112
|
+
)}
|
|
113
|
+
{...props}
|
|
114
|
+
/>
|
|
115
|
+
))
|
|
116
|
+
DialogOverlay.displayName = "DialogOverlay"
|
|
117
|
+
|
|
118
|
+
const DialogContent = React.forwardRef<
|
|
119
|
+
HTMLDivElement,
|
|
120
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
121
|
+
>(({ className, children, ...props }, ref) => {
|
|
122
|
+
const { open, onOpenChange } = useDialogContext()
|
|
123
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
124
|
+
|
|
125
|
+
// Handle escape key
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
128
|
+
if (e.key === "Escape") {
|
|
129
|
+
onOpenChange(false)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (open) {
|
|
134
|
+
document.addEventListener("keydown", handleEscape)
|
|
135
|
+
document.body.style.overflow = "hidden"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return () => {
|
|
139
|
+
document.removeEventListener("keydown", handleEscape)
|
|
140
|
+
document.body.style.overflow = ""
|
|
141
|
+
}
|
|
142
|
+
}, [open, onOpenChange])
|
|
143
|
+
|
|
144
|
+
// Focus trap
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
if (open && contentRef.current) {
|
|
147
|
+
const focusableElements = contentRef.current.querySelectorAll(
|
|
148
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
149
|
+
)
|
|
150
|
+
const firstElement = focusableElements[0] as HTMLElement
|
|
151
|
+
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement
|
|
152
|
+
|
|
153
|
+
const handleTab = (e: KeyboardEvent) => {
|
|
154
|
+
if (e.key === "Tab") {
|
|
155
|
+
if (e.shiftKey && document.activeElement === firstElement) {
|
|
156
|
+
e.preventDefault()
|
|
157
|
+
lastElement?.focus()
|
|
158
|
+
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
159
|
+
e.preventDefault()
|
|
160
|
+
firstElement?.focus()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
document.addEventListener("keydown", handleTab)
|
|
166
|
+
firstElement?.focus()
|
|
167
|
+
|
|
168
|
+
return () => document.removeEventListener("keydown", handleTab)
|
|
169
|
+
}
|
|
170
|
+
}, [open])
|
|
171
|
+
|
|
172
|
+
if (!open) return null
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<DialogPortal>
|
|
176
|
+
<DialogOverlay onClick={() => onOpenChange(false)} />
|
|
177
|
+
<div
|
|
178
|
+
ref={(node) => {
|
|
179
|
+
(contentRef as any).current = node
|
|
180
|
+
if (typeof ref === "function") ref(node)
|
|
181
|
+
else if (ref) ref.current = node
|
|
182
|
+
}}
|
|
183
|
+
role="dialog"
|
|
184
|
+
aria-modal="true"
|
|
185
|
+
className={cn(
|
|
186
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg",
|
|
187
|
+
className
|
|
188
|
+
)}
|
|
189
|
+
{...props}
|
|
190
|
+
>
|
|
191
|
+
{children}
|
|
192
|
+
<button
|
|
193
|
+
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 disabled:pointer-events-none"
|
|
194
|
+
onClick={() => onOpenChange(false)}
|
|
195
|
+
>
|
|
196
|
+
<svg
|
|
197
|
+
className="h-4 w-4"
|
|
198
|
+
fill="none"
|
|
199
|
+
viewBox="0 0 24 24"
|
|
200
|
+
stroke="currentColor"
|
|
201
|
+
strokeWidth={2}
|
|
202
|
+
>
|
|
203
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
204
|
+
</svg>
|
|
205
|
+
<span className="sr-only">Close</span>
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
</DialogPortal>
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
DialogContent.displayName = "DialogContent"
|
|
212
|
+
|
|
213
|
+
const DialogHeader = ({
|
|
214
|
+
className,
|
|
215
|
+
...props
|
|
216
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
217
|
+
<div
|
|
218
|
+
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
|
219
|
+
{...props}
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
DialogHeader.displayName = "DialogHeader"
|
|
223
|
+
|
|
224
|
+
const DialogFooter = ({
|
|
225
|
+
className,
|
|
226
|
+
...props
|
|
227
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
228
|
+
<div
|
|
229
|
+
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
230
|
+
{...props}
|
|
231
|
+
/>
|
|
232
|
+
)
|
|
233
|
+
DialogFooter.displayName = "DialogFooter"
|
|
234
|
+
|
|
235
|
+
const DialogTitle = React.forwardRef<
|
|
236
|
+
HTMLHeadingElement,
|
|
237
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
238
|
+
>(({ className, ...props }, ref) => (
|
|
239
|
+
<h2
|
|
240
|
+
ref={ref}
|
|
241
|
+
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
|
242
|
+
{...props}
|
|
243
|
+
/>
|
|
244
|
+
))
|
|
245
|
+
DialogTitle.displayName = "DialogTitle"
|
|
246
|
+
|
|
247
|
+
const DialogDescription = React.forwardRef<
|
|
248
|
+
HTMLParagraphElement,
|
|
249
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
250
|
+
>(({ className, ...props }, ref) => (
|
|
251
|
+
<p
|
|
252
|
+
ref={ref}
|
|
253
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
254
|
+
{...props}
|
|
255
|
+
/>
|
|
256
|
+
))
|
|
257
|
+
DialogDescription.displayName = "DialogDescription"
|
|
258
|
+
|
|
259
|
+
export {
|
|
260
|
+
Dialog,
|
|
261
|
+
DialogPortal,
|
|
262
|
+
DialogOverlay,
|
|
263
|
+
DialogTrigger,
|
|
264
|
+
DialogContent,
|
|
265
|
+
DialogHeader,
|
|
266
|
+
DialogFooter,
|
|
267
|
+
DialogTitle,
|
|
268
|
+
DialogDescription,
|
|
269
|
+
}
|
|
@@ -86,8 +86,12 @@ const DrawerTrigger = React.forwardRef<HTMLButtonElement, DrawerTriggerProps>(
|
|
|
86
86
|
DrawerTrigger.displayName = "DrawerTrigger"
|
|
87
87
|
|
|
88
88
|
// Drawer Close
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
interface DrawerCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
90
|
+
asChild?: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DrawerClose = React.forwardRef<HTMLButtonElement, DrawerCloseProps>(
|
|
94
|
+
({ children, onClick, asChild, ...props }, ref) => {
|
|
91
95
|
const { onOpenChange } = useDrawer()
|
|
92
96
|
|
|
93
97
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
@@ -95,10 +99,12 @@ const DrawerClose = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttribut
|
|
|
95
99
|
onOpenChange(false)
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
const Comp = asChild ? Slot : "button"
|
|
103
|
+
|
|
98
104
|
return (
|
|
99
|
-
<
|
|
105
|
+
<Comp ref={ref} onClick={handleClick} {...props}>
|
|
100
106
|
{children}
|
|
101
|
-
</
|
|
107
|
+
</Comp>
|
|
102
108
|
)
|
|
103
109
|
}
|
|
104
110
|
)
|