1"use client";
2
3import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
4import { XIcon } from "lucide-react";
5import { cn } from "~/lib/utils";
6import { Button } from "~/components/ui/button";
7import {
8 DIALOG_BACKDROP_CLASS,
9 DIALOG_MEDIA_BACKDROP_CLASS,
10 DIALOG_MEDIA_POPUP_CLASS,
11 DIALOG_MOBILE_SHEET_CLASS,
12 DIALOG_POPUP_CLASS,
13} from "~/components/ui/dialog-styles";
14import { ScrollArea } from "~/components/ui/scroll-area";
15
16const Dialog = DialogPrimitive.Root;
17
18const DialogPortal = DialogPrimitive.Portal;
19
20function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
21 return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
22}
23
24function DialogClose(props: DialogPrimitive.Close.Props) {
25 return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
26}
27
28function DialogBackdrop({
29 className,
30 variant = "default",
31 ...props
32}: DialogPrimitive.Backdrop.Props & { variant?: "default" | "media" }) {
33 return (
34 <DialogPrimitive.Backdrop
35 forceRender
36 className={cn(
37 variant === "media" ? DIALOG_MEDIA_BACKDROP_CLASS : DIALOG_BACKDROP_CLASS,
38 className,
39 )}
40 data-slot="dialog-backdrop"
41 {...props}
42 />
43 );
44}
45
46function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props) {
47 return (
48 <DialogPrimitive.Viewport
49 className={cn(
50 "fixed inset-0 z-50 grid grid-rows-[1fr_auto_1fr] justify-items-center p-4",
51 className,
52 )}
53 data-slot="dialog-viewport"
54 {...props}
55 />
56 );
57}
58
59function DialogPopup({
60 className,
61 children,
62 showCloseButton = true,
63 bottomStickOnMobile = true,
64 variant = "default",
65 ...props
66}: DialogPrimitive.Popup.Props & {
67 showCloseButton?: boolean;
68 bottomStickOnMobile?: boolean;
69 variant?: "default" | "media";
70}) {
71 return (
72 <DialogPortal>
73 {/* Media opens from inside other overlays (a composer chip, a popover), so it sits above them. */}
74 <DialogBackdrop className={variant === "media" ? "z-[60]" : undefined} variant={variant} />
75 <DialogViewport
76 className={cn(
77 bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
78 variant === "media" &&
79 "z-[60] grid-rows-1 place-items-center px-4 py-6 [-webkit-app-region:no-drag]",
80 )}
81 >
82 <DialogPrimitive.Popup
83 className={cn(
84 variant === "media" ? DIALOG_MEDIA_POPUP_CLASS : DIALOG_POPUP_CLASS,
85 "row-start-2 text-popover-foreground",
86 variant === "default" && "max-h-full max-w-lg",
87 bottomStickOnMobile && DIALOG_MOBILE_SHEET_CLASS,
88 className,
89 )}
90 data-slot="dialog-popup"
91 {...props}
92 >
93 {children}
94 {showCloseButton && (
95 <DialogPrimitive.Close
96 aria-label="Close"
97 className="absolute end-2 top-2"
98 render={<Button size="icon" variant="ghost" />}
99 >
100 <XIcon />
101 </DialogPrimitive.Close>
102 )}
103 </DialogPrimitive.Popup>
104 </DialogViewport>
105 </DialogPortal>
106 );
107}
108
109function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
110 return (
111 <div
112 className={cn(
113 "flex flex-col gap-2 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pb-3 max-sm:pb-4",
114 className,
115 )}
116 data-slot="dialog-header"
117 {...props}
118 />
119 );
120}
121
122function DialogFooter({
123 className,
124 variant = "default",
125 ...props
126}: React.ComponentProps<"div"> & {
127 variant?: "default" | "bare";
128}) {
129 return (
130 <div
131 className={cn(
132 "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
133 variant === "default" && "border-t bg-muted/72 py-4",
134 variant === "bare" && "py-4",
135 className,
136 )}
137 data-slot="dialog-footer"
138 {...props}
139 />
140 );
141}
142
143function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
144 return (
145 <DialogPrimitive.Title
146 className={cn("wrap-anywhere font-semibold text-xl leading-none", className)}
147 data-slot="dialog-title"
148 {...props}
149 />
150 );
151}
152
153function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
154 return (
155 <DialogPrimitive.Description
156 className={cn("text-muted-foreground text-sm", className)}
157 data-slot="dialog-description"
158 {...props}
159 />
160 );
161}
162
163function DialogPanel({
164 className,
165 scrollFade = true,
166 ...props
167}: React.ComponentProps<"div"> & { scrollFade?: boolean }) {
168 return (
169 <ScrollArea scrollFade={scrollFade}>
170 <div
171 className={cn(
172 "space-y-4 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-header])]:pt-1 in-[[data-slot=dialog-popup]:has([data-slot=dialog-footer]:not(.border-t))]:pb-1",
173 className,
174 )}
175 data-slot="dialog-panel"
176 {...props}
177 />
178 </ScrollArea>
179 );
180}
181
182export {
183 Dialog,
184 DialogTrigger,
185 DialogPortal,
186 DialogClose,
187 DialogBackdrop,
188 DialogBackdrop as DialogOverlay,
189 DialogPopup,
190 DialogPopup as DialogContent,
191 DialogHeader,
192 DialogFooter,
193 DialogTitle,
194 DialogDescription,
195 DialogPanel,
196 DialogViewport,
197};