├── .prettierrc ├── app ├── favicon.ico ├── page.tsx ├── layout.tsx └── playground │ └── page.tsx ├── public ├── og-image.png ├── vercel.svg ├── window.svg ├── file.svg ├── dialogs.svg ├── globe.svg ├── next.svg ├── logo.svg ├── registry │ └── dialog.json └── avatar.svg ├── registry ├── index.ts ├── registry-ui.ts ├── schema.ts └── ui │ └── dialog.tsx ├── next.config.ts ├── postcss.config.mjs ├── lib └── utils.ts ├── components ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── radio-group.tsx │ ├── button.tsx │ ├── tabs.tsx │ ├── table.tsx │ └── dialog.tsx ├── types │ ├── types.tsx │ ├── default-dialog.tsx │ └── nested-dialog.tsx ├── installation.tsx ├── others │ ├── others.tsx │ └── draggable-dialog.tsx ├── site-footer.tsx ├── positions │ ├── positions.tsx │ ├── top-dialog-position.tsx │ ├── left-dialog-position.tsx │ ├── right-dialog-position.tsx │ ├── bottom-dialog-position.tsx │ └── default-dialog-position.tsx ├── demonstration.tsx ├── usage │ └── usage.tsx ├── props-table.tsx ├── demos │ └── render-nested-dialog.tsx ├── dialog-props-table.tsx ├── hero.tsx ├── code-block.tsx └── manual.tsx ├── config └── site.ts ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md ├── tailwind.config.ts ├── scripts └── build-registry.ts └── styles └── globals.css /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorwelander/shadcn-dialog/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorwelander/shadcn-dialog/HEAD/public/og-image.png -------------------------------------------------------------------------------- /registry/index.ts: -------------------------------------------------------------------------------- 1 | import { ui } from "./registry-ui"; 2 | 3 | export const registryComponents = [...ui]; 4 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /registry/registry-ui.ts: -------------------------------------------------------------------------------- 1 | import { Registry } from "./schema"; 2 | 3 | export const ui: Registry = [ 4 | { 5 | name: "dialog", 6 | type: "registry:ui", 7 | dependencies: ["@radix-ui/react-dialog", "lucide-react"], 8 | files: ["ui/dialog.tsx"], 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "Nested Shadcn Dialog", 3 | url: "https://shadcn-dialog.vercel.app/", 4 | ogImage: "https://shadcn-dialog.vercel.app/og-image.png", 5 | description: "Render multiple shadcn dialogs within eachother", 6 | links: { 7 | twitter: "https://x.com/victorwelander_", 8 | github: "https://github.com/victorwelander", 9 | }, 10 | }; 11 | 12 | export type SiteConfig = typeof siteConfig; 13 | -------------------------------------------------------------------------------- /components/types/types.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NestedDialog from "./nested-dialog"; 3 | import DefaultDialog from "./default-dialog"; 4 | 5 | export default function Types() { 6 | return ( 7 |
8 |

Types

9 |
10 | 11 | 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /public/dialogs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/installation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CodeBlock from "./code-block"; 3 | 4 | export default function Installation() { 5 | return ( 6 |
7 |
8 |

Installation

9 |

10 | This will install the dialog and the necessary dependencies using the 11 | shadcn CLI remote URL 12 |

13 |
14 |
15 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/others/others.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DraggableDialog from "./draggable-dialog"; 3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; 4 | import CodeBlock from "../code-block"; 5 | 6 | export default function Others() { 7 | return ( 8 |
9 |

Others

10 |
11 | 12 |
13 | 14 | 15 | Draggable 16 | 17 | 18 | `} 22 | /> 23 | 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | import avatar from "@/public/avatar.svg"; 4 | import Link from "next/link"; 5 | 6 | export default function SiteFooter() { 7 | return ( 8 |
9 |
10 |

11 | victor welander 18 | 19 | Made by{" "} 20 | 26 | Victor Welander 27 | 28 | 29 |

30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Hero from "@/components/hero"; 4 | import Usage from "@/components/usage/usage"; 5 | import React from "react"; 6 | import Types from "@/components/types/types"; 7 | import Positions from "@/components/positions/positions"; 8 | import Others from "@/components/others/others"; 9 | import DialogCode from "@/components/manual"; 10 | import Installation from "@/components/installation"; 11 | import DialogPropsTable from "@/components/dialog-props-table"; 12 | import Link from "next/link"; 13 | 14 | export default function Index() { 15 | return ( 16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |

32 | If you have any questions or find any bugs feel free to reach out to me 33 | at{" "} 34 | 40 | @victorwelander_ 41 | 42 |

43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadcn-dialog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.1.2", 13 | "@radix-ui/react-label": "^2.1.0", 14 | "@radix-ui/react-popover": "^1.1.2", 15 | "@radix-ui/react-radio-group": "^1.2.1", 16 | "@radix-ui/react-slot": "^1.1.0", 17 | "@radix-ui/react-tabs": "^1.1.1", 18 | "@vercel/analytics": "^1.3.2", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.1.1", 21 | "copy-to-clipboard": "^3.3.3", 22 | "framer-motion": "^11.11.10", 23 | "geist": "^1.3.1", 24 | "lucide-react": "^0.453.0", 25 | "next": "15.0.1", 26 | "prism-react-renderer": "^1.3.5", 27 | "react": "^18", 28 | "react-dom": "^18", 29 | "react-icons": "^5.3.0", 30 | "react-live": "^4.1.7", 31 | "react-payment-inputs": "^1.2.0", 32 | "react-use-measure": "^2.1.1", 33 | "shiki": "^1.22.2", 34 | "tailwind-merge": "^2.5.4", 35 | "tailwindcss-animate": "^1.0.7", 36 | "zod": "^3.23.8" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^20", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "@types/react-payment-inputs": "^1.1.4", 43 | "postcss": "^8", 44 | "prettier": "^3.3.3", 45 | "prettier-plugin-tailwindcss": "^0.6.8", 46 | "tailwindcss": "^3.4.1", 47 | "typescript": "^5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" 5 | import { Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const RadioGroup = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => { 13 | return ( 14 | 19 | ) 20 | }) 21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName 22 | 23 | const RadioGroupItem = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => { 27 | return ( 28 | 36 | 37 | 38 | 39 | 40 | ) 41 | }) 42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName 43 | 44 | export { RadioGroup, RadioGroupItem } 45 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap cursor-pointer relative font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-70", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-8 px-3 text-sm rounded-md", 24 | small: "h-7 px-2 text-xs rounded-[6px]", 25 | large: "h-10 px-4 text-sm rounded-lg", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | }, 33 | ); 34 | 35 | export interface ButtonProps 36 | extends React.ButtonHTMLAttributes, 37 | VariantProps { 38 | asChild?: boolean; 39 | } 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, asChild = false, ...props }, ref) => { 43 | const Comp = asChild ? Slot : "button"; 44 | return ( 45 | 50 | ); 51 | }, 52 | ); 53 | Button.displayName = "Button"; 54 | 55 | export { Button, buttonVariants }; 56 | -------------------------------------------------------------------------------- /components/types/default-dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | } from "@/components/ui/dialog"; 12 | import { Button } from "@/components/ui/button"; 13 | import { usePaymentInputs } from "react-payment-inputs"; 14 | import { Label } from "@/components/ui/label"; 15 | import { Input } from "@/components/ui/input"; 16 | import { CreditCard } from "lucide-react"; 17 | import { Skeleton } from "../ui/skeleton"; 18 | 19 | export default function DefaultDialog() { 20 | const { getCardNumberProps } = usePaymentInputs(); 21 | const { getExpiryDateProps } = usePaymentInputs(); 22 | const { getCVCProps } = usePaymentInputs(); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | Default Dialog 32 | This is the default dialog 33 | 34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Analytics } from "@vercel/analytics/react"; 3 | import { GeistSans } from "geist/font/sans"; 4 | import { GeistMono } from "geist/font/mono"; 5 | import "@/styles/globals.css"; 6 | import SiteFooter from "@/components/site-footer"; 7 | import { siteConfig } from "@/config/site"; 8 | 9 | export const metadata: Metadata = { 10 | title: { 11 | default: siteConfig.name, 12 | template: `%s - ${siteConfig.name}`, 13 | }, 14 | metadataBase: new URL(siteConfig.url), 15 | description: siteConfig.description, 16 | keywords: ["Shadcn Dialog", "Dialog", "Shadcn", "Dialog Component"], 17 | authors: [ 18 | { 19 | name: "Nested Shadcn Dialog", 20 | url: "https://shadcn-dialog.vercel.app/", 21 | }, 22 | ], 23 | creator: "victorwelander", 24 | openGraph: { 25 | type: "website", 26 | locale: "en_US", 27 | url: siteConfig.url, 28 | title: siteConfig.name, 29 | description: siteConfig.description, 30 | siteName: siteConfig.name, 31 | images: [ 32 | { 33 | url: siteConfig.ogImage, 34 | width: 1200, 35 | height: 630, 36 | alt: siteConfig.name, 37 | }, 38 | ], 39 | }, 40 | twitter: { 41 | card: "summary_large_image", 42 | title: siteConfig.name, 43 | description: siteConfig.description, 44 | images: [siteConfig.ogImage], 45 | creator: "@victorwelander_", 46 | }, 47 | icons: { 48 | icon: "/favicon.ico", 49 | }, 50 | alternates: { 51 | canonical: siteConfig.url, 52 | }, 53 | robots: { 54 | index: true, 55 | follow: true, 56 | googleBot: { 57 | index: true, 58 | follow: true, 59 | "max-video-preview": -1, 60 | "max-image-preview": "large", 61 | "max-snippet": -1, 62 | }, 63 | }, 64 | }; 65 | 66 | export default function RootLayout({ 67 | children, 68 | }: Readonly<{ 69 | children: React.ReactNode; 70 | }>) { 71 | return ( 72 | 73 | 74 | 75 | {children} 76 | 77 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /scripts/build-registry.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import { z } from "zod"; 3 | import { registryItemFileSchema } from "@/registry/schema"; 4 | import path from "path"; 5 | import { registryComponents } from "@/registry"; 6 | 7 | const REGISTRY_BASE_PATH = "registry"; 8 | const PUBLIC_FOLDER_BASE_PATH = "public/registry"; 9 | const COMPONENT_FOLDER_PATH = "components"; 10 | 11 | type File = z.infer; 12 | const FolderToComponentTypeMap = { 13 | component: "registry:component", 14 | ui: "registry:ui", 15 | }; 16 | 17 | async function writeFileRecursive(filePath: string, data: string) { 18 | const dir = path.dirname(filePath); 19 | 20 | try { 21 | await fs.mkdir(dir, { recursive: true }); 22 | 23 | await fs.writeFile(filePath, data, "utf-8"); 24 | console.log(`File written to ${filePath}`); 25 | } catch (error) { 26 | console.error(`Error writing file`); 27 | console.error(error); 28 | } 29 | } 30 | 31 | const getComponentFiles = async (files: File[]) => { 32 | const filesArrayPromises = (files ?? []).map(async (file) => { 33 | if (typeof file === "string") { 34 | const filePath = `${REGISTRY_BASE_PATH}/${file}`; 35 | const fileContent = await fs.readFile(filePath, "utf-8"); 36 | return { 37 | type: FolderToComponentTypeMap[ 38 | file.split("/")[0] as keyof typeof FolderToComponentTypeMap 39 | ], 40 | content: fileContent, 41 | path: file, 42 | target: `${COMPONENT_FOLDER_PATH}/${file}`, 43 | }; 44 | } 45 | }); 46 | const filesArray = await Promise.all(filesArrayPromises); 47 | 48 | return filesArray; 49 | }; 50 | 51 | const main = async () => { 52 | for (let i = 0; i < registryComponents.length; i++) { 53 | const component = registryComponents[i]; 54 | const files = component.files; 55 | if (!files) throw new Error("No files found for component"); 56 | 57 | const filesArray = await getComponentFiles(files); 58 | 59 | const json = JSON.stringify( 60 | { 61 | ...component, 62 | files: filesArray, 63 | }, 64 | null, 65 | 2, 66 | ); 67 | const jsonPath = `${PUBLIC_FOLDER_BASE_PATH}/${component.name}.json`; 68 | await writeFileRecursive(jsonPath, json); 69 | console.log(json); 70 | } 71 | }; 72 | 73 | main() 74 | .then(() => { 75 | console.log("done"); 76 | }) 77 | .catch((err) => { 78 | console.error(err); 79 | }); 80 | -------------------------------------------------------------------------------- /registry/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const blockChunkSchema = z.object({ 4 | name: z.string(), 5 | description: z.string(), 6 | component: z.any(), 7 | file: z.string(), 8 | code: z.string().optional(), 9 | container: z 10 | .object({ 11 | className: z.string().nullish(), 12 | }) 13 | .optional(), 14 | }); 15 | 16 | export const registryItemTypeSchema = z.enum([ 17 | "registry:component", 18 | "registry:ui", 19 | ]); 20 | 21 | export const registryItemFileSchema = z.union([ 22 | z.string(), 23 | z.object({ 24 | path: z.string(), 25 | content: z.string().optional(), 26 | type: registryItemTypeSchema, 27 | target: z.string().optional(), 28 | }), 29 | ]); 30 | 31 | export const registryItemTailwindSchema = z.object({ 32 | config: z.object({ 33 | content: z.array(z.string()).optional(), 34 | theme: z.record(z.string(), z.any()).optional(), 35 | plugins: z.array(z.string()).optional(), 36 | }), 37 | }); 38 | 39 | export const registryItemCssVarsSchema = z.object({ 40 | light: z.record(z.string(), z.string()).optional(), 41 | dark: z.record(z.string(), z.string()).optional(), 42 | }); 43 | 44 | export const registryEntrySchema = z.object({ 45 | name: z.string(), 46 | type: registryItemTypeSchema, 47 | description: z.string().optional(), 48 | dependencies: z.array(z.string()).optional(), 49 | devDependencies: z.array(z.string()).optional(), 50 | registryDependencies: z.array(z.string()).optional(), 51 | files: z.array(registryItemFileSchema).optional(), 52 | tailwind: registryItemTailwindSchema.optional(), 53 | cssVars: registryItemCssVarsSchema.optional(), 54 | source: z.string().optional(), 55 | category: z.string().optional(), 56 | subcategory: z.string().optional(), 57 | chunks: z.array(blockChunkSchema).optional(), 58 | docs: z.string().optional(), 59 | }); 60 | 61 | export const registrySchema = z.array(registryEntrySchema); 62 | 63 | export type RegistryEntry = z.infer; 64 | 65 | export type Registry = z.infer; 66 | 67 | export const blockSchema = registryEntrySchema.extend({ 68 | type: z.literal("registry:block"), 69 | style: z.enum(["default", "new-york"]), 70 | component: z.any(), 71 | container: z 72 | .object({ 73 | height: z.string().nullish(), 74 | className: z.string().nullish(), 75 | }) 76 | .optional(), 77 | code: z.string(), 78 | highlightedCode: z.string(), 79 | }); 80 | 81 | export type Block = z.infer; 82 | 83 | export type BlockChunk = z.infer; 84 | -------------------------------------------------------------------------------- /components/positions/positions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import DefaultDialogPosition from "./default-dialog-position"; 4 | import BottomDialogPosition from "./bottom-dialog-position"; 5 | import TopDialogPosition from "./top-dialog-position"; 6 | import LeftDialogPosition from "./left-dialog-position"; 7 | import RightDialogPosition from "./right-dialog-position"; 8 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; 9 | import CodeBlock from "../code-block"; 10 | 11 | export default function Positions() { 12 | return ( 13 |
14 |
15 |

Positions

16 |

17 | How the nested dialog appears 18 |

19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | Default 30 | Bottom 31 | Top 32 | Left 33 | Right 34 | 35 | 36 | `} /> 37 | 38 | 39 | `} 43 | /> 44 | 45 | 46 | `} 50 | /> 51 | 52 | 53 | `} 57 | /> 58 | 59 | 60 | `} 64 | /> 65 | 66 | 67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /app/playground/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useRef } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { Skeleton } from "@/components/ui/skeleton"; 6 | 7 | export default function Demonstration() { 8 | const [isInnerActive, setIsInnerActive] = useState(false); 9 | const [cycleCount, setCycleCount] = useState(0); 10 | const outerRef = useRef(null); 11 | const innerRef = useRef(null); 12 | 13 | useEffect(() => { 14 | const interval = setInterval(() => { 15 | setCycleCount((prev) => prev + 1); 16 | }, 1000); 17 | 18 | return () => clearInterval(interval); 19 | }, []); 20 | 21 | useEffect(() => { 22 | setIsInnerActive(cycleCount % 4 < 2); 23 | }, [cycleCount]); 24 | 25 | return ( 26 |
27 |
28 | 44 |
45 |
46 |
47 | 48 |
49 | 50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | {isInnerActive && ( 59 | 70 | )} 71 | 72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 28 | )); 29 | TableHeader.displayName = "TableHeader"; 30 | 31 | const TableBody = React.forwardRef< 32 | HTMLTableSectionElement, 33 | React.HTMLAttributes 34 | >(({ className, ...props }, ref) => ( 35 | 40 | )); 41 | TableBody.displayName = "TableBody"; 42 | 43 | const TableFooter = React.forwardRef< 44 | HTMLTableSectionElement, 45 | React.HTMLAttributes 46 | >(({ className, ...props }, ref) => ( 47 | tr]:last:border-b-0", 51 | className, 52 | )} 53 | {...props} 54 | /> 55 | )); 56 | TableFooter.displayName = "TableFooter"; 57 | 58 | const TableRow = React.forwardRef< 59 | HTMLTableRowElement, 60 | React.HTMLAttributes 61 | >(({ className, ...props }, ref) => ( 62 | 70 | )); 71 | TableRow.displayName = "TableRow"; 72 | 73 | const TableHead = React.forwardRef< 74 | HTMLTableCellElement, 75 | React.ThHTMLAttributes 76 | >(({ className, ...props }, ref) => ( 77 |
85 | )); 86 | TableHead.displayName = "TableHead"; 87 | 88 | const TableCell = React.forwardRef< 89 | HTMLTableCellElement, 90 | React.TdHTMLAttributes 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )); 98 | TableCell.displayName = "TableCell"; 99 | 100 | const TableCaption = React.forwardRef< 101 | HTMLTableCaptionElement, 102 | React.HTMLAttributes 103 | >(({ className, ...props }, ref) => ( 104 |
109 | )); 110 | TableCaption.displayName = "TableCaption"; 111 | 112 | export { 113 | Table, 114 | TableHeader, 115 | TableBody, 116 | TableFooter, 117 | TableHead, 118 | TableRow, 119 | TableCell, 120 | TableCaption, 121 | }; 122 | -------------------------------------------------------------------------------- /components/demonstration.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useRef } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { Skeleton } from "@/components/ui/skeleton"; 6 | 7 | export default function Demonstration() { 8 | const [isInnerActive, setIsInnerActive] = useState(false); 9 | const [cycleCount, setCycleCount] = useState(0); 10 | const outerRef = useRef(null); 11 | const innerRef = useRef(null); 12 | 13 | useEffect(() => { 14 | const interval = setInterval(() => { 15 | setCycleCount((prev) => prev + 1); 16 | }, 1000); 17 | 18 | return () => clearInterval(interval); 19 | }, []); 20 | 21 | useEffect(() => { 22 | setIsInnerActive(cycleCount % 4 < 2); 23 | }, [cycleCount]); 24 | 25 | return ( 26 |
27 |
28 | 44 |
45 |
46 |
47 | 48 |
49 | 50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | {isInnerActive && ( 59 | 73 | {" "} 74 |
75 |
76 |
77 | 78 |
79 | 80 | 81 |
82 |
83 | 84 |
85 |
86 |
87 | )} 88 |
89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 0 0% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 3.9%; 13 | --primary: 0 0% 9%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 0 0% 96.1%; 16 | --secondary-foreground: 0 0% 9%; 17 | --muted: 0 0% 96.1%; 18 | --muted-foreground: 0 0% 45.1%; 19 | --accent: 0 0% 96.1%; 20 | --accent-foreground: 0 0% 9%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 0 0% 89.8%; 24 | --input: 0 0% 89.8%; 25 | --ring: 0 0% 3.9%; 26 | --chart-1: 12 76% 61%; 27 | --chart-2: 173 58% 39%; 28 | --chart-3: 197 37% 24%; 29 | --chart-4: 43 74% 66%; 30 | --chart-5: 27 87% 67%; 31 | --radius: 0.5rem; 32 | } 33 | .dark { 34 | --background: 0 0% 3.9%; 35 | --foreground: 0 0% 98%; 36 | --card: 0 0% 3.9%; 37 | --card-foreground: 0 0% 98%; 38 | --popover: 0 0% 3.9%; 39 | --popover-foreground: 0 0% 98%; 40 | --primary: 0 0% 98%; 41 | --primary-foreground: 0 0% 9%; 42 | --secondary: 0 0% 14.9%; 43 | --secondary-foreground: 0 0% 98%; 44 | --muted: 0 0% 14.9%; 45 | --muted-foreground: 0 0% 63.9%; 46 | --accent: 0 0% 14.9%; 47 | --accent-foreground: 0 0% 98%; 48 | --destructive: 0 62.8% 30.6%; 49 | --destructive-foreground: 0 0% 98%; 50 | --border: 0 0% 14.9%; 51 | --input: 0 0% 14.9%; 52 | --ring: 0 0% 83.1%; 53 | --chart-1: 220 70% 50%; 54 | --chart-2: 160 60% 45%; 55 | --chart-3: 30 80% 55%; 56 | --chart-4: 280 65% 60%; 57 | --chart-5: 340 75% 55%; 58 | } 59 | } 60 | 61 | body, 62 | html { 63 | text-rendering: optimizeLegibility; 64 | background-color: var(--ds-background-200); 65 | color: var(--ds-gray-1000); 66 | -webkit-text-size-adjust: 100%; 67 | -moz-text-size-adjust: 100%; 68 | text-size-adjust: 100%; 69 | -webkit-font-smoothing: antialiased; 70 | -moz-osx-font-smoothing: grayscale; 71 | } 72 | 73 | :root, 74 | .dark-theme, 75 | .invert-theme { 76 | --geist-foreground: #000000; 77 | --geist-background: #ffffff; 78 | 79 | --accents-1: #fafafa; 80 | --accents-2: #eaeaea; 81 | --accents-3: #999999; 82 | --accents-4: #888888; 83 | --accents-5: #666666; 84 | --accents-6: #444444; 85 | --accents-7: #333333; 86 | --accents-8: #111111; 87 | } 88 | 89 | .dark-theme, 90 | .invert-theme { 91 | --geist-foreground: #ffffff; 92 | --geist-background: #000000; 93 | 94 | --accents-8: #fafafa; 95 | --accents-7: #eaeaea; 96 | --accents-6: #999; 97 | --accents-5: #888; 98 | --accents-4: #666; 99 | --accents-3: #444; 100 | --accents-2: #333; 101 | --accents-1: #111; 102 | } 103 | 104 | :root { 105 | --ds-gray-100: #fcfcfc; 106 | --ds-gray-300: #e6e6e6; 107 | --ds-gray-500: #c9c9c9; 108 | --ds-gray-900: #666; 109 | --ds-gray-1000: #171717; 110 | 111 | --ds-gray-alpha-300: #0000001a; 112 | --ds-gray-alpha-400: #00000014; 113 | 114 | --ds-blue-300: #dfefff; 115 | --ds-blue-900: #0059ec; 116 | 117 | --ds-background-100: #ffffff; 118 | } 119 | 120 | .dark-theme { 121 | --ds-gray-100: #1a1a1a; 122 | --ds-gray-300: #292929; 123 | --ds-gray-500: #454545; 124 | --ds-gray-900: #a1a1a1; 125 | --ds-gray-1000: #ededed; 126 | 127 | --ds-gray-alpha-300: #ffffff21; 128 | --ds-gray-alpha-400: #ffffff24; 129 | 130 | --ds-blue-300: #002f62; 131 | --ds-blue-900: #47a8ff; 132 | 133 | --ds-background-100: #0a0a0a; 134 | } 135 | -------------------------------------------------------------------------------- /components/types/nested-dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | InnerDialog, 12 | InnerDialogTrigger, 13 | InnerDialogContent, 14 | InnerDialogClose, 15 | InnerDialogHeader, 16 | InnerDialogFooter, 17 | InnerDialogTitle, 18 | InnerDialogDescription, 19 | } from "@/components/ui/dialog"; 20 | import { Button } from "@/components/ui/button"; 21 | import { Skeleton } from "../ui/skeleton"; 22 | 23 | export default function NestedDialog() { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | Nested Dialog 32 | This is the nested dialog 33 | 34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | Choose a payment method 58 | 59 | Nested Dialog Description 60 | 61 | 62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 |
70 |
71 |
72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 |
82 |
83 |
84 | 85 | 88 | 89 | 90 |
91 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /components/usage/usage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 4 | import CodeBlock from "../code-block"; 5 | 6 | export default function Usage() { 7 | return ( 8 |
9 |

Usage

10 | {/* 11 | 12 | 13 | Default 14 | Nested 15 | 16 | 17 | {` 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | `} 31 | 32 | 33 | {` 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | `} 59 | 60 | 61 | */} 62 | 63 | 64 | Default 65 | Nested 66 | 67 | 68 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | `} 83 | /> 84 | 85 | 86 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | `} 115 | /> 116 | 117 | 118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /components/positions/top-dialog-position.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | InnerDialog, 12 | InnerDialogTrigger, 13 | InnerDialogContent, 14 | InnerDialogClose, 15 | InnerDialogHeader, 16 | InnerDialogFooter, 17 | InnerDialogTitle, 18 | InnerDialogDescription, 19 | } from "@/components/ui/dialog"; 20 | import { Button } from "@/components/ui/button"; 21 | import { Skeleton } from "../ui/skeleton"; 22 | 23 | export default function TopDialogPosition() { 24 | return ( 25 | 26 | 27 | 46 | 47 | 48 | 49 | Top Position 50 | 51 | This dialog appears from the top 52 | 53 | 54 | 55 |
56 |
57 |
58 | 59 |
60 | 61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | Choose a payment method 78 | 79 | Nested Dialog Description 80 | 81 | 82 | 83 |
84 |
85 |
86 | 87 | 88 | 89 |
90 |
91 |
92 | 93 | 94 | 95 | 98 | 99 | 100 | 101 |
102 |
103 |
104 | 105 | 108 | 109 | 110 |
111 |
112 |
113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /components/positions/left-dialog-position.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | InnerDialog, 12 | InnerDialogTrigger, 13 | InnerDialogContent, 14 | InnerDialogClose, 15 | InnerDialogHeader, 16 | InnerDialogFooter, 17 | InnerDialogTitle, 18 | InnerDialogDescription, 19 | } from "@/components/ui/dialog"; 20 | import { Button } from "@/components/ui/button"; 21 | import { Skeleton } from "../ui/skeleton"; 22 | 23 | export default function LeftDialogPosition() { 24 | return ( 25 | 26 | 27 | 45 | 46 | 47 | 48 | Left Position 49 | 50 | This dialog appears from the left 51 | 52 | 53 | 54 |
55 |
56 |
57 | 58 |
59 | 60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | Choose a payment method 77 | 78 | Nested Dialog Description 79 | 80 | 81 | 82 |
83 |
84 |
85 | 86 | 87 | 88 |
89 |
90 |
91 | 92 | 93 | 94 | 97 | 98 | 99 | 100 |
101 |
102 |
103 | 104 | 107 | 108 | 109 |
110 |
111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /components/positions/right-dialog-position.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | InnerDialog, 12 | InnerDialogTrigger, 13 | InnerDialogContent, 14 | InnerDialogClose, 15 | InnerDialogHeader, 16 | InnerDialogFooter, 17 | InnerDialogTitle, 18 | InnerDialogDescription, 19 | } from "@/components/ui/dialog"; 20 | import { Button } from "@/components/ui/button"; 21 | import { Skeleton } from "../ui/skeleton"; 22 | 23 | export default function RightDialogPosition() { 24 | return ( 25 | 26 | 27 | 46 | 47 | 48 | 49 | Right Position 50 | 51 | This dialog appears from the right 52 | 53 | 54 | 55 |
56 |
57 |
58 | 59 |
60 | 61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | Choose a payment method 78 | 79 | Nested Dialog Description 80 | 81 | 82 | 83 |
84 |
85 |
86 | 87 | 88 | 89 |
90 |
91 |
92 | 93 | 94 | 95 | 98 | 99 | 100 | 101 |
102 |
103 |
104 | 105 | 108 | 109 | 110 |
111 |
112 |
113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /components/positions/bottom-dialog-position.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | InnerDialog, 12 | InnerDialogTrigger, 13 | InnerDialogContent, 14 | InnerDialogClose, 15 | InnerDialogHeader, 16 | InnerDialogFooter, 17 | InnerDialogTitle, 18 | InnerDialogDescription, 19 | } from "@/components/ui/dialog"; 20 | import { Button } from "@/components/ui/button"; 21 | import { Skeleton } from "../ui/skeleton"; 22 | 23 | export default function BottomDialogPosition() { 24 | return ( 25 | 26 | 27 | 45 | 46 | 47 | 48 | Bottom Position 49 | 50 | This dialog appears from the bottom 51 | 52 | 53 | 54 |
55 |
56 |
57 | 58 |
59 | 60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 | 68 | 69 | 70 | 73 | 74 | 78 | 79 | Choose a payment method 80 | 81 | Nested Dialog Description 82 | 83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | 91 |
92 |
93 |
94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 |
104 |
105 |
106 | 107 | 110 | 111 | 112 |
113 |
114 |
115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /components/props-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Info } from "lucide-react"; 4 | import React from "react"; 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from "./ui/table"; 13 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; 14 | 15 | export type PropDef = { 16 | name: string; 17 | required?: boolean; 18 | default?: string | boolean; 19 | type?: string; 20 | typeSimple: string; 21 | description?: string | React.ReactNode; 22 | }; 23 | 24 | const PropsTable = ({ 25 | data, 26 | propHeaderFixedWidth = true, 27 | }: { 28 | data: PropDef[]; 29 | propHeaderFixedWidth?: boolean; 30 | }) => { 31 | return ( 32 |
33 | 34 | 35 | 36 | 37 | Prop 38 | 39 | Type 40 | Default 41 | 42 | 43 | 44 | 45 | {data.map( 46 | ( 47 | { 48 | name, 49 | type, 50 | typeSimple, 51 | required, 52 | default: defaultValue, 53 | description, 54 | }, 55 | i, 56 | ) => { 57 | return ( 58 | 59 | 60 |
61 | 62 | {name} 63 | {required ? "*" : null} 64 | 65 | 66 | {description && ( 67 | 68 | 69 | 70 | 71 | 72 | 73 | { 79 | event.preventDefault(); 80 | (event.currentTarget as HTMLElement)?.focus(); 81 | }} 82 | > 83 |
{description}
84 |
85 |
86 | )} 87 |
88 |
89 | 90 |
91 | 92 | {Boolean(typeSimple) ? typeSimple : type} 93 | 94 | {Boolean(typeSimple) && Boolean(type) && ( 95 | 96 | 97 | 98 | 99 | 100 | 101 | 107 |
{type}
108 |
109 |
110 | )} 111 |
112 |
113 | 114 | 115 | {Boolean(defaultValue) ? ( 116 | 117 | {defaultValue} 118 | 119 | ) : ( 120 |

-

121 | )} 122 |
123 |
124 | ); 125 | }, 126 | )} 127 |
128 |
129 |
130 | ); 131 | }; 132 | 133 | export default PropsTable; 134 | -------------------------------------------------------------------------------- /components/positions/default-dialog-position.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | InnerDialog, 12 | InnerDialogTrigger, 13 | InnerDialogContent, 14 | InnerDialogClose, 15 | InnerDialogHeader, 16 | InnerDialogFooter, 17 | InnerDialogTitle, 18 | InnerDialogDescription, 19 | } from "@/components/ui/dialog"; 20 | import { Button } from "@/components/ui/button"; 21 | import { Skeleton } from "../ui/skeleton"; 22 | 23 | export default function DefaultDialogPosition() { 24 | return ( 25 | 26 | 27 | 43 | 44 | 45 | 46 | Default Position 47 | 48 | This dialog appears from the center 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 |
57 | 58 | 59 |
60 |
61 | 62 |
63 |
64 | 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | Choose a payment method 75 | 76 | Nested Dialog Description 77 | 78 | 79 | 80 |
81 |
82 |
83 | 84 | 85 | 86 |
87 |
88 |
89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 |
99 |
100 |
101 | 102 | 105 | 106 | 107 |
108 |
109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /components/others/draggable-dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogFooter, 8 | DialogTitle, 9 | DialogDescription, 10 | DialogClose, 11 | InnerDialog, 12 | InnerDialogTrigger, 13 | InnerDialogContent, 14 | InnerDialogClose, 15 | InnerDialogHeader, 16 | InnerDialogFooter, 17 | InnerDialogTitle, 18 | InnerDialogDescription, 19 | } from "@/components/ui/dialog"; 20 | import { Button } from "@/components/ui/button"; 21 | import { Skeleton } from "../ui/skeleton"; 22 | 23 | export default function DraggableDialog() { 24 | return ( 25 | 26 | 27 | 62 | 63 | 64 | 65 | Default Position 66 | 67 | This dialog appears from the center 68 | 69 | 70 | 71 |
72 |
73 |
74 | 75 |
76 | 77 | 78 |
79 |
80 | 81 |
82 |
83 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 93 | Choose a payment method 94 | 95 | Nested Dialog Description 96 | 97 | 98 | 99 |
100 |
101 |
102 | 103 | 104 | 105 |
106 |
107 |
108 | 109 | 110 | 111 | 114 | 115 | 116 | 117 |
118 |
119 |
120 | 121 | 124 | 125 | 126 |
127 |
128 |
129 |
130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /components/demos/render-nested-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | import { 5 | Dialog, 6 | DialogTrigger, 7 | DialogContent, 8 | DialogHeader, 9 | DialogFooter, 10 | DialogTitle, 11 | DialogDescription, 12 | DialogClose, 13 | InnerDialog, 14 | InnerDialogTrigger, 15 | InnerDialogContent, 16 | InnerDialogClose, 17 | InnerDialogHeader, 18 | InnerDialogFooter, 19 | InnerDialogTitle, 20 | InnerDialogDescription, 21 | } from "@/components/ui/dialog"; 22 | import { Button } from "@/components/ui/button"; 23 | import { usePaymentInputs } from "react-payment-inputs"; 24 | import { CreditCard } from "lucide-react"; 25 | import { Label } from "@/components/ui/label"; 26 | import { Input } from "@/components/ui/input"; 27 | import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; 28 | import { SiApplepay, SiPaypal, SiVisa } from "react-icons/si"; 29 | 30 | export default function RenderNestedDialog() { 31 | const { getCardNumberProps, getExpiryDateProps, getCVCProps } = 32 | usePaymentInputs(); 33 | const [selectedPaymentMethod, setSelectedPaymentMethod] = useState("paypal"); 34 | 35 | const handleClick = (value: string) => { 36 | setSelectedPaymentMethod(value); 37 | }; 38 | 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | Payment 47 | 48 | Please enter your credit card credentials below to complete the 49 | payment process. 50 | 51 | 52 | 53 |
54 |
55 | 58 |
59 | 60 |
61 |
62 |
63 | 66 |
67 | 71 |
72 |
74 |
75 |
76 |
77 |
78 | 81 | 85 |
86 |
87 | 90 | 91 |
92 |
93 |
94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | Choose a payment method 104 | 105 | Nested Dialog Description 106 | 107 | 108 | 109 |
110 | 114 |
handleClick("paypal")} 119 | > 120 |
121 | 122 |
123 |

PayPal

124 |

125 | Pay with your PayPal account 126 |

127 |
128 |
129 | 130 |
131 |
handleClick("creditcard")} 136 | > 137 |
138 | 139 |
140 |

Credit Card

141 |

142 | Pay with Visa, Mastercard, or American Express 143 |

144 |
145 |
146 | 147 |
148 |
handleClick("applepay")} 153 | > 154 |
155 | 156 |
157 |

Apple Pay

158 |

159 | Pay with Apple Pay 160 |

161 |
162 |
163 | 164 |
165 |
166 |
167 | 168 | 169 | 170 | 173 | 174 | 175 | 176 |
177 |
178 |
179 | 180 | 183 | 184 | 185 |
186 |
187 |
188 |
189 | ); 190 | } 191 | -------------------------------------------------------------------------------- /components/dialog-props-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import PropsTable, { PropDef } from "./props-table"; 5 | 6 | const dialogProps: PropDef[] = [ 7 | { 8 | name: "children", 9 | required: true, 10 | type: "React.ReactNode", 11 | typeSimple: "ReactNode", 12 | description: "The content of the dialog.", 13 | }, 14 | ]; 15 | 16 | const dialogTriggerProps: PropDef[] = [ 17 | { 18 | name: "asChild", 19 | type: "boolean", 20 | default: "false", 21 | description: 22 | "Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.", 23 | typeSimple: "boolean", 24 | }, 25 | ]; 26 | 27 | const dialogPortalProps: PropDef[] = [ 28 | { 29 | name: "container", 30 | type: "HTMLElement", 31 | typeSimple: "HTMLElement", 32 | description: "Specify a container element to portal the content into.", 33 | }, 34 | ]; 35 | 36 | const dialogContentProps: PropDef[] = [ 37 | { 38 | name: "className", 39 | type: "string", 40 | typeSimple: "string", 41 | description: "Additional CSS class names to apply to the dialog content.", 42 | }, 43 | { 44 | name: "children", 45 | required: true, 46 | type: "React.ReactNode", 47 | typeSimple: "ReactNode", 48 | description: "The content of the dialog.", 49 | }, 50 | ]; 51 | 52 | const dialogOverlayProps: PropDef[] = [ 53 | { 54 | name: "className", 55 | type: "string", 56 | typeSimple: "string", 57 | description: "Additional CSS class names to apply to the dialog overlay.", 58 | }, 59 | ]; 60 | 61 | const dialogHeaderProps: PropDef[] = [ 62 | { 63 | name: "className", 64 | type: "string", 65 | typeSimple: "string", 66 | description: "Additional CSS class names to apply to the dialog header.", 67 | }, 68 | ]; 69 | 70 | const dialogFooterProps: PropDef[] = [ 71 | { 72 | name: "className", 73 | type: "string", 74 | typeSimple: "string", 75 | description: "Additional CSS class names to apply to the dialog footer.", 76 | }, 77 | ]; 78 | 79 | const dialogTitleProps: PropDef[] = [ 80 | { 81 | name: "className", 82 | type: "string", 83 | typeSimple: "string", 84 | description: "Additional CSS class names to apply to the dialog title.", 85 | }, 86 | ]; 87 | 88 | const dialogDescriptionProps: PropDef[] = [ 89 | { 90 | name: "className", 91 | type: "string", 92 | typeSimple: "string", 93 | description: 94 | "Additional CSS class names to apply to the dialog description.", 95 | }, 96 | ]; 97 | 98 | const dialogCloseProps: PropDef[] = [ 99 | { 100 | name: "className", 101 | type: "string", 102 | typeSimple: "string", 103 | description: "Additional CSS class names to apply to the close button.", 104 | }, 105 | ]; 106 | 107 | const innerDialogProps: PropDef[] = [ 108 | { 109 | name: "children", 110 | required: true, 111 | type: "React.ReactNode", 112 | typeSimple: "ReactNode", 113 | description: "The content of the inner dialog.", 114 | }, 115 | ]; 116 | 117 | const innerDialogTriggerProps: PropDef[] = [ 118 | { 119 | name: "asChild", 120 | type: "boolean", 121 | default: "false", 122 | description: 123 | "Change the component to the HTML tag or custom component of the only child. This will merge the original component props with the props of the supplied element/component and change the underlying DOM node.", 124 | typeSimple: "boolean", 125 | }, 126 | ]; 127 | 128 | const innerDialogContentProps: PropDef[] = [ 129 | { 130 | name: "position", 131 | type: '"default" | "bottom" | "top" | "left" | "right"', 132 | typeSimple: "string", 133 | default: '"default"', 134 | description: "The position of the inner dialog.", 135 | }, 136 | { 137 | name: "draggable", 138 | type: "boolean", 139 | typeSimple: "boolean", 140 | default: "false", 141 | description: "Whether the inner dialog is draggable.", 142 | }, 143 | { 144 | name: "className", 145 | type: "string", 146 | typeSimple: "string", 147 | description: 148 | "Additional CSS class names to apply to the inner dialog content.", 149 | }, 150 | ]; 151 | 152 | const innerDialogHeaderProps: PropDef[] = [ 153 | { 154 | name: "className", 155 | type: "string", 156 | typeSimple: "string", 157 | description: 158 | "Additional CSS class names to apply to the inner dialog header.", 159 | }, 160 | ]; 161 | 162 | const innerDialogFooterProps: PropDef[] = [ 163 | { 164 | name: "className", 165 | type: "string", 166 | typeSimple: "string", 167 | description: 168 | "Additional CSS class names to apply to the inner dialog footer.", 169 | }, 170 | ]; 171 | 172 | const innerDialogTitleProps: PropDef[] = [ 173 | { 174 | name: "className", 175 | type: "string", 176 | typeSimple: "string", 177 | description: 178 | "Additional CSS class names to apply to the inner dialog title.", 179 | }, 180 | ]; 181 | 182 | const innerDialogDescriptionProps: PropDef[] = [ 183 | { 184 | name: "className", 185 | type: "string", 186 | typeSimple: "string", 187 | description: 188 | "Additional CSS class names to apply to the inner dialog description.", 189 | }, 190 | ]; 191 | 192 | const innerDialogCloseProps: PropDef[] = [ 193 | { 194 | name: "className", 195 | type: "string", 196 | typeSimple: "string", 197 | description: 198 | "Additional CSS class names to apply to the inner dialog close button.", 199 | }, 200 | ]; 201 | 202 | export default function DialogPropsTable() { 203 | return ( 204 |
205 |
206 |

Dialog

207 | 208 |
209 | 210 |
211 |

Dialog Trigger

212 | 213 |
214 | 215 |
216 |

Dialog Portal

217 | 218 |
219 | 220 |
221 |

Dialog Content

222 | 223 |
224 | 225 |
226 |

Dialog Overlay

227 | 228 |
229 | 230 |
231 |

Dialog Header

232 | 233 |
234 | 235 |
236 |

Dialog Footer

237 | 238 |
239 | 240 |
241 |

Dialog Title

242 | 243 |
244 | 245 |
246 |

Dialog Description

247 | 248 |
249 | 250 |
251 |

Dialog Close

252 | 253 |
254 | 255 |
256 | 257 |
258 |

Inner Dialog

259 | 260 |
261 | 262 |
263 |

Inner Dialog Trigger

264 | 265 |
266 | 267 |
268 |

Inner Dialog Content

269 | 270 |
271 | 272 |
273 |

Inner Dialog Header

274 | 275 |
276 | 277 |
278 |

Inner Dialog Footer

279 | 280 |
281 | 282 |
283 |

Inner Dialog Title

284 | 285 |
286 | 287 |
288 |

289 | Inner Dialog Description 290 |

291 | 292 |
293 | 294 |
295 |

Inner Dialog Close

296 | 297 |
298 |
299 | ); 300 | } 301 | -------------------------------------------------------------------------------- /components/hero.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "./ui/button"; 3 | import { SiGithub } from "react-icons/si"; 4 | import RenderNestedDialog from "./demos/render-nested-dialog"; 5 | import Image from "next/image"; 6 | import dialogs from "@/public/dialogs.svg"; 7 | import Link from "next/link"; 8 | import Demonstration from "./demonstration"; 9 | 10 | export default function Hero() { 11 | return ( 12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |

22 | Nested Shadcn Dialog 23 |

24 |

25 | Render multiple dialogs within each other 26 |

27 |
28 |
29 |
30 | 31 |
32 | 39 | 43 | 49 | 55 | 61 | 62 |
63 | 64 |
65 | 66 | 77 |
78 | 79 |

80 | The nested{" "} 81 | 85 | shadcn 86 | {" "} 87 | dialog allows you to use nested dialogs, meaning you can render multiple 88 | dialogs within each other, essentially stacking dialogs. 89 | {/* 90 | The nested dialog uses a shared context to manage state, making it 91 | easy to control multiple layers. The outer dialog handles the inner 92 | one, so transitions are smooth and everything just works intuitively. 93 | Each layer scales visually, keeping the dialogs clear and the user 94 | experience seamless. 95 | */} 96 |

97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /components/code-block.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useMemo } from "react"; 4 | import type { BundledLanguage } from "shiki"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | interface CodeBlockProps { 8 | fileName?: string; 9 | code?: string | { [key: string]: { code: string; lang?: BundledLanguage } }; 10 | lang?: BundledLanguage; 11 | type?: "file" | "terminal"; 12 | showLineNumbers?: boolean; 13 | className?: string; 14 | } 15 | 16 | export default function CodeBlock({ 17 | fileName, 18 | code, 19 | lang = "tsx", 20 | type = "file", 21 | showLineNumbers = false, 22 | className, 23 | }: CodeBlockProps) { 24 | const [copied, setCopied] = useState(false); 25 | const [html, setHtml] = useState([]); 26 | 27 | const content = useMemo(() => { 28 | if (typeof code === "object") { 29 | const [, content] = Object.entries(code)[0]; 30 | return { code: content.code, lang: content.lang || lang }; 31 | } 32 | return { code: code as string, lang }; 33 | }, [code, lang]); 34 | 35 | useEffect(() => { 36 | let isMounted = true; 37 | import("shiki").then(({ getHighlighter }) => { 38 | getHighlighter({ 39 | themes: ["github-light", "github-dark"], 40 | langs: [content.lang], 41 | }).then((highlighter) => { 42 | if (isMounted) { 43 | const highlighted = highlighter.codeToHtml(content.code, { 44 | lang: content.lang, 45 | themes: { 46 | light: "github-light", 47 | dark: "github-dark", 48 | }, 49 | }); 50 | setHtml([highlighted]); 51 | } 52 | }); 53 | }); 54 | 55 | return () => { 56 | isMounted = false; 57 | }; 58 | }, [content]); 59 | 60 | const copyToClipboard = () => { 61 | navigator.clipboard.writeText(content.code).then(() => { 62 | setCopied(true); 63 | setTimeout(() => setCopied(false), 1000); 64 | }); 65 | }; 66 | 67 | const IconComponent = type === "terminal" ? Terminal : File; 68 | 69 | return ( 70 |
76 | {fileName && ( 77 |
78 |
79 | 80 | 81 | {fileName} 82 | 83 |
84 | 129 |
130 | )} 131 |
132 |
133 | {!fileName && ( 134 | 179 | )} 180 |
181 |
184 |           
185 |             
186 |               {content.code.split("\n").map((line, lineIndex) => (
187 |                 
188 |                   {showLineNumbers && (
189 |                     
192 |                   )}
193 |                   
205 |                 
206 |               ))}
207 |             
208 |           
190 | {lineIndex + 1} 191 | 194 | {html[0] && ( 195 | 203 | )} 204 |
209 |
210 |
211 |
212 | ); 213 | } 214 | 215 | function Terminal({ className }: { className?: string }) { 216 | return ( 217 | 225 | 231 | 232 | ); 233 | } 234 | 235 | function File({ className }: { className?: string }) { 236 | return ( 237 | 245 | 249 | 253 | 254 | ); 255 | } 256 | -------------------------------------------------------------------------------- /public/registry/dialog.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dialog", 3 | "type": "registry:ui", 4 | "dependencies": [ 5 | "@radix-ui/react-dialog", 6 | "lucide-react" 7 | ], 8 | "files": [ 9 | { 10 | "type": "registry:ui", 11 | "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DialogContextValue {\n innerOpen: boolean;\n setInnerOpen: React.Dispatch>;\n}\n\nconst DialogContext = React.createContext(\n undefined,\n);\n\nfunction Dialog({ children }: { children: React.ReactNode }) {\n const [outerOpen, setOuterOpen] = React.useState(false);\n const [innerOpen, setInnerOpen] = React.useState(false);\n\n return (\n \n \n {children}\n \n \n );\n}\n\nconst DialogTrigger = DialogPrimitive.Trigger;\nconst DialogPortal = DialogPrimitive.Portal;\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => {\n const context = React.useContext(DialogContext);\n if (!context) throw new Error(\"DialogContent must be used within a Dialog\");\n\n return (\n \n \n \n {children}\n \n \n Close\n \n \n \n );\n});\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nfunction InnerDialog({ children }: { children: React.ReactNode }) {\n const context = React.useContext(DialogContext);\n if (!context) throw new Error(\"InnerDialog must be used within a Dialog\");\n\n React.useEffect(() => {\n const handleEscapeKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\" && context.innerOpen) {\n context.setInnerOpen(false);\n event.stopPropagation();\n }\n };\n\n document.addEventListener(\"keydown\", handleEscapeKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", handleEscapeKeyDown);\n };\n }, [context.innerOpen, context.setInnerOpen]);\n\n return (\n \n {children}\n \n );\n}\n\nconst InnerDialogTrigger = DialogPrimitive.Trigger;\nconst InnerDialogClose = DialogPrimitive.Close;\n\ninterface InnerDialogContentProps\n extends React.ComponentPropsWithoutRef {\n position?: \"default\" | \"bottom\" | \"top\" | \"left\" | \"right\";\n draggable?: boolean;\n}\n\nconst InnerDialogContent = React.forwardRef<\n React.ElementRef,\n InnerDialogContentProps\n>(\n (\n { className, children, position = \"default\", draggable = false, ...props },\n ref,\n ) => {\n const context = React.useContext(DialogContext);\n if (!context)\n throw new Error(\"InnerDialogContent must be used within a Dialog\");\n\n const [isDragging, setIsDragging] = React.useState(false);\n const [startY, setStartY] = React.useState(0);\n const [currentY, setCurrentY] = React.useState(0);\n const [isClosingByDrag, setIsClosingByDrag] = React.useState(false);\n const contentRef = React.useRef(null);\n\n React.useEffect(() => {\n if (context.innerOpen) {\n setCurrentY(0);\n setIsClosingByDrag(false);\n }\n }, [context.innerOpen]);\n\n const handlePointerDown = (e: React.PointerEvent) => {\n if (!draggable) return;\n setIsDragging(true);\n setStartY(e.clientY - currentY);\n e.currentTarget.setPointerCapture(e.pointerId);\n };\n\n const handlePointerMove = (e: React.PointerEvent) => {\n if (!isDragging || !draggable) return;\n const newY = e.clientY - startY;\n setCurrentY(newY > 0 ? newY : 0);\n };\n\n const handlePointerUp = () => {\n if (!draggable) return;\n setIsDragging(false);\n if (currentY > (contentRef.current?.offsetHeight || 0) / 2) {\n setIsClosingByDrag(true);\n context.setInnerOpen(false);\n } else {\n setCurrentY(0);\n }\n };\n\n return (\n \n \n
{children}
\n \n \n Close\n \n \n
\n );\n },\n);\nInnerDialogContent.displayName = \"InnerDialogContent\";\n\nconst InnerDialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes) => (\n \n);\nInnerDialogHeader.displayName = \"InnerDialogHeader\";\n\nconst InnerDialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes) => (\n \n);\nInnerDialogFooter.displayName = \"InnerDialogFooter\";\n\nconst InnerDialogTitle = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nInnerDialogTitle.displayName = \"InnerDialogTitle\";\n\nconst InnerDialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nInnerDialogDescription.displayName = \"InnerDialogDescription\";\n\nconst DialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes) => (\n \n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes) => (\n \n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport type { InnerDialogContentProps };\nexport {\n Dialog,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n DialogClose,\n InnerDialog,\n InnerDialogTrigger,\n InnerDialogContent,\n InnerDialogHeader,\n InnerDialogFooter,\n InnerDialogTitle,\n InnerDialogDescription,\n InnerDialogClose,\n DialogPortal,\n DialogOverlay,\n};\n", 12 | "path": "ui/dialog.tsx", 13 | "target": "components/ui/dialog.tsx" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { X } from "lucide-react"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | interface DialogContextValue { 9 | innerOpen: boolean; 10 | setInnerOpen: React.Dispatch>; 11 | } 12 | 13 | const DialogContext = React.createContext( 14 | undefined, 15 | ); 16 | 17 | function Dialog({ children }: { children: React.ReactNode }) { 18 | const [outerOpen, setOuterOpen] = React.useState(false); 19 | const [innerOpen, setInnerOpen] = React.useState(false); 20 | 21 | return ( 22 | 23 | 24 | {children} 25 | 26 | 27 | ); 28 | } 29 | 30 | const DialogTrigger = DialogPrimitive.Trigger; 31 | const DialogPortal = DialogPrimitive.Portal; 32 | const DialogClose = DialogPrimitive.Close; 33 | 34 | const DialogOverlay = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 46 | )); 47 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 48 | 49 | const DialogContent = React.forwardRef< 50 | React.ElementRef, 51 | React.ComponentPropsWithoutRef 52 | >(({ className, children, ...props }, ref) => { 53 | const context = React.useContext(DialogContext); 54 | if (!context) throw new Error("DialogContent must be used within a Dialog"); 55 | 56 | return ( 57 | 58 | 59 | 68 | {children} 69 | 70 | 71 | Close 72 | 73 | 74 | 75 | ); 76 | }); 77 | DialogContent.displayName = DialogPrimitive.Content.displayName; 78 | 79 | function InnerDialog({ children }: { children: React.ReactNode }) { 80 | const context = React.useContext(DialogContext); 81 | if (!context) throw new Error("InnerDialog must be used within a Dialog"); 82 | 83 | React.useEffect(() => { 84 | const handleEscapeKeyDown = (event: KeyboardEvent) => { 85 | if (event.key === "Escape" && context.innerOpen) { 86 | context.setInnerOpen(false); 87 | event.stopPropagation(); 88 | } 89 | }; 90 | 91 | document.addEventListener("keydown", handleEscapeKeyDown); 92 | return () => { 93 | document.removeEventListener("keydown", handleEscapeKeyDown); 94 | }; 95 | }, [context.innerOpen, context.setInnerOpen]); 96 | 97 | return ( 98 | 102 | {children} 103 | 104 | ); 105 | } 106 | 107 | const InnerDialogTrigger = DialogPrimitive.Trigger; 108 | const InnerDialogClose = DialogPrimitive.Close; 109 | 110 | interface InnerDialogContentProps 111 | extends React.ComponentPropsWithoutRef { 112 | position?: "default" | "bottom" | "top" | "left" | "right"; 113 | draggable?: boolean; 114 | } 115 | 116 | const InnerDialogContent = React.forwardRef< 117 | React.ElementRef, 118 | InnerDialogContentProps 119 | >( 120 | ( 121 | { className, children, position = "default", draggable = false, ...props }, 122 | ref, 123 | ) => { 124 | const context = React.useContext(DialogContext); 125 | if (!context) 126 | throw new Error("InnerDialogContent must be used within a Dialog"); 127 | 128 | const [isDragging, setIsDragging] = React.useState(false); 129 | const [startY, setStartY] = React.useState(0); 130 | const [currentY, setCurrentY] = React.useState(0); 131 | const [isClosingByDrag, setIsClosingByDrag] = React.useState(false); 132 | const contentRef = React.useRef(null); 133 | 134 | React.useEffect(() => { 135 | if (context.innerOpen) { 136 | setCurrentY(0); 137 | setIsClosingByDrag(false); 138 | } 139 | }, [context.innerOpen]); 140 | 141 | const handlePointerDown = (e: React.PointerEvent) => { 142 | if (!draggable) return; 143 | setIsDragging(true); 144 | setStartY(e.clientY - currentY); 145 | e.currentTarget.setPointerCapture(e.pointerId); 146 | }; 147 | 148 | const handlePointerMove = (e: React.PointerEvent) => { 149 | if (!isDragging || !draggable) return; 150 | const newY = e.clientY - startY; 151 | setCurrentY(newY > 0 ? newY : 0); 152 | }; 153 | 154 | const handlePointerUp = () => { 155 | if (!draggable) return; 156 | setIsDragging(false); 157 | if (currentY > (contentRef.current?.offsetHeight || 0) / 2) { 158 | setIsClosingByDrag(true); 159 | context.setInnerOpen(false); 160 | } else { 161 | setCurrentY(0); 162 | } 163 | }; 164 | 165 | return ( 166 | 167 | 196 |
{children}
197 | 198 | 199 | Close 200 | 201 |
202 |
203 | ); 204 | }, 205 | ); 206 | InnerDialogContent.displayName = "InnerDialogContent"; 207 | 208 | const InnerDialogHeader = ({ 209 | className, 210 | ...props 211 | }: React.HTMLAttributes) => ( 212 |
219 | ); 220 | InnerDialogHeader.displayName = "InnerDialogHeader"; 221 | 222 | const InnerDialogFooter = ({ 223 | className, 224 | ...props 225 | }: React.HTMLAttributes) => ( 226 |
230 | ); 231 | InnerDialogFooter.displayName = "InnerDialogFooter"; 232 | 233 | const InnerDialogTitle = React.forwardRef< 234 | React.ElementRef, 235 | React.ComponentPropsWithoutRef 236 | >(({ className, ...props }, ref) => ( 237 | 245 | )); 246 | InnerDialogTitle.displayName = "InnerDialogTitle"; 247 | 248 | const InnerDialogDescription = React.forwardRef< 249 | React.ElementRef, 250 | React.ComponentPropsWithoutRef 251 | >(({ className, ...props }, ref) => ( 252 | 257 | )); 258 | InnerDialogDescription.displayName = "InnerDialogDescription"; 259 | 260 | const DialogHeader = ({ 261 | className, 262 | ...props 263 | }: React.HTMLAttributes) => ( 264 |
271 | ); 272 | DialogHeader.displayName = "DialogHeader"; 273 | 274 | const DialogFooter = ({ 275 | className, 276 | ...props 277 | }: React.HTMLAttributes) => ( 278 |
282 | ); 283 | DialogFooter.displayName = "DialogFooter"; 284 | 285 | const DialogTitle = React.forwardRef< 286 | React.ElementRef, 287 | React.ComponentPropsWithoutRef 288 | >(({ className, ...props }, ref) => ( 289 | 297 | )); 298 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 299 | 300 | const DialogDescription = React.forwardRef< 301 | React.ElementRef, 302 | React.ComponentPropsWithoutRef 303 | >(({ className, ...props }, ref) => ( 304 | 309 | )); 310 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 311 | 312 | export type { InnerDialogContentProps }; 313 | export { 314 | Dialog, 315 | DialogTrigger, 316 | DialogContent, 317 | DialogHeader, 318 | DialogFooter, 319 | DialogTitle, 320 | DialogDescription, 321 | DialogClose, 322 | InnerDialog, 323 | InnerDialogTrigger, 324 | InnerDialogContent, 325 | InnerDialogHeader, 326 | InnerDialogFooter, 327 | InnerDialogTitle, 328 | InnerDialogDescription, 329 | InnerDialogClose, 330 | DialogPortal, 331 | DialogOverlay, 332 | }; 333 | -------------------------------------------------------------------------------- /registry/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { X } from "lucide-react"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | interface DialogContextValue { 9 | innerOpen: boolean; 10 | setInnerOpen: React.Dispatch>; 11 | } 12 | 13 | const DialogContext = React.createContext( 14 | undefined, 15 | ); 16 | 17 | function Dialog({ children }: { children: React.ReactNode }) { 18 | const [outerOpen, setOuterOpen] = React.useState(false); 19 | const [innerOpen, setInnerOpen] = React.useState(false); 20 | 21 | return ( 22 | 23 | 24 | {children} 25 | 26 | 27 | ); 28 | } 29 | 30 | const DialogTrigger = DialogPrimitive.Trigger; 31 | const DialogPortal = DialogPrimitive.Portal; 32 | const DialogClose = DialogPrimitive.Close; 33 | 34 | const DialogOverlay = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 46 | )); 47 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 48 | 49 | const DialogContent = React.forwardRef< 50 | React.ElementRef, 51 | React.ComponentPropsWithoutRef 52 | >(({ className, children, ...props }, ref) => { 53 | const context = React.useContext(DialogContext); 54 | if (!context) throw new Error("DialogContent must be used within a Dialog"); 55 | 56 | return ( 57 | 58 | 59 | 68 | {children} 69 | 70 | 71 | Close 72 | 73 | 74 | 75 | ); 76 | }); 77 | DialogContent.displayName = DialogPrimitive.Content.displayName; 78 | 79 | function InnerDialog({ children }: { children: React.ReactNode }) { 80 | const context = React.useContext(DialogContext); 81 | if (!context) throw new Error("InnerDialog must be used within a Dialog"); 82 | 83 | React.useEffect(() => { 84 | const handleEscapeKeyDown = (event: KeyboardEvent) => { 85 | if (event.key === "Escape" && context.innerOpen) { 86 | context.setInnerOpen(false); 87 | event.stopPropagation(); 88 | } 89 | }; 90 | 91 | document.addEventListener("keydown", handleEscapeKeyDown); 92 | return () => { 93 | document.removeEventListener("keydown", handleEscapeKeyDown); 94 | }; 95 | }, [context.innerOpen, context.setInnerOpen]); 96 | 97 | return ( 98 | 102 | {children} 103 | 104 | ); 105 | } 106 | 107 | const InnerDialogTrigger = DialogPrimitive.Trigger; 108 | const InnerDialogClose = DialogPrimitive.Close; 109 | 110 | interface InnerDialogContentProps 111 | extends React.ComponentPropsWithoutRef { 112 | position?: "default" | "bottom" | "top" | "left" | "right"; 113 | draggable?: boolean; 114 | } 115 | 116 | const InnerDialogContent = React.forwardRef< 117 | React.ElementRef, 118 | InnerDialogContentProps 119 | >( 120 | ( 121 | { className, children, position = "default", draggable = false, ...props }, 122 | ref, 123 | ) => { 124 | const context = React.useContext(DialogContext); 125 | if (!context) 126 | throw new Error("InnerDialogContent must be used within a Dialog"); 127 | 128 | const [isDragging, setIsDragging] = React.useState(false); 129 | const [startY, setStartY] = React.useState(0); 130 | const [currentY, setCurrentY] = React.useState(0); 131 | const [isClosingByDrag, setIsClosingByDrag] = React.useState(false); 132 | const contentRef = React.useRef(null); 133 | 134 | React.useEffect(() => { 135 | if (context.innerOpen) { 136 | setCurrentY(0); 137 | setIsClosingByDrag(false); 138 | } 139 | }, [context.innerOpen]); 140 | 141 | const handlePointerDown = (e: React.PointerEvent) => { 142 | if (!draggable) return; 143 | setIsDragging(true); 144 | setStartY(e.clientY - currentY); 145 | e.currentTarget.setPointerCapture(e.pointerId); 146 | }; 147 | 148 | const handlePointerMove = (e: React.PointerEvent) => { 149 | if (!isDragging || !draggable) return; 150 | const newY = e.clientY - startY; 151 | setCurrentY(newY > 0 ? newY : 0); 152 | }; 153 | 154 | const handlePointerUp = () => { 155 | if (!draggable) return; 156 | setIsDragging(false); 157 | if (currentY > (contentRef.current?.offsetHeight || 0) / 2) { 158 | setIsClosingByDrag(true); 159 | context.setInnerOpen(false); 160 | } else { 161 | setCurrentY(0); 162 | } 163 | }; 164 | 165 | return ( 166 | 167 | 196 |
{children}
197 | 198 | 199 | Close 200 | 201 |
202 |
203 | ); 204 | }, 205 | ); 206 | InnerDialogContent.displayName = "InnerDialogContent"; 207 | 208 | const InnerDialogHeader = ({ 209 | className, 210 | ...props 211 | }: React.HTMLAttributes) => ( 212 |
219 | ); 220 | InnerDialogHeader.displayName = "InnerDialogHeader"; 221 | 222 | const InnerDialogFooter = ({ 223 | className, 224 | ...props 225 | }: React.HTMLAttributes) => ( 226 |
230 | ); 231 | InnerDialogFooter.displayName = "InnerDialogFooter"; 232 | 233 | const InnerDialogTitle = React.forwardRef< 234 | React.ElementRef, 235 | React.ComponentPropsWithoutRef 236 | >(({ className, ...props }, ref) => ( 237 | 245 | )); 246 | InnerDialogTitle.displayName = "InnerDialogTitle"; 247 | 248 | const InnerDialogDescription = React.forwardRef< 249 | React.ElementRef, 250 | React.ComponentPropsWithoutRef 251 | >(({ className, ...props }, ref) => ( 252 | 257 | )); 258 | InnerDialogDescription.displayName = "InnerDialogDescription"; 259 | 260 | const DialogHeader = ({ 261 | className, 262 | ...props 263 | }: React.HTMLAttributes) => ( 264 |
271 | ); 272 | DialogHeader.displayName = "DialogHeader"; 273 | 274 | const DialogFooter = ({ 275 | className, 276 | ...props 277 | }: React.HTMLAttributes) => ( 278 |
282 | ); 283 | DialogFooter.displayName = "DialogFooter"; 284 | 285 | const DialogTitle = React.forwardRef< 286 | React.ElementRef, 287 | React.ComponentPropsWithoutRef 288 | >(({ className, ...props }, ref) => ( 289 | 297 | )); 298 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 299 | 300 | const DialogDescription = React.forwardRef< 301 | React.ElementRef, 302 | React.ComponentPropsWithoutRef 303 | >(({ className, ...props }, ref) => ( 304 | 309 | )); 310 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 311 | 312 | export type { InnerDialogContentProps }; 313 | export { 314 | Dialog, 315 | DialogTrigger, 316 | DialogContent, 317 | DialogHeader, 318 | DialogFooter, 319 | DialogTitle, 320 | DialogDescription, 321 | DialogClose, 322 | InnerDialog, 323 | InnerDialogTrigger, 324 | InnerDialogContent, 325 | InnerDialogHeader, 326 | InnerDialogFooter, 327 | InnerDialogTitle, 328 | InnerDialogDescription, 329 | InnerDialogClose, 330 | DialogPortal, 331 | DialogOverlay, 332 | }; 333 | -------------------------------------------------------------------------------- /components/manual.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CodeBlock from "./code-block"; 3 | 4 | export default function DialogCode() { 5 | return ( 6 |
7 |
8 |

Manual Installation

9 |

10 | Install the necessary dependencies and add the following files 11 |

12 |
13 |
14 | 22 | >; 37 | } 38 | 39 | const DialogContext = React.createContext( 40 | undefined, 41 | ); 42 | 43 | function Dialog({ children }: { children: React.ReactNode }) { 44 | const [outerOpen, setOuterOpen] = React.useState(false); 45 | const [innerOpen, setInnerOpen] = React.useState(false); 46 | 47 | return ( 48 | 49 | 50 | {children} 51 | 52 | 53 | ); 54 | } 55 | 56 | const DialogTrigger = DialogPrimitive.Trigger; 57 | const DialogPortal = DialogPrimitive.Portal; 58 | const DialogClose = DialogPrimitive.Close; 59 | 60 | const DialogOverlay = React.forwardRef< 61 | React.ElementRef, 62 | React.ComponentPropsWithoutRef 63 | >(({ className, ...props }, ref) => ( 64 | 72 | )); 73 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 74 | 75 | const DialogContent = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, ...props }, ref) => { 79 | const context = React.useContext(DialogContext); 80 | if (!context) throw new Error("DialogContent must be used within a Dialog"); 81 | 82 | return ( 83 | 84 | 85 | 94 | {children} 95 | 96 | 97 | Close 98 | 99 | 100 | 101 | ); 102 | }); 103 | DialogContent.displayName = DialogPrimitive.Content.displayName; 104 | 105 | function InnerDialog({ children }: { children: React.ReactNode }) { 106 | const context = React.useContext(DialogContext); 107 | if (!context) throw new Error("InnerDialog must be used within a Dialog"); 108 | 109 | React.useEffect(() => { 110 | const handleEscapeKeyDown = (event: KeyboardEvent) => { 111 | if (event.key === "Escape" && context.innerOpen) { 112 | context.setInnerOpen(false); 113 | event.stopPropagation(); 114 | } 115 | }; 116 | 117 | document.addEventListener("keydown", handleEscapeKeyDown); 118 | return () => { 119 | document.removeEventListener("keydown", handleEscapeKeyDown); 120 | }; 121 | }, [context.innerOpen, context.setInnerOpen]); 122 | 123 | return ( 124 | 128 | {children} 129 | 130 | ); 131 | } 132 | 133 | const InnerDialogTrigger = DialogPrimitive.Trigger; 134 | const InnerDialogClose = DialogPrimitive.Close; 135 | 136 | interface InnerDialogContentProps 137 | extends React.ComponentPropsWithoutRef { 138 | position?: "default" | "bottom" | "top" | "left" | "right"; 139 | draggable?: boolean; 140 | } 141 | 142 | const InnerDialogContent = React.forwardRef< 143 | React.ElementRef, 144 | InnerDialogContentProps 145 | >( 146 | ( 147 | { className, children, position = "default", draggable = false, ...props }, 148 | ref, 149 | ) => { 150 | const context = React.useContext(DialogContext); 151 | if (!context) 152 | throw new Error("InnerDialogContent must be used within a Dialog"); 153 | 154 | const [isDragging, setIsDragging] = React.useState(false); 155 | const [startY, setStartY] = React.useState(0); 156 | const [currentY, setCurrentY] = React.useState(0); 157 | const [isClosingByDrag, setIsClosingByDrag] = React.useState(false); 158 | const contentRef = React.useRef(null); 159 | 160 | React.useEffect(() => { 161 | if (context.innerOpen) { 162 | setCurrentY(0); 163 | setIsClosingByDrag(false); 164 | } 165 | }, [context.innerOpen]); 166 | 167 | const handlePointerDown = (e: React.PointerEvent) => { 168 | if (!draggable) return; 169 | setIsDragging(true); 170 | setStartY(e.clientY - currentY); 171 | e.currentTarget.setPointerCapture(e.pointerId); 172 | }; 173 | 174 | const handlePointerMove = (e: React.PointerEvent) => { 175 | if (!isDragging || !draggable) return; 176 | const newY = e.clientY - startY; 177 | setCurrentY(newY > 0 ? newY : 0); 178 | }; 179 | 180 | const handlePointerUp = () => { 181 | if (!draggable) return; 182 | setIsDragging(false); 183 | if (currentY > (contentRef.current?.offsetHeight || 0) / 2) { 184 | setIsClosingByDrag(true); 185 | context.setInnerOpen(false); 186 | } else { 187 | setCurrentY(0); 188 | } 189 | }; 190 | 191 | return ( 192 | 193 | 222 |
{children}
223 | 224 | 225 | Close 226 | 227 |
228 |
229 | ); 230 | }, 231 | ); 232 | InnerDialogContent.displayName = "InnerDialogContent"; 233 | 234 | const InnerDialogHeader = ({ 235 | className, 236 | ...props 237 | }: React.HTMLAttributes) => ( 238 |
245 | ); 246 | InnerDialogHeader.displayName = "InnerDialogHeader"; 247 | 248 | const InnerDialogFooter = ({ 249 | className, 250 | ...props 251 | }: React.HTMLAttributes) => ( 252 |
256 | ); 257 | InnerDialogFooter.displayName = "InnerDialogFooter"; 258 | 259 | const InnerDialogTitle = React.forwardRef< 260 | React.ElementRef, 261 | React.ComponentPropsWithoutRef 262 | >(({ className, ...props }, ref) => ( 263 | 271 | )); 272 | InnerDialogTitle.displayName = "InnerDialogTitle"; 273 | 274 | const InnerDialogDescription = React.forwardRef< 275 | React.ElementRef, 276 | React.ComponentPropsWithoutRef 277 | >(({ className, ...props }, ref) => ( 278 | 283 | )); 284 | InnerDialogDescription.displayName = "InnerDialogDescription"; 285 | 286 | const DialogHeader = ({ 287 | className, 288 | ...props 289 | }: React.HTMLAttributes) => ( 290 |
297 | ); 298 | DialogHeader.displayName = "DialogHeader"; 299 | 300 | const DialogFooter = ({ 301 | className, 302 | ...props 303 | }: React.HTMLAttributes) => ( 304 |
308 | ); 309 | DialogFooter.displayName = "DialogFooter"; 310 | 311 | const DialogTitle = React.forwardRef< 312 | React.ElementRef, 313 | React.ComponentPropsWithoutRef 314 | >(({ className, ...props }, ref) => ( 315 | 323 | )); 324 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 325 | 326 | const DialogDescription = React.forwardRef< 327 | React.ElementRef, 328 | React.ComponentPropsWithoutRef 329 | >(({ className, ...props }, ref) => ( 330 | 335 | )); 336 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 337 | 338 | export type { InnerDialogContentProps }; 339 | export { 340 | Dialog, 341 | DialogTrigger, 342 | DialogContent, 343 | DialogHeader, 344 | DialogFooter, 345 | DialogTitle, 346 | DialogDescription, 347 | DialogClose, 348 | InnerDialog, 349 | InnerDialogTrigger, 350 | InnerDialogContent, 351 | InnerDialogHeader, 352 | InnerDialogFooter, 353 | InnerDialogTitle, 354 | InnerDialogDescription, 355 | InnerDialogClose, 356 | DialogPortal, 357 | DialogOverlay, 358 | };`} 359 | /> 360 | 372 |
373 |
374 | ); 375 | } 376 | -------------------------------------------------------------------------------- /public/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------