├── .commitlintrc.js ├── .eslintrc.json ├── .github └── workflows │ └── code-check.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── LICENSE.md ├── README.md ├── app ├── (home) │ ├── page.tsx │ └── sections │ │ ├── hero.tsx │ │ ├── setup.tsx │ │ └── variants.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── robots.ts └── sitemap.ts ├── components.json ├── components ├── code-block.tsx ├── copy-button.tsx ├── examples │ ├── form-in-dialog-example.tsx │ ├── form-in-sheet-example.tsx │ └── standalone-form-dialog-example.tsx ├── mode-toggle.tsx ├── pre.tsx ├── snippet.tsx ├── svg-icons.tsx ├── theme-provider.tsx └── ui │ ├── accordion.tsx │ ├── button.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── phone-input.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── sheet.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── toast.tsx │ ├── toaster.tsx │ └── use-toast.ts ├── config └── site.ts ├── content └── snippets │ └── phone-input.mdx ├── contentlayer.config.ts ├── hooks └── use-copy.tsx ├── lib └── utils.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.mjs ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── types └── index.d.ts /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | // Changes that affect the build system or dependency-only changes 9 | "build", 10 | // Changes to CI workflows 11 | "ci", 12 | // Documentation-only changes 13 | "docs", 14 | // A new feature 15 | "feat", 16 | //A bug fix 17 | "fix", 18 | // A code change that improves performance 19 | "perf", 20 | // A code change that neither fixes a bug nor adds a feature 21 | "refactor", 22 | // A commit that reverts a previous commit 23 | "revert", 24 | // Changes that do not affect the meaning of the code 25 | "style", 26 | // Adding missing tests or correcting existing tests 27 | "test", 28 | ], 29 | ], 30 | "scope-enum": [ 31 | 2, 32 | "always", 33 | [ 34 | // Dependency-related changes 35 | "deps", 36 | // ESLint-related changes 37 | "eslint", 38 | // Prettier-related changes 39 | "prettier", 40 | // TypeScript-related changes 41 | "typescript", 42 | // Go-related changes 43 | "golang", 44 | ], 45 | ], 46 | "scope-empty": [1, "never"], 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "plugins": ["tailwindcss"], 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended", 8 | "plugin:@next/next/recommended" 9 | ], 10 | "rules": { 11 | "@next/next/no-html-link-for-pages": "off", 12 | "react/jsx-key": "off", 13 | "tailwindcss/no-custom-classname": "off", 14 | "tailwindcss/classnames-order": "off" 15 | }, 16 | "settings": { 17 | "tailwindcss": { 18 | "callees": ["cn", "cva"], 19 | "config": "tailwind.config.ts" 20 | }, 21 | "next": { 22 | "rootDir": "./" 23 | } 24 | }, 25 | "overrides": [ 26 | { 27 | "files": ["*.ts", "*.tsx"], 28 | "parser": "@typescript-eslint/parser" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/code-check.yml: -------------------------------------------------------------------------------- 1 | name: Code check 2 | 3 | on: 4 | pull_request: 5 | branches: ["*"] 6 | 7 | jobs: 8 | typecheck-lint: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v3 14 | 15 | - name: Setup Node 18 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 9.14.2 24 | 25 | - name: Get pnpm store directory 26 | id: pnpm-cache 27 | run: | 28 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 29 | 30 | - name: Setup pnpm cache 31 | uses: actions/cache@v3 32 | with: 33 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 34 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 35 | restore-keys: | 36 | ${{ runner.os }}-pnpm-store- 37 | 38 | - name: Install deps (with cache) 39 | run: pnpm install 40 | 41 | - name: Contentlayer build 42 | run: pnpm contentlayer build 43 | 44 | - name: Run typecheck 45 | run: pnpm run typecheck 46 | -------------------------------------------------------------------------------- /.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.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # contentlayer 38 | .contentlayer -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Omer Alpi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | https://github.com/omeralpi/shadcn-phone-input/assets/19254700/2f32e063-248f-40e3-8c08-041adf3954be 2 | 3 | [Shadcn Phone Input](https://shadcn-phone-input.vercel.app/) is a phone input 4 | component built as part of the Shadcn design system. It offers a blend of 5 | customization and out-of-the-box styling, adhering to Shadcn's sleek and modern 6 | design principles. 7 | 8 | ## Why 9 | 10 | I needed a phone input component for a project. I looked around for any phone 11 | input components that used Shadcn's design system, but I couldn't find any. So, 12 | I decided to make one myself. I hope you find it useful! 13 | 14 | ## Usage 15 | 16 | This component is designed to handle phone inputs in your application. It 17 | includes the option to select a country along with the phone input. 18 | 19 | > [!WARNING] 20 | > Before you dive in, just double-check that you're using version 1.0.0 of the cmdk package! 21 | 22 | ```tsx 23 | import { zodResolver } from "@hookform/resolvers/zod"; 24 | import { useForm } from "react-hook-form"; 25 | import { isValidPhoneNumber } from "react-phone-number-input"; 26 | import { z } from "zod"; 27 | 28 | import { Button } from "@/components/ui/button"; 29 | import { 30 | Form, 31 | FormControl, 32 | FormDescription, 33 | FormField, 34 | FormItem, 35 | FormLabel, 36 | FormMessage, 37 | } from "@/components/ui/form"; 38 | import { PhoneInput } from "@/components/ui/phone-input"; 39 | import { toast } from "@/components/ui/use-toast"; 40 | 41 | const FormSchema = z.object({ 42 | phone: z 43 | .string() 44 | .refine(isValidPhoneNumber, { message: "Invalid phone number" }), 45 | }); 46 | 47 | export default function Hero() { 48 | const form = useForm>({ 49 | resolver: zodResolver(FormSchema), 50 | defaultValues: { 51 | phone: "", 52 | }, 53 | }); 54 | 55 | function onSubmit(data: z.infer) { 56 | toast({ 57 | title: "You submitted the following values:", 58 | description: ( 59 |
 60 |           {JSON.stringify(data, null, 2)}
 61 |         
62 | ), 63 | }); 64 | } 65 | 66 | return ( 67 |
68 | 72 | ( 76 | 77 | Phone Number 78 | 79 | 80 | 81 | 82 | Enter a phone number 83 | 84 | 85 | 86 | )} 87 | /> 88 | 89 | 90 | 91 | ); 92 | } 93 | ``` 94 | 95 | ## Other Examples 96 | 97 | ### Optional Phone Input 98 | 99 | ```tsx 100 | const FormSchema = z.object({ 101 | phone: z 102 | .string() 103 | .refine(isValidPhoneNumber, { message: "Invalid phone number" }) 104 | .or(z.literal("")), 105 | }); 106 | ``` 107 | 108 | ## Documentation 109 | 110 | You can find out more about the API and implementation in the 111 | [Documentation](https://shadcn-phone-input.vercel.app/#setup). 112 | -------------------------------------------------------------------------------- /app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | import Hero from "@/app/(home)/sections/hero"; 6 | import Setup from "@/app/(home)/sections/setup"; 7 | import { ModeToggle } from "@/components/mode-toggle"; 8 | import { SvgIcons } from "@/components/svg-icons"; 9 | import { buttonVariants } from "@/components/ui/button"; 10 | import { siteConfig } from "@/config/site"; 11 | 12 | import Variants from "./sections/variants"; 13 | 14 | export default function Home() { 15 | return ( 16 | <> 17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | 44 |
45 |
46 |
47 |

48 | Built by{" "} 49 | 55 | Omer Alpi 56 | 57 | . 58 |

59 |
60 |
Also available:
61 | 68 | 69 | Shadcn Vue Phone Input 70 | 71 |
72 |
73 |
74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /app/(home)/sections/hero.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { FormInDialogExample } from "@/components/examples/form-in-dialog-example"; 4 | import { FormInSheetExample } from "@/components/examples/form-in-sheet-example"; 5 | import { StandaloneFormExample } from "@/components/examples/standalone-form-dialog-example"; 6 | import { buttonVariants } from "@/components/ui/button"; 7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 8 | import { siteConfig } from "@/config/site"; 9 | 10 | export default function Hero() { 11 | return ( 12 | <> 13 |
14 |
15 |

16 | Shadcn Phone Input 17 |

18 |

19 | An implementation of a Phone Input component for React, built 20 | on top of Shadcn UI's input component. 21 |

22 |
23 | 30 | Try it out 31 | 32 | 39 | Github 40 | 41 |
42 |
43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | Standalone Form 51 | 52 | Form in Dialog 53 | Form in Sheet 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /app/(home)/sections/setup.tsx: -------------------------------------------------------------------------------- 1 | import { allSnippets, Snippet as SnippetType } from "contentlayer/generated"; 2 | 3 | import CodeBlock from "@/components/code-block"; 4 | import { Snippet } from "@/components/snippet"; 5 | import { 6 | Accordion, 7 | AccordionContent, 8 | AccordionItem, 9 | AccordionTrigger, 10 | } from "@/components/ui/accordion"; 11 | 12 | const snippets: SnippetType[] = allSnippets.sort((a, b) => a.order - b.order); 13 | 14 | export default function Setup() { 15 | return ( 16 |
17 |

18 | Setup 19 |

20 |
21 |

22 | Install Shadcn via CLI 23 |

24 |

25 | Run the{" "} 26 | 27 | shadcn-ui 28 | {" "} 29 | init command to setup your project: 30 |

31 | 32 |
33 |
34 |

35 | Install necessary Shadcn components: 36 |

37 |

38 | Run the{" "} 39 | 40 | shadcn 41 | {" "} 42 | add command to add the necessary shadcn components to your project: 43 |

44 |
45 | 49 |
50 |
51 |
52 |

53 | Install necessary React Phone Number Input package: 54 |

55 | 59 |
60 |
61 |

62 | To use the phone input component: 63 |

64 | {/* */} 74 |
75 |

76 | Snippets 77 |

78 | 79 | {snippets.map((snippet) => ( 80 | 81 | 82 | {snippet.file} 83 | 84 | 85 | 86 | 87 | 88 | ))} 89 | 90 |
91 |
92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /app/(home)/sections/variants.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { 3 | Country, 4 | formatPhoneNumber, 5 | formatPhoneNumberIntl, 6 | getCountryCallingCode, 7 | } from "react-phone-number-input"; 8 | import tr from "react-phone-number-input/locale/tr"; 9 | 10 | import { PhoneInput } from "@/components/ui/phone-input"; 11 | 12 | export default function Variants() { 13 | const [country, setCountry] = useState(); 14 | const [phoneNumber, setPhoneNumber] = useState(""); 15 | 16 | return ( 17 |
18 |

19 | Variants 20 |

21 |
22 |

23 | With country 24 |

25 |

26 | The phone input component can be used with a country select dropdown. 27 |

28 |
29 |

30 | Summary 31 |

32 |
33 | 38 |
39 |
40 |
41 |

42 | Setting default country 43 |

44 |
45 | 51 |
52 |
53 |
54 |

55 | Internationalization 56 |

57 |
58 | 64 |
65 |
66 |
67 |

68 | Force international format 69 |

70 |
71 | 77 |
78 |
79 |
80 |

81 | Force national format 82 |

83 |
84 | 91 |
92 |
93 |
94 |

95 | initialValueFormat 96 |

97 |
98 | 104 |
105 |
106 |
107 |

108 | Formatting value 109 |

110 |
111 |
112 | 118 |
119 |
120 | National: {phoneNumber && formatPhoneNumber(phoneNumber)} 121 |
122 |
123 | International:{" "} 124 | {phoneNumber && formatPhoneNumberIntl(phoneNumber)} 125 |
126 |
127 | Country code: {country && getCountryCallingCode(country)} 128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omeralpi/shadcn-phone-input/f06c3c083b5c68add83aa28230e8829a4d8eb6b6/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import { Toaster } from "@/components/ui/toaster"; 7 | 8 | import { siteConfig } from "../config/site"; 9 | 10 | import "./globals.css"; 11 | 12 | const inter = Inter({ subsets: ["latin"] }); 13 | 14 | export const metadata: Metadata = { 15 | title: "Shadcn Phone Input", 16 | description: `A phone input component implementation of Shadcn's input component`, 17 | keywords: [ 18 | "shadcn", 19 | "phone input", 20 | "shadcn/ui", 21 | "shadcn phone input", 22 | "phone input component", 23 | "shadcn phone input component", 24 | "input", 25 | "radix ui", 26 | "react phone input", 27 | ], 28 | authors: [ 29 | { 30 | name: "Omer Alpi", 31 | url: "https://jaleelbennett.com", 32 | }, 33 | ], 34 | creator: "Omer Alpi", 35 | openGraph: { 36 | type: "website", 37 | locale: "en_US", 38 | url: siteConfig.url, 39 | title: siteConfig.name, 40 | description: siteConfig.description, 41 | siteName: siteConfig.name, 42 | }, 43 | twitter: { 44 | card: "summary_large_image", 45 | title: siteConfig.name, 46 | description: siteConfig.description, 47 | creator: "@omeralpi", 48 | }, 49 | }; 50 | 51 | export default function RootLayout({ 52 | children, 53 | }: { 54 | children: React.ReactNode; 55 | }) { 56 | return ( 57 | 58 | 59 | 65 | {children} 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | import { siteConfig } from "../config/site"; 4 | 5 | export default function robots(): MetadataRoute.Robots { 6 | return { 7 | rules: { 8 | userAgent: "*", 9 | allow: "/", 10 | disallow: "/private/", 11 | }, 12 | sitemap: `${siteConfig.url}/sitemap.xml`, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { type MetadataRoute } from "next"; 2 | 3 | import { siteConfig } from "../config/site"; 4 | 5 | export default function sitemap(): MetadataRoute.Sitemap { 6 | return [ 7 | { 8 | url: `${siteConfig.url}/`, 9 | lastModified: new Date(), 10 | }, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /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": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components/code-block.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | import CopyButton from "./copy-button"; 4 | 5 | function CodeBlock({ 6 | value, 7 | className, 8 | copyable = true, 9 | }: { 10 | value: string; 11 | className?: string; 12 | codeClass?: string; 13 | copyable?: boolean; 14 | codeWrap?: boolean; 15 | noCodeFont?: boolean; 16 | noMask?: boolean; 17 | }) { 18 | value = value || ""; 19 | 20 | return ( 21 |
28 |       
29 |       {value}
30 |     
31 | ); 32 | } 33 | 34 | export default CodeBlock; 35 | -------------------------------------------------------------------------------- /components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { AnimatePresence, motion, MotionConfig } from "framer-motion"; 3 | import { Check, Copy } from "lucide-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | import { Button } from "./ui/button"; 8 | 9 | export default function CopyButton({ 10 | value, 11 | }: { 12 | value: string; 13 | copyable?: boolean; 14 | }) { 15 | const [copying, setCopying] = useState(0); 16 | 17 | const onCopy = useCallback(async () => { 18 | try { 19 | await navigator.clipboard.writeText(value); 20 | setCopying((c) => c + 1); 21 | setTimeout(() => { 22 | setCopying((c) => c - 1); 23 | }, 2000); 24 | } catch (err) { 25 | console.error("Failed to copy text: ", err); 26 | } 27 | }, [value]); 28 | 29 | const variants = { 30 | visible: { opacity: 1, scale: 1 }, 31 | hidden: { opacity: 0, scale: 0.5 }, 32 | }; 33 | 34 | return ( 35 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /components/examples/form-in-dialog-example.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from "@/components/ui/dialog"; 10 | 11 | import { StandaloneFormExample } from "./standalone-form-dialog-example"; 12 | 13 | export function FormInDialogExample() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | Phone Number Form 22 | 23 | Enter your phone number in the form below. 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/examples/form-in-sheet-example.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | 3 | import { 4 | Sheet, 5 | SheetContent, 6 | SheetDescription, 7 | SheetHeader, 8 | SheetTitle, 9 | SheetTrigger, 10 | } from "../ui/sheet"; 11 | import { StandaloneFormExample } from "./standalone-form-dialog-example"; 12 | 13 | export function FormInSheetExample() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | Phone Number Form 22 | 23 | Enter your phone number in the form below. 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/examples/standalone-form-dialog-example.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from "@hookform/resolvers/zod"; 2 | import { useForm } from "react-hook-form"; 3 | import { isValidPhoneNumber } from "react-phone-number-input"; 4 | import { z } from "zod"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormDescription, 11 | FormField, 12 | FormItem, 13 | FormLabel, 14 | FormMessage, 15 | } from "@/components/ui/form"; 16 | import { PhoneInput } from "@/components/ui/phone-input"; 17 | import { toast } from "@/components/ui/use-toast"; 18 | 19 | const FormSchema = z.object({ 20 | phone: z 21 | .string() 22 | .refine(isValidPhoneNumber, { message: "Invalid phone number" }), 23 | }); 24 | 25 | export function StandaloneFormExample() { 26 | const form = useForm>({ 27 | resolver: zodResolver(FormSchema), 28 | defaultValues: { 29 | phone: "", 30 | }, 31 | }); 32 | 33 | function onSubmit(data: z.infer) { 34 | toast({ 35 | title: "You submitted the following values:", 36 | description: ( 37 |
38 |           {JSON.stringify(data, null, 2)}
39 |         
40 | ), 41 | }); 42 | } 43 | 44 | return ( 45 |
46 | 50 | ( 54 | 55 | Phone Number 56 | 57 | 58 | 59 | 60 | Enter a phone number 61 | 62 | 63 | 64 | )} 65 | /> 66 |
67 |           
68 |             {JSON.stringify(form.watch("phone"), null, 2)}
69 |           
70 |         
71 | 72 | 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 30 | 31 | 32 | setTheme("light")}> 33 | Light 34 | 35 | setTheme("dark")}> 36 | Dark 37 | 38 | setTheme("system")}> 39 | System 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/pre.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Check, Copy } from "lucide-react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | import { Button } from "./ui/button"; 7 | 8 | export default function Pre({ 9 | children, 10 | className, 11 | ...props 12 | }: React.HTMLAttributes) { 13 | const [copied, setCopied] = React.useState(false); 14 | const ref = React.useRef(null); 15 | 16 | React.useEffect(() => { 17 | let timer: ReturnType; 18 | if (copied) { 19 | timer = setTimeout(() => { 20 | setCopied(false); 21 | }, 2000); 22 | } 23 | return () => { 24 | clearTimeout(timer); 25 | }; 26 | }, [copied]); 27 | 28 | const onClick = () => { 29 | setCopied(true); 30 | const content = ref.current?.textContent; 31 | if (content) { 32 | navigator.clipboard.writeText(content); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 48 |
56 |         {children}
57 |       
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/snippet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { useMDXComponent } from "next-contentlayer/hooks"; 5 | 6 | import type { Snippet as SnippetType } from ".contentlayer/generated"; 7 | import Pre from "./pre"; 8 | 9 | const components = { 10 | pre: Pre, 11 | }; 12 | 13 | export function Snippet({ snippet }: { snippet: SnippetType }) { 14 | const MDXContent = useMDXComponent(snippet.body.code); 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /components/svg-icons.tsx: -------------------------------------------------------------------------------- 1 | export const SvgIcons = { 2 | vue: ({ ...props }) => ( 3 | 11 | 16 | 21 | 22 | ), 23 | }; 24 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDown } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )); 21 | AccordionItem.displayName = "AccordionItem"; 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className, 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )); 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )); 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 57 | 58 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; 59 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm 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-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 10 | { 11 | variants: { 12 | variant: { 13 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-10 px-4 py-2", 25 | sm: "h-9 rounded-md px-3", 26 | lg: "h-11 rounded-md px-8", 27 | icon: "size-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { type DialogProps } from "@radix-ui/react-dialog"; 5 | import { Command as CommandPrimitive } from "cmdk"; 6 | import { Search } from "lucide-react"; 7 | 8 | import { Dialog, DialogContent } from "@/components/ui/dialog"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | Command.displayName = CommandPrimitive.displayName; 25 | 26 | const CommandDialog = ({ children, ...props }: DialogProps) => { 27 | return ( 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | const CommandInput = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 |
43 | 44 | 52 |
53 | )); 54 | 55 | CommandInput.displayName = CommandPrimitive.Input.displayName; 56 | 57 | const CommandList = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 66 | )); 67 | 68 | CommandList.displayName = CommandPrimitive.List.displayName; 69 | 70 | const CommandEmpty = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >((props, ref) => ( 74 | 79 | )); 80 | 81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName; 82 | 83 | const CommandGroup = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )); 96 | 97 | CommandGroup.displayName = CommandPrimitive.Group.displayName; 98 | 99 | const CommandSeparator = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName; 110 | 111 | const CommandItem = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 123 | )); 124 | 125 | CommandItem.displayName = CommandPrimitive.Item.displayName; 126 | 127 | const CommandShortcut = ({ 128 | className, 129 | ...props 130 | }: React.HTMLAttributes) => { 131 | return ( 132 | 139 | ); 140 | }; 141 | CommandShortcut.displayName = "CommandShortcut"; 142 | 143 | export { 144 | Command, 145 | CommandDialog, 146 | CommandInput, 147 | CommandList, 148 | CommandEmpty, 149 | CommandGroup, 150 | CommandItem, 151 | CommandShortcut, 152 | CommandSeparator, 153 | }; 154 | -------------------------------------------------------------------------------- /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 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ); 68 | DialogHeader.displayName = "DialogHeader"; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ); 82 | DialogFooter.displayName = "DialogFooter"; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogClose, 114 | DialogContent, 115 | DialogDescription, 116 | DialogFooter, 117 | DialogHeader, 118 | DialogOverlay, 119 | DialogPortal, 120 | DialogTitle, 121 | DialogTrigger, 122 | }; 123 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean; 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )); 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName; 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )); 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName; 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )); 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean; 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )); 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )); 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName; 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )); 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean; 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )); 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )); 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ); 181 | }; 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuCheckboxItem, 187 | DropdownMenuContent, 188 | DropdownMenuGroup, 189 | DropdownMenuItem, 190 | DropdownMenuLabel, 191 | DropdownMenuPortal, 192 | DropdownMenuRadioGroup, 193 | DropdownMenuRadioItem, 194 | DropdownMenuSeparator, 195 | DropdownMenuShortcut, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuTrigger, 200 | }; 201 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form"; 12 | 13 | import { Label } from "@/components/ui/label"; 14 | import { cn } from "@/lib/utils"; 15 | 16 | const Form = FormProvider; 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath, 21 | > = { 22 | name: TName; 23 | }; 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue, 27 | ); 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath, 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext); 44 | const itemContext = React.useContext(FormItemContext); 45 | const { getFieldState, formState } = useFormContext(); 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState); 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within "); 51 | } 52 | 53 | const { id } = itemContext; 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | }; 63 | }; 64 | 65 | type FormItemContextValue = { 66 | id: string; 67 | }; 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue, 71 | ); 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId(); 78 | 79 | return ( 80 | 81 |
82 | 83 | ); 84 | }); 85 | FormItem.displayName = "FormItem"; 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField(); 92 | 93 | return ( 94 |