Skip to content
74 linesCodeBlameRaw
1import { X } from "lucide-react";
2import { Dialog as Primitive } from "radix-ui";
3import type { ComponentProps } from "react";
4
5import { cn } from "../../lib/cn";
6import { Button } from "./button";
7
8// shadcn/ui's dialog, styled with g1t's tokens.
9
10export const Dialog = Primitive.Root;
11export const DialogTrigger = Primitive.Trigger;
12export const DialogClose = Primitive.Close;
13export const DialogPortal = Primitive.Portal;
14
15export function DialogOverlay({ className, ...props }: ComponentProps<typeof Primitive.Overlay>) {
16 return (
17 <Primitive.Overlay
18 className={cn(
19 "fixed inset-0 z-50 bg-black/60 backdrop-blur-[2px]",
20 "data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out motion-reduce:animate-none",
21 className,
22 )}
23 {...props}
24 />
25 );
26}
27
28/**
29 * The panel, centred. On a phone (below 640px) it is a sheet from the
30 * bottom instead, the full width, clear of the home indicator and sitting
31 * on the keyboard while it is up (`--keyboard-inset`, components/mobile.tsx).
32 */
33export const DIALOG_PANEL =
34 "fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto overscroll-contain rounded-xl border border-line-strong bg-surface p-6 text-fg shadow-2xl shadow-black/60 outline-none data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out motion-reduce:animate-none " +
35 "max-sm:top-auto max-sm:bottom-(--keyboard-inset,0px) max-sm:left-0 max-sm:w-full max-sm:max-w-none max-sm:translate-x-0 max-sm:translate-y-0 max-sm:max-h-[calc(var(--vv-height,100dvh)-env(safe-area-inset-top)-0.5rem)] max-sm:rounded-b-none max-sm:border-x-0 max-sm:border-b-0 max-sm:pb-[calc(1.5rem+env(safe-area-inset-bottom))] max-sm:data-[state=open]:animate-[sheet-in-bottom_0.24s_cubic-bezier(0.16,1,0.3,1)] max-sm:data-[state=closed]:animate-fade-out";
36
37export function DialogContent({
38 className,
39 children,
40 showClose = true,
41 ...props
42}: ComponentProps<typeof Primitive.Content> & { showClose?: boolean }) {
43 return (
44 <Primitive.Portal>
45 <DialogOverlay />
46 <Primitive.Content className={cn(DIALOG_PANEL, className)} {...props}>
47 {children}
48 {showClose && (
49 <Primitive.Close asChild>
50 <Button variant="ghost" size="inline" aria-label="Close" className="absolute top-4 right-4 p-1 text-faint max-sm:top-2.5 max-sm:right-2.5 max-sm:size-10">
51 <X size={16} />
52 </Button>
53 </Primitive.Close>
54 )}
55 </Primitive.Content>
56 </Primitive.Portal>
57 );
58}
59
60export function DialogHeader({ className, ...props }: ComponentProps<"div">) {
61 return <div className={cn("flex flex-col gap-1.5 pr-6", className)} {...props} />;
62}
63
64export function DialogFooter({ className, ...props }: ComponentProps<"div">) {
65 return <div className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} />;
66}
67
68export function DialogTitle({ className, ...props }: ComponentProps<typeof Primitive.Title>) {
69 return <Primitive.Title className={cn("text-base font-semibold tracking-tight", className)} {...props} />;
70}
71
72export function DialogDescription({ className, ...props }: ComponentProps<typeof Primitive.Description>) {
73 return <Primitive.Description className={cn("text-sm text-muted", className)} {...props} />;
74}