Skip to content

Commit 5dc4b29

Browse files
committed
feat: add component dialog
1 parent f847111 commit 5dc4b29

File tree

19 files changed

+600
-0
lines changed

19 files changed

+600
-0
lines changed

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@radix-ui/react-collapsible": "^1.1.11",
3131
"@radix-ui/react-compose-refs": "1.1.2",
3232
"@radix-ui/react-context-menu": "^2.2.15",
33+
"@radix-ui/react-dialog": "^1.1.14",
3334
"@radix-ui/react-dropdown-menu": "^2.1.15",
3435
"@radix-ui/react-label": "2.1.7",
3536
"@radix-ui/react-menu": "^2.1.15",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { Content } from '@radix-ui/react-dialog';
2+
import { Portal, Root, Trigger } from '@radix-ui/react-dialog';
3+
import type { ComponentRef } from 'react';
4+
import { forwardRef } from 'react';
5+
6+
import DialogClose from './DialogClose';
7+
import DialogContent from './DialogContent';
8+
import DialogDescription from './DialogDescription';
9+
import DialogFooter from './DialogFooter';
10+
import DialogHeader from './DialogHeader';
11+
import DialogOverlay from './DialogOverlay';
12+
import DialogTitle from './DialogTitle';
13+
import type { DialogProps } from './types';
14+
15+
const Dialog = forwardRef<ComponentRef<typeof Content>, DialogProps>((props, ref) => {
16+
const {
17+
children,
18+
className,
19+
classNames,
20+
defaultOpen,
21+
description,
22+
footer,
23+
onOpenChange,
24+
open,
25+
size,
26+
title,
27+
trigger,
28+
...rest
29+
} = props;
30+
31+
return (
32+
<Root
33+
data-slot="dialog-root"
34+
defaultOpen={defaultOpen}
35+
open={open}
36+
onOpenChange={onOpenChange}
37+
>
38+
<Trigger
39+
asChild
40+
data-slot="dialog-trigger"
41+
>
42+
{trigger}
43+
</Trigger>
44+
45+
<Portal data-slot="dialog-portal">
46+
<DialogOverlay className={classNames?.overlay} />
47+
48+
<DialogContent
49+
{...rest}
50+
className={className || classNames?.content}
51+
ref={ref}
52+
size={size}
53+
>
54+
<DialogHeader
55+
className={classNames?.header}
56+
size={size}
57+
>
58+
<DialogTitle
59+
className={classNames?.title}
60+
size={size}
61+
>
62+
{title}
63+
</DialogTitle>
64+
65+
{description && (
66+
<DialogDescription
67+
className={classNames?.description}
68+
size={size}
69+
>
70+
{description}
71+
</DialogDescription>
72+
)}
73+
</DialogHeader>
74+
75+
<DialogClose
76+
className={classNames?.close}
77+
size={size}
78+
/>
79+
80+
{children}
81+
82+
{footer && (
83+
<DialogFooter
84+
className={classNames?.footer}
85+
size={size}
86+
>
87+
{footer}
88+
</DialogFooter>
89+
)}
90+
</DialogContent>
91+
</Portal>
92+
</Root>
93+
);
94+
});
95+
96+
Dialog.displayName = 'Dialog';
97+
98+
export default Dialog;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Close as _Close } from '@radix-ui/react-dialog';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib/utils';
6+
7+
import { ButtonIcon } from '../button';
8+
9+
import { dialogVariants } from './dialog-variants';
10+
import type { DialogCloseProps } from './types';
11+
12+
const DialogClose = forwardRef<ComponentRef<typeof _Close>, DialogCloseProps>((props, ref) => {
13+
const { children, className, component: Close = _Close, size, ...rest } = props;
14+
15+
const { close } = dialogVariants({ size });
16+
17+
const mergedClass = cn(close(), className);
18+
return (
19+
<Close
20+
{...rest}
21+
asChild
22+
className={children ? (className as string) : mergedClass}
23+
data-slot="dialog-close"
24+
ref={ref}
25+
>
26+
{children || (
27+
<ButtonIcon
28+
icon="lucide:x"
29+
size={size}
30+
/>
31+
)}
32+
</Close>
33+
);
34+
});
35+
36+
DialogClose.displayName = 'DialogClose';
37+
38+
export default DialogClose;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Content as _Content } from '@radix-ui/react-dialog';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib/utils';
6+
7+
import { dialogVariants } from './dialog-variants';
8+
import type { DialogContentProps } from './types';
9+
10+
const DialogContent = forwardRef<ComponentRef<typeof _Content>, DialogContentProps>((props, ref) => {
11+
const { className, component: Content = _Content, size, ...rest } = props;
12+
13+
const { content } = dialogVariants({ size });
14+
15+
const mergedClass = cn(content(), className);
16+
return (
17+
<Content
18+
{...rest}
19+
className={mergedClass}
20+
data-slot="dialog-content"
21+
ref={ref}
22+
/>
23+
);
24+
});
25+
26+
DialogContent.displayName = 'DialogContent';
27+
28+
export default DialogContent;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Description as _Description } from '@radix-ui/react-dialog';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib/utils';
6+
7+
import { dialogVariants } from './dialog-variants';
8+
import type { DialogDescriptionProps } from './types';
9+
10+
const DialogDescription = forwardRef<ComponentRef<typeof _Description>, DialogDescriptionProps>((props, ref) => {
11+
const { className, component: Description = _Description, size, ...rest } = props;
12+
13+
const { description } = dialogVariants({ size });
14+
15+
const mergedClass = cn(description(), className);
16+
return (
17+
<Description
18+
{...rest}
19+
className={mergedClass}
20+
data-slot="dialog-description"
21+
ref={ref}
22+
/>
23+
);
24+
});
25+
26+
DialogDescription.displayName = 'DialogDescription';
27+
28+
export default DialogDescription;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { forwardRef } from 'react';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
import { dialogVariants } from './dialog-variants';
6+
import type { DialogFooterProps } from './types';
7+
8+
const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>((props, ref) => {
9+
const { className, size, ...rest } = props;
10+
11+
const { footer } = dialogVariants({ size });
12+
13+
const mergedClass = cn(footer(), className);
14+
return (
15+
<footer
16+
{...rest}
17+
className={mergedClass}
18+
data-slot="dialog-footer"
19+
ref={ref}
20+
/>
21+
);
22+
});
23+
24+
DialogFooter.displayName = 'DialogFooter';
25+
26+
export default DialogFooter;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { forwardRef } from 'react';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
import { dialogVariants } from './dialog-variants';
6+
import type { DialogHeaderProps } from './types';
7+
8+
const DialogHeader = forwardRef<HTMLDivElement, DialogHeaderProps>((props, ref) => {
9+
const { className, size, ...rest } = props;
10+
11+
const { header } = dialogVariants({ size });
12+
13+
const mergedClass = cn(header(), className);
14+
return (
15+
<header
16+
{...rest}
17+
className={mergedClass}
18+
data-slot="dialog-header"
19+
ref={ref}
20+
/>
21+
);
22+
});
23+
24+
DialogHeader.displayName = 'DialogHeader';
25+
26+
export default DialogHeader;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Overlay as _Overlay } from '@radix-ui/react-dialog';
2+
import { type ComponentRef, forwardRef } from 'react';
3+
4+
import { cn } from '@/lib/utils';
5+
6+
import { dialogVariants } from './dialog-variants';
7+
import type { DialogOverlayProps } from './types';
8+
9+
const DialogOverlay = forwardRef<ComponentRef<typeof _Overlay>, DialogOverlayProps>((props, ref) => {
10+
const { className, component: Overlay = _Overlay, size, ...rest } = props;
11+
12+
const { overlay } = dialogVariants({ size });
13+
14+
const mergedClass = cn(overlay(), className);
15+
return (
16+
<Overlay
17+
{...rest}
18+
className={mergedClass}
19+
data-slot="dialog-overlay"
20+
ref={ref}
21+
/>
22+
);
23+
});
24+
25+
DialogOverlay.displayName = 'DialogOverlay';
26+
27+
export default DialogOverlay;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Title as _Title } from '@radix-ui/react-dialog';
2+
import { type ComponentRef, forwardRef } from 'react';
3+
4+
import { cn } from '@/lib/utils';
5+
6+
import { dialogVariants } from './dialog-variants';
7+
import type { DialogTitleProps } from './types';
8+
9+
const DialogTitle = forwardRef<ComponentRef<typeof _Title>, DialogTitleProps>((props, ref) => {
10+
const { className, component: Title = _Title, size, ...rest } = props;
11+
12+
const { title } = dialogVariants({ size });
13+
14+
const mergedClass = cn(title(), className);
15+
return (
16+
<Title
17+
{...rest}
18+
className={mergedClass}
19+
data-slot="dialog-title"
20+
ref={ref}
21+
/>
22+
);
23+
});
24+
25+
DialogTitle.displayName = 'DialogTitle';
26+
27+
export default DialogTitle;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { tv } from 'tailwind-variants';
2+
3+
export const dialogVariants = tv({
4+
defaultVariants: {
5+
size: 'md'
6+
},
7+
slots: {
8+
close: `absolute`,
9+
content: [
10+
`fixed left-[50%] top-[50%] z-50 flex flex-col w-full border bg-background shadow-lg translate-x-[-50%] translate-y-[-50%] md:w-full duration-200 sm:rounded-lg`,
11+
`data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]`,
12+
`data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]`
13+
],
14+
description: `text-muted-foreground`,
15+
footer: `flex flex-col-reverse sm:flex-row sm:justify-end`,
16+
header: `flex flex-col text-center sm:text-left`,
17+
overlay: [
18+
`fixed inset-0 z-50 bg-black/80`,
19+
`data-[state=open]:animate-in data-[state=open]:fade-in-0`,
20+
`data-[state=closed]:animate-out data-[state=closed]:fade-out-0`
21+
],
22+
title: `flex items-center font-semibold leading-none tracking-tight`
23+
},
24+
variants: {
25+
size: {
26+
'2xl': {
27+
close: 'right-6 top-6',
28+
content: `gap-y-6 max-w-3xl px-7 py-6 text-xl`,
29+
description: 'text-xl',
30+
footer: 'gap-6',
31+
header: 'gap-y-6',
32+
title: 'gap-x-3.5 text-2xl'
33+
},
34+
lg: {
35+
close: 'right-4 top-4',
36+
content: `gap-y-4 max-w-xl px-5 py-4 text-base`,
37+
description: 'text-base',
38+
footer: 'gap-4',
39+
header: 'gap-y-4',
40+
title: 'gap-x-2.5 text-lg'
41+
},
42+
md: {
43+
close: 'right-3 top-3',
44+
content: `gap-y-3 max-w-lg px-4 py-3 text-sm`,
45+
description: 'text-sm',
46+
footer: 'gap-3',
47+
header: 'gap-y-3',
48+
title: 'gap-x-2 text-base'
49+
},
50+
sm: {
51+
close: 'right-2 top-2',
52+
content: `gap-y-2 max-w-md px-3 py-2 text-xs`,
53+
description: 'text-xs',
54+
footer: 'gap-2',
55+
header: 'gap-y-2',
56+
title: 'gap-x-1.75 text-sm'
57+
},
58+
xl: {
59+
close: 'right-5 top-5',
60+
content: `gap-y-5 max-w-2xl px-6 py-5 text-lg`,
61+
description: 'text-lg',
62+
footer: 'gap-5',
63+
header: 'gap-y-5',
64+
title: 'gap-x-3 text-xl'
65+
},
66+
xs: {
67+
close: 'right-1.5 top-1.5',
68+
content: `gap-y-1.5 max-w-md px-2 py-1.5 text-2xs`,
69+
description: 'text-2xs',
70+
footer: 'gap-1.5',
71+
header: 'gap-y-1.5',
72+
title: 'gap-x-1.5 text-xs'
73+
}
74+
}
75+
}
76+
});
77+
78+
export type DialogSlots = keyof typeof dialogVariants.slots;

0 commit comments

Comments
 (0)