├── src ├── vite-env.d.ts ├── lib │ └── utils.ts ├── main.tsx ├── App.tsx ├── components │ ├── ui │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── button.tsx │ │ ├── form.tsx │ │ ├── monthpicker.tsx │ │ ├── dropdown-menu.tsx │ │ └── monthrangepicker.tsx │ ├── mode-toggle.tsx │ └── theme-provider.tsx ├── index.css ├── Example.tsx └── FormExample.tsx ├── postcss.config.js ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── components.json ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── public ├── vite.svg └── r │ ├── monthpicker.json │ └── monthrangepicker.json ├── registry.json ├── package.json ├── tailwind.config.js └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import react from "@vitejs/plugin-react"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | "@": path.resolve(__dirname, "./src"), 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Example from "./Example"; 2 | import { FormExample } from "./FormExample"; 3 | import { ThemeProvider } from "@/components/theme-provider"; 4 | 5 | function App() { 6 | return ( 7 | 8 |
9 |

Components

10 | 11 |

Form Example

12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )) 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 | 29 | export { Popover, PopoverTrigger, PopoverContent } 30 | -------------------------------------------------------------------------------- /src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; 5 | import { useTheme } from "@/components/theme-provider"; 6 | 7 | export function ModeToggle() { 8 | const { setTheme } = useTheme(); 9 | 10 | return ( 11 | 12 | 13 | 18 | 19 | 20 | setTheme("light")}>Light 21 | setTheme("dark")}>Dark 22 | setTheme("system")}>System 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry.json", 3 | "name": "greenkdev", 4 | "homepage": "https://greenk.dev", 5 | "items": [ 6 | { 7 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 8 | "name": "monthpicker", 9 | "type": "registry:ui", 10 | "title": "Month Picker", 11 | "description": "A month picker component.", 12 | "author": "@greenkdev", 13 | "files": [ 14 | { 15 | "path": "src/components/ui/monthpicker.tsx", 16 | "type": "registry:ui" 17 | } 18 | ], 19 | "registryDependencies": ["button"], 20 | 21 | "dependencies": ["lucide-react"] 22 | }, 23 | { 24 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 25 | "name": "monthrangepicker", 26 | "type": "registry:ui", 27 | "title": "Month Range Picker", 28 | "description": "A month range picker component.", 29 | "author": "@greenkdev", 30 | "files": [ 31 | { 32 | "path": "src/components/ui/monthrangepicker.tsx", 33 | "type": "registry:ui" 34 | } 35 | ], 36 | "registryDependencies": ["button"], 37 | 38 | "dependencies": ["lucide-react"] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daterangepicker", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "registry:build": "shadcn build" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^3.9.0", 15 | "@radix-ui/react-dropdown-menu": "^2.1.1", 16 | "@radix-ui/react-label": "^2.1.0", 17 | "@radix-ui/react-popover": "^1.1.1", 18 | "@radix-ui/react-slot": "^1.1.0", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.1.1", 21 | "date-fns": "^3.6.0", 22 | "lucide-react": "^0.435.0", 23 | "react": "^18.3.1", 24 | "react-day-picker": "^8.10.1", 25 | "react-dom": "^18.3.1", 26 | "react-hook-form": "^7.53.0", 27 | "shadcn": "^2.4.0-canary.13", 28 | "tailwind-merge": "^2.5.2", 29 | "tailwindcss-animate": "^1.0.7", 30 | "zod": "^3.23.8" 31 | }, 32 | "devDependencies": { 33 | "@eslint/js": "^9.9.0", 34 | "@types/node": "^22.5.0", 35 | "@types/react": "^18.3.3", 36 | "@types/react-dom": "^18.3.0", 37 | "@vitejs/plugin-react": "^4.3.1", 38 | "autoprefixer": "^10.4.20", 39 | "eslint": "^9.9.0", 40 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 41 | "eslint-plugin-react-refresh": "^0.4.9", 42 | "globals": "^15.9.0", 43 | "postcss": "^8.4.41", 44 | "tailwindcss": "^3.4.10", 45 | "typescript": "^5.5.3", 46 | "typescript-eslint": "^8.0.1", 47 | "vite": "^5.4.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | type Theme = "dark" | "light" | "system"; 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode; 7 | defaultTheme?: Theme; 8 | storageKey?: string; 9 | }; 10 | 11 | type ThemeProviderState = { 12 | theme: Theme; 13 | setTheme: (theme: Theme) => void; 14 | }; 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: "system", 18 | setTheme: () => null, 19 | }; 20 | 21 | const ThemeProviderContext = createContext(initialState); 22 | 23 | export function ThemeProvider({ children, defaultTheme = "system", storageKey = "vite-ui-theme", ...props }: ThemeProviderProps) { 24 | const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme); 25 | 26 | useEffect(() => { 27 | const root = window.document.documentElement; 28 | 29 | root.classList.remove("light", "dark"); 30 | 31 | if (theme === "system") { 32 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; 33 | 34 | root.classList.add(systemTheme); 35 | return; 36 | } 37 | 38 | root.classList.add(theme); 39 | }, [theme]); 40 | 41 | const value = { 42 | theme, 43 | setTheme: (theme: Theme) => { 44 | localStorage.setItem(storageKey, theme); 45 | setTheme(theme); 46 | }, 47 | }; 48 | 49 | return ( 50 | 51 | {children} 52 | 53 | ); 54 | } 55 | 56 | export const useTheme = () => { 57 | const context = useContext(ThemeProviderContext); 58 | 59 | if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider"); 60 | 61 | return context; 62 | }; 63 | -------------------------------------------------------------------------------- /src/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 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", 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-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 222.2 47.4% 11.2%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 222.2 84% 4.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 210 40% 98%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 212.7 26.8% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", "./src/components/**/*.{ts,tsx}"], 5 | prefix: "", 6 | theme: { 7 | container: { 8 | center: true, 9 | padding: "2rem", 10 | screens: { 11 | "2xl": "1400px", 12 | }, 13 | }, 14 | extend: { 15 | colors: { 16 | border: "hsl(var(--border))", 17 | input: "hsl(var(--input))", 18 | ring: "hsl(var(--ring))", 19 | background: "hsl(var(--background))", 20 | foreground: "hsl(var(--foreground))", 21 | primary: { 22 | DEFAULT: "hsl(var(--primary))", 23 | foreground: "hsl(var(--primary-foreground))", 24 | }, 25 | secondary: { 26 | DEFAULT: "hsl(var(--secondary))", 27 | foreground: "hsl(var(--secondary-foreground))", 28 | }, 29 | destructive: { 30 | DEFAULT: "hsl(var(--destructive))", 31 | foreground: "hsl(var(--destructive-foreground))", 32 | }, 33 | muted: { 34 | DEFAULT: "hsl(var(--muted))", 35 | foreground: "hsl(var(--muted-foreground))", 36 | }, 37 | accent: { 38 | DEFAULT: "hsl(var(--accent))", 39 | foreground: "hsl(var(--accent-foreground))", 40 | }, 41 | popover: { 42 | DEFAULT: "hsl(var(--popover))", 43 | foreground: "hsl(var(--popover-foreground))", 44 | }, 45 | card: { 46 | DEFAULT: "hsl(var(--card))", 47 | foreground: "hsl(var(--card-foreground))", 48 | }, 49 | }, 50 | borderRadius: { 51 | lg: "var(--radius)", 52 | md: "calc(var(--radius) - 2px)", 53 | sm: "calc(var(--radius) - 4px)", 54 | }, 55 | keyframes: { 56 | "accordion-down": { 57 | from: { height: "0" }, 58 | to: { height: "var(--radix-accordion-content-height)" }, 59 | }, 60 | "accordion-up": { 61 | from: { height: "var(--radix-accordion-content-height)" }, 62 | to: { height: "0" }, 63 | }, 64 | }, 65 | animation: { 66 | "accordion-down": "accordion-down 0.2s ease-out", 67 | "accordion-up": "accordion-up 0.2s ease-out", 68 | }, 69 | }, 70 | }, 71 | plugins: [require("tailwindcss-animate")], 72 | }; 73 | -------------------------------------------------------------------------------- /src/Example.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Popover, PopoverContent, PopoverTrigger } from "./components/ui/popover"; 3 | import { CalendarIcon } from "lucide-react"; 4 | import { format } from "date-fns/format"; 5 | import { MonthPicker } from "@/components/ui/monthpicker"; 6 | import { cn } from "@/lib/utils"; 7 | import { Button } from "@/components/ui/button"; 8 | import { MonthRangePicker } from "@/components/ui/monthrangepicker"; 9 | import { ModeToggle } from "@/components/mode-toggle"; 10 | 11 | export default function Example() { 12 | const [date, setDate] = React.useState(); 13 | const [dates, setDates] = React.useState<{ start: Date; end: Date }>({ start: new Date(), end: new Date() }); 14 | 15 | const max = new Date(); 16 | max.setFullYear(2027); 17 | 18 | const min = new Date(); 19 | min.setFullYear(2025); 20 | 21 | return ( 22 |
23 | 24 |
25 | 26 | 27 | 31 | 32 | 33 | setDate(newDate)} selectedMonth={date} variant={{ chevrons: "ghost" }}> 34 | 35 | 36 |

selected date: {date?.toDateString()}

37 |
38 | 39 |
40 | 41 | 42 | 46 | 47 | 48 | setDates(newDates)} selectedMonthRange={dates}> 49 | 50 | 51 |

selected date: {`${dates?.start?.toDateString()} - ${dates?.end?.toDateString()}`}

52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/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 { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 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 |