Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

89 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
37/**
38 * `split`: a wide panel with no padding of its own, for a dialog in two
39 * columns (a rail and a pane, as Getting started is) whose parts scroll on
40 * their own. Its height is fixed on a computer so moving between steps
41 * does not make it jump. On a phone it is the sheet, of one height for
42 * the same reason (up to 44rem, less while the keyboard is up), and its
43 * pane scrolls inside it.
44 */
45const SIZES = {
46 default: "",
47 split:
48 "max-w-4xl gap-0 p-0 sm:h-[min(42rem,calc(100dvh-4rem))] sm:grid-cols-[16rem_minmax(0,1fr)] sm:grid-rows-[minmax(0,1fr)] sm:overflow-hidden max-sm:h-[44rem] max-sm:grid-cols-[minmax(0,1fr)] max-sm:grid-rows-[minmax(0,1fr)] max-sm:overflow-hidden max-sm:pb-[env(safe-area-inset-bottom)]",
49} as const;
50
51export function DialogContent({
52 className,
53 children,
54 showClose = true,
55 size = "default",
56 ...props
57}: ComponentProps<typeof Primitive.Content> & { showClose?: boolean; size?: keyof typeof SIZES }) {
58 return (
59 <Primitive.Portal>
60 <DialogOverlay />
61 <Primitive.Content className={cn(DIALOG_PANEL, SIZES[size], className)} {...props}>
62 {children}
63 {showClose && (
64 <Primitive.Close asChild>
65 <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">
66 <X size={16} />
67 </Button>
68 </Primitive.Close>
69 )}
70 </Primitive.Content>
71 </Primitive.Portal>
72 );
73}
74
75export function DialogHeader({ className, ...props }: ComponentProps<"div">) {
76 return <div className={cn("flex flex-col gap-1.5 pr-6", className)} {...props} />;
77}
78
79export function DialogFooter({ className, ...props }: ComponentProps<"div">) {
80 return <div className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} />;
81}
82
83export function DialogTitle({ className, ...props }: ComponentProps<typeof Primitive.Title>) {
84 return <Primitive.Title className={cn("text-base font-semibold tracking-tight", className)} {...props} />;
85}
86
87export function DialogDescription({ className, ...props }: ComponentProps<typeof Primitive.Description>) {
88 return <Primitive.Description className={cn("text-sm text-muted", className)} {...props} />;
89}