Skip to content
76 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";
7import { DialogOverlay } from "./dialog";
8
9// shadcn/ui's sheet, styled with g1t's tokens: a dialog that slides in
10// from the edge of the screen, for a panel to work in beside the page.
11
12export const Sheet = Primitive.Root;
13export const SheetTrigger = Primitive.Trigger;
14export const SheetClose = Primitive.Close;
15
16type Side = "right" | "left";
17
18const SIDES: Record<Side, string> = {
19 // Full width on a phone, a column beside the page from there up.
20 right:
21 "inset-y-0 right-0 border-l data-[state=open]:animate-sheet-in-right data-[state=closed]:animate-sheet-out-right sm:max-w-md",
22 left: "inset-y-0 left-0 border-r data-[state=open]:animate-sheet-in-left data-[state=closed]:animate-sheet-out-left sm:max-w-md",
23};
24
25export function SheetContent({
26 side = "right",
27 className,
28 children,
29 showClose = true,
30 ...props
31}: ComponentProps<typeof Primitive.Content> & { side?: Side; showClose?: boolean }) {
32 return (
33 <Primitive.Portal>
34 <DialogOverlay />
35 <Primitive.Content
36 className={cn(
37 // Clear of the notch and the home indicator on a phone.
38 "fixed z-50 flex h-dvh w-full flex-col border-line-strong bg-surface pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] text-fg shadow-2xl shadow-black/60 outline-none motion-reduce:animate-none",
39 SIDES[side],
40 className,
41 )}
42 {...props}
43 >
44 {children}
45 {showClose && (
46 <Primitive.Close asChild>
47 <Button
48 variant="ghost"
49 size="inline"
50 aria-label="Close"
51 className="absolute top-[calc(1rem+env(safe-area-inset-top))] right-4 p-1 text-faint max-sm:top-[calc(0.75rem+env(safe-area-inset-top))] max-sm:right-3 max-sm:size-10"
52 >
53 <X size={16} />
54 </Button>
55 </Primitive.Close>
56 )}
57 </Primitive.Content>
58 </Primitive.Portal>
59 );
60}
61
62export function SheetHeader({ className, ...props }: ComponentProps<"div">) {
63 return <div className={cn("flex flex-col gap-1.5 border-b border-line px-5 py-4 pr-12", className)} {...props} />;
64}
65
66export function SheetFooter({ className, ...props }: ComponentProps<"div">) {
67 return <div className={cn("mt-auto flex items-center gap-2 border-t border-line px-5 py-3", className)} {...props} />;
68}
69
70export function SheetTitle({ className, ...props }: ComponentProps<typeof Primitive.Title>) {
71 return <Primitive.Title className={cn("text-base font-semibold tracking-tight", className)} {...props} />;
72}
73
74export function SheetDescription({ className, ...props }: ComponentProps<typeof Primitive.Description>) {
75 return <Primitive.Description className={cn("text-sm text-muted", className)} {...props} />;
76}