├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── layout.tsx │ ├── api │ │ └── chat │ │ │ └── route.ts │ └── globals.css ├── components │ ├── accessibility │ │ ├── index.tsx │ │ ├── skip-links.tsx │ │ ├── live-region.tsx │ │ └── focus-indicator.tsx │ ├── theme-provider.tsx │ ├── mode-toggle.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── toaster.tsx │ │ ├── sonner.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── use-toast.ts │ │ ├── sheet.tsx │ │ ├── form.tsx │ │ ├── toast.tsx │ │ ├── command.tsx │ │ └── chart.tsx │ ├── color-mode-toggle.tsx │ ├── site-header.tsx │ ├── page-header.tsx │ ├── reading-progress.tsx │ ├── icons.tsx │ ├── copy-code-button.tsx │ ├── table-of-contents.tsx │ ├── code-sheet.tsx │ ├── ai-chat.tsx │ └── visualization-charts.tsx ├── lib │ └── utils.ts ├── contexts │ └── color-mode-context.tsx └── styles │ └── markdown.css ├── next.config.mjs ├── postcss.config.js ├── components.json ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── LICENSE ├── package.json ├── tailwind.config.ts ├── knowledge-base.md └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sersavan/shadcn-multi-select-component/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/accessibility/index.tsx: -------------------------------------------------------------------------------- 1 | export { SkipLinks } from "./skip-links"; 2 | export { FocusIndicator } from "./focus-indicator"; 3 | export { LiveRegion, announce } from "./live-region"; 4 | -------------------------------------------------------------------------------- /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/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"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /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": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { Icons } from "@/components/icons"; 7 | 8 | export function ModeToggle() { 9 | const { setTheme, theme } = useTheme(); 10 | 11 | return ( 12 | 21 | ); 22 | } 23 | 24 | export default ModeToggle; 25 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | }, 29 | "target": "ES2017" 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/components/color-mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Icons } from "@/components/icons"; 5 | import { useColorMode } from "@/contexts/color-mode-context"; 6 | 7 | export function ColorModeToggle() { 8 | const { isGrayMode, toggleGrayMode } = useColorMode(); 9 | 10 | return ( 11 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/contexts/color-mode-context.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { createContext, useContext, useState, ReactNode } from "react"; 4 | 5 | interface ColorModeContextType { 6 | isGrayMode: boolean; 7 | setIsGrayMode: (value: boolean) => void; 8 | toggleGrayMode: () => void; 9 | } 10 | 11 | const ColorModeContext = createContext( 12 | undefined 13 | ); 14 | 15 | export function ColorModeProvider({ children }: { children: ReactNode }) { 16 | const [isGrayMode, setIsGrayMode] = useState(true); 17 | 18 | const toggleGrayMode = () => { 19 | setIsGrayMode(!isGrayMode); 20 | }; 21 | 22 | return ( 23 | 29 | {children} 30 | 31 | ); 32 | } 33 | 34 | export function useColorMode() { 35 | const context = useContext(ColorModeContext); 36 | if (context === undefined) { 37 | throw new Error("useColorMode must be used within a ColorModeProvider"); 38 | } 39 | return context; 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sersavan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/site-header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import ModeToggle from "@/components/mode-toggle"; 4 | import { ColorModeToggle } from "@/components/color-mode-toggle"; 5 | import { buttonVariants } from "@/components/ui/button"; 6 | import { Icons } from "@/components/icons"; 7 | 8 | export async function SiteHeader() { 9 | return ( 10 |
11 |
12 |
13 | 30 |
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/accessibility/skip-links.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface SkipLinksProps { 6 | className?: string; 7 | } 8 | 9 | export function SkipLinks({ className }: SkipLinksProps) { 10 | return ( 11 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/components/accessibility/live-region.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | interface LiveRegionProps { 7 | className?: string; 8 | } 9 | 10 | let announceFunction: 11 | | ((message: string, priority?: "polite" | "assertive") => void) 12 | | null = null; 13 | 14 | export function LiveRegion({ className }: LiveRegionProps) { 15 | const [politeMessage, setPoliteMessage] = useState(""); 16 | const [assertiveMessage, setAssertiveMessage] = useState(""); 17 | 18 | useEffect(() => { 19 | announceFunction = ( 20 | message: string, 21 | priority: "polite" | "assertive" = "polite" 22 | ) => { 23 | if (priority === "assertive") { 24 | setAssertiveMessage(message); 25 | setTimeout(() => setAssertiveMessage(""), 100); 26 | } else { 27 | setPoliteMessage(message); 28 | setTimeout(() => setPoliteMessage(""), 100); 29 | } 30 | }; 31 | 32 | return () => { 33 | announceFunction = null; 34 | }; 35 | }, []); 36 | 37 | return ( 38 |
39 |
40 | {politeMessage} 41 |
42 |
43 | {assertiveMessage} 44 |
45 |
46 | ); 47 | } 48 | 49 | export function announce( 50 | message: string, 51 | priority: "polite" | "assertive" = "polite" 52 | ) { 53 | if (announceFunction) { 54 | announceFunction(message, priority); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/page-header.tsx: -------------------------------------------------------------------------------- 1 | import Balance from "react-wrap-balancer"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function PageHeader({ 6 | className, 7 | children, 8 | ...props 9 | }: React.HTMLAttributes) { 10 | return ( 11 |
17 | {children} 18 |
19 | ); 20 | } 21 | 22 | function PageHeaderHeading({ 23 | className, 24 | ...props 25 | }: React.HTMLAttributes) { 26 | return ( 27 |

34 | ); 35 | } 36 | 37 | function PageHeaderDescription({ 38 | className, 39 | ...props 40 | }: React.HTMLAttributes) { 41 | return ( 42 | 49 | ); 50 | } 51 | 52 | function PageActions({ 53 | className, 54 | ...props 55 | }: React.HTMLAttributes) { 56 | return ( 57 |
64 | ); 65 | } 66 | 67 | export { PageHeader, PageHeaderHeading, PageHeaderDescription, PageActions }; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-select-component", 3 | "version": "0.2.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^5.2.1", 13 | "@radix-ui/react-dialog": "^1.1.14", 14 | "@radix-ui/react-label": "^2.1.7", 15 | "@radix-ui/react-popover": "^1.1.14", 16 | "@radix-ui/react-separator": "^1.1.7", 17 | "@radix-ui/react-slot": "^1.2.3", 18 | "@radix-ui/react-toast": "^1.2.14", 19 | "@vercel/analytics": "^1.5.0", 20 | "@vercel/speed-insights": "^1.2.0", 21 | "class-variance-authority": "^0.7.1", 22 | "clsx": "^2.1.0", 23 | "cmdk": "^1.1.1", 24 | "lucide-react": "^0.536.0", 25 | "next": "15.4.10", 26 | "next-themes": "^0.4.6", 27 | "react": "^19", 28 | "react-dom": "^19", 29 | "react-hook-form": "^7.62.0", 30 | "react-markdown": "^10.1.0", 31 | "react-syntax-highlighter": "^15.6.1", 32 | "react-wrap-balancer": "^1.1.1", 33 | "recharts": "^3.1.2", 34 | "rehype-highlight": "^7.0.2", 35 | "remark-gfm": "^4.0.1", 36 | "sonner": "^2.0.7", 37 | "tailwind-merge": "^3.3.1", 38 | "tailwindcss-animate": "^1.0.7", 39 | "zod": "^4.0.14" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^24.2.0", 43 | "@types/react": "^19.1.9", 44 | "@types/react-dom": "^19.1.7", 45 | "@types/react-syntax-highlighter": "^15.5.13", 46 | "autoprefixer": "^10.4.21", 47 | "eslint": "^9.32.0", 48 | "eslint-config-next": "15.4.5", 49 | "postcss": "^8.5.6", 50 | "tailwindcss": "^3.4.17", 51 | "typescript": "^5.9.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /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/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { SpeedInsights } from "@vercel/speed-insights/next"; 4 | import { Analytics } from "@vercel/analytics/react"; 5 | 6 | import "./globals.css"; 7 | import { Toaster } from "@/components/ui/sonner"; 8 | import { ThemeProvider } from "@/components/theme-provider"; 9 | import { SiteHeader } from "@/components/site-header"; 10 | import { ColorModeProvider } from "@/contexts/color-mode-context"; 11 | import { cn } from "@/lib/utils"; 12 | 13 | const inter = Inter({ subsets: ["latin"] }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Multi-Select Component | shadcn/ui React TypeScript", 17 | description: 18 | "A powerful and flexible multi-select component built with React, TypeScript, Tailwind CSS, and shadcn/ui. Features animations, search, multiple variants, and full accessibility support.", 19 | keywords: [ 20 | "react", 21 | "typescript", 22 | "shadcn", 23 | "tailwind", 24 | "multi-select", 25 | "component", 26 | "ui", 27 | ], 28 | authors: [{ name: "sersavan", url: "https://github.com/sersavan" }], 29 | openGraph: { 30 | title: "Multi-Select Component | shadcn/ui React TypeScript", 31 | description: 32 | "A powerful and flexible multi-select component with animations, search, and multiple variants", 33 | type: "website", 34 | }, 35 | }; 36 | 37 | export default function RootLayout({ 38 | children, 39 | }: Readonly<{ 40 | children: React.ReactNode; 41 | }>) { 42 | return ( 43 | 44 | 49 | 54 | 55 |
56 | 57 | {children} 58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/reading-progress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | interface ReadingProgressProps { 7 | className?: string; 8 | showPercentage?: boolean; 9 | } 10 | 11 | export function ReadingProgress({ 12 | className, 13 | showPercentage = false, 14 | }: ReadingProgressProps) { 15 | const [progress, setProgress] = useState(0); 16 | const [isVisible, setIsVisible] = useState(false); 17 | 18 | useEffect(() => { 19 | const updateProgress = () => { 20 | const scrollTop = window.scrollY; 21 | const docHeight = 22 | document.documentElement.scrollHeight - window.innerHeight; 23 | const scrollPercent = (scrollTop / docHeight) * 100; 24 | 25 | setProgress(Math.min(100, Math.max(0, scrollPercent))); 26 | setIsVisible(scrollTop > 100); 27 | }; 28 | 29 | let ticking = false; 30 | const handleScroll = () => { 31 | if (!ticking) { 32 | requestAnimationFrame(() => { 33 | updateProgress(); 34 | ticking = false; 35 | }); 36 | ticking = true; 37 | } 38 | }; 39 | 40 | window.addEventListener("scroll", handleScroll, { passive: true }); 41 | updateProgress(); 42 | 43 | return () => window.removeEventListener("scroll", handleScroll); 44 | }, []); 45 | 46 | if (!isVisible) return null; 47 | 48 | return ( 49 | <> 50 | {/* Progress bar */} 51 |
56 | {/* Background track */} 57 |
58 | 59 | {/* Progress bar with gradient */} 60 |
64 | 65 | {/* Subtle glow effect */} 66 |
70 |
71 | 72 | {/* Optional percentage indicator */} 73 | {showPercentage && ( 74 |
75 | {Math.round(progress)}% 76 |
77 | )} 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MoonIcon, 3 | SunMedium, 4 | LucideProps, 5 | Cat, 6 | Dog, 7 | Fish, 8 | Rabbit, 9 | Turtle, 10 | Code, 11 | Globe, 12 | Users, 13 | Star, 14 | Heart, 15 | Zap, 16 | Cpu, 17 | Database, 18 | Monitor, 19 | Smartphone, 20 | Wand2, 21 | Calendar, 22 | HardDrive, 23 | TrendingUp, 24 | DollarSign, 25 | Target, 26 | Shield, 27 | Mail, 28 | PieChart, 29 | Activity, 30 | Search, 31 | MessageCircle, 32 | Bot, 33 | Maximize, 34 | Minimize, 35 | X, 36 | EyeOff, 37 | Eye, 38 | Copy, 39 | Check, 40 | Clock, 41 | ChevronUp, 42 | ChevronDown, 43 | } from "lucide-react"; 44 | 45 | export const Icons = { 46 | moonIcon: MoonIcon, 47 | sunIcon: SunMedium, 48 | cat: Cat, 49 | dog: Dog, 50 | fish: Fish, 51 | rabbit: Rabbit, 52 | turtle: Turtle, 53 | code: Code, 54 | globe: Globe, 55 | users: Users, 56 | star: Star, 57 | heart: Heart, 58 | zap: Zap, 59 | cpu: Cpu, 60 | database: Database, 61 | monitor: Monitor, 62 | smartphone: Smartphone, 63 | wand: Wand2, 64 | calendar: Calendar, 65 | harddrive: HardDrive, 66 | trendingUp: TrendingUp, 67 | dollarSign: DollarSign, 68 | target: Target, 69 | shield: Shield, 70 | mail: Mail, 71 | pieChart: PieChart, 72 | activity: Activity, 73 | search: Search, 74 | messageCircle: MessageCircle, 75 | bot: Bot, 76 | maximize: Maximize, 77 | minimize: Minimize, 78 | x: X, 79 | eyeOff: EyeOff, 80 | eye: Eye, 81 | copy: Copy, 82 | check: Check, 83 | clock: Clock, 84 | chevronUp: ChevronUp, 85 | chevronDown: ChevronDown, 86 | gitHub: ({ ...props }: LucideProps) => ( 87 | 100 | ), 101 | }; 102 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{ts,tsx}", 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./src/**/*.{ts,tsx}", 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | sm: "640px", 18 | md: "768px", 19 | lg: "1024px", 20 | xl: "1280px", 21 | "2xl": "1400px", 22 | }, 23 | }, 24 | extend: { 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | muted: { 44 | DEFAULT: "hsl(var(--muted))", 45 | foreground: "hsl(var(--muted-foreground))", 46 | }, 47 | accent: { 48 | DEFAULT: "hsl(var(--accent))", 49 | foreground: "hsl(var(--accent-foreground))", 50 | }, 51 | popover: { 52 | DEFAULT: "hsl(var(--popover))", 53 | foreground: "hsl(var(--popover-foreground))", 54 | }, 55 | card: { 56 | DEFAULT: "hsl(var(--card))", 57 | foreground: "hsl(var(--card-foreground))", 58 | }, 59 | }, 60 | borderRadius: { 61 | lg: "var(--radius)", 62 | md: "calc(var(--radius) - 2px)", 63 | sm: "calc(var(--radius) - 4px)", 64 | }, 65 | keyframes: { 66 | "accordion-down": { 67 | from: { height: "0" }, 68 | to: { height: "var(--radix-accordion-content-height)" }, 69 | }, 70 | "accordion-up": { 71 | from: { height: "var(--radix-accordion-content-height)" }, 72 | to: { height: "0" }, 73 | }, 74 | wiggle: { 75 | "0%, 100%": { transform: "rotate(-3deg)" }, 76 | "50%": { transform: "rotate(3deg)" }, 77 | }, 78 | scaleIn: { 79 | "0%": { transform: "scale(0.95)", opacity: "0" }, 80 | "100%": { transform: "scale(1)", opacity: "1" }, 81 | }, 82 | slideInDown: { 83 | "0%": { transform: "translateY(-10px)", opacity: "0" }, 84 | "100%": { transform: "translateY(0)", opacity: "1" }, 85 | }, 86 | fadeIn: { 87 | "0%": { opacity: "0" }, 88 | "100%": { opacity: "1" }, 89 | }, 90 | flipIn: { 91 | "0%": { transform: "rotateY(-90deg)", opacity: "0" }, 92 | "100%": { transform: "rotateY(0)", opacity: "1" }, 93 | }, 94 | }, 95 | animation: { 96 | "accordion-down": "accordion-down 0.2s ease-out", 97 | "accordion-up": "accordion-up 0.2s ease-out", 98 | wiggle: "wiggle 0.5s ease-in-out infinite", 99 | scaleIn: "scaleIn 0.2s ease-out", 100 | slideInDown: "slideInDown 0.3s ease-out", 101 | fadeIn: "fadeIn 0.2s ease-in", 102 | flipIn: "flipIn 0.6s ease-out", 103 | }, 104 | }, 105 | }, 106 | plugins: [require("tailwindcss-animate")], 107 | } satisfies Config; 108 | 109 | export default config; 110 | -------------------------------------------------------------------------------- /src/components/copy-code-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { toast } from "sonner"; 5 | import { cn } from "@/lib/utils"; 6 | import { Icons } from "@/components/icons"; 7 | import { Button } from "@/components/ui/button"; 8 | 9 | interface CopyCodeButtonProps { 10 | code: string; 11 | className?: string; 12 | variant?: "default" | "ghost" | "outline"; 13 | size?: "sm" | "default" | "lg"; 14 | label?: string; 15 | } 16 | 17 | export function CopyCodeButton({ 18 | code, 19 | className, 20 | variant = "ghost", 21 | size = "sm", 22 | label = "Copy code", 23 | }: CopyCodeButtonProps) { 24 | const [isCopied, setIsCopied] = useState(false); 25 | 26 | const copyToClipboard = async () => { 27 | try { 28 | await navigator.clipboard.writeText(code); 29 | setIsCopied(true); 30 | toast.success("Code copied to clipboard!", { 31 | description: "You can now paste it in your project", 32 | duration: 2000, 33 | }); 34 | setTimeout(() => setIsCopied(false), 2000); 35 | } catch (err) { 36 | toast.error("Failed to copy code", { 37 | description: "Please try selecting and copying manually", 38 | duration: 3000, 39 | }); 40 | } 41 | }; 42 | 43 | return ( 44 | 61 | ); 62 | } 63 | 64 | interface CodeBlockProps { 65 | code: string; 66 | language?: string; 67 | title?: string; 68 | className?: string; 69 | showCopyButton?: boolean; 70 | } 71 | 72 | export function CodeBlock({ 73 | code, 74 | language = "tsx", 75 | title, 76 | className, 77 | showCopyButton = true, 78 | }: CodeBlockProps) { 79 | return ( 80 |
81 | {title && ( 82 |
83 | 84 | {title} 85 | 86 | {showCopyButton && ( 87 | 93 | )} 94 |
95 | )} 96 |
97 |
103 | 					{code}
104 | 				
105 | {!title && showCopyButton && ( 106 |
107 | 113 |
114 | )} 115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /src/components/accessibility/focus-indicator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | interface FocusIndicatorProps { 7 | className?: string; 8 | } 9 | 10 | export function FocusIndicator({ className }: FocusIndicatorProps) { 11 | const [isKeyboardUser, setIsKeyboardUser] = useState(false); 12 | 13 | useEffect(() => { 14 | let isUsingKeyboard = false; 15 | const handleKeyDown = (e: KeyboardEvent) => { 16 | if (e.key === "Tab") { 17 | isUsingKeyboard = true; 18 | setIsKeyboardUser(true); 19 | document.body.classList.add("using-keyboard"); 20 | } 21 | }; 22 | const handleMouseDown = () => { 23 | if (isUsingKeyboard) { 24 | isUsingKeyboard = false; 25 | setIsKeyboardUser(false); 26 | document.body.classList.remove("using-keyboard"); 27 | } 28 | }; 29 | document.addEventListener("keydown", handleKeyDown); 30 | document.addEventListener("mousedown", handleMouseDown); 31 | 32 | const style = document.createElement("style"); 33 | style.textContent = ` 34 | .using-keyboard *:focus { 35 | outline: 2px solid hsl(var(--ring)) !important; 36 | outline-offset: 2px !important; 37 | border-radius: 4px; 38 | } 39 | 40 | .using-keyboard button:focus, 41 | .using-keyboard [role="button"]:focus { 42 | box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--ring)) !important; 43 | } 44 | 45 | .using-keyboard input:focus, 46 | .using-keyboard textarea:focus, 47 | .using-keyboard select:focus { 48 | border-color: hsl(var(--ring)) !important; 49 | box-shadow: 0 0 0 1px hsl(var(--ring)) !important; 50 | } 51 | 52 | .using-keyboard a:focus { 53 | text-decoration: underline; 54 | text-decoration-thickness: 2px; 55 | text-underline-offset: 4px; 56 | } 57 | 58 | /* Enhanced focus for custom components */ 59 | .using-keyboard [data-state="open"]:focus, 60 | .using-keyboard [aria-expanded="true"]:focus { 61 | outline: 2px solid hsl(var(--ring)) !important; 62 | outline-offset: 2px !important; 63 | } 64 | 65 | /* Skip link styling when focused */ 66 | .using-keyboard .sr-only:focus { 67 | position: fixed !important; 68 | top: 1rem !important; 69 | left: 1rem !important; 70 | width: auto !important; 71 | height: auto !important; 72 | padding: 0.5rem 1rem !important; 73 | background: hsl(var(--background)) !important; 74 | color: hsl(var(--foreground)) !important; 75 | border: 2px solid hsl(var(--ring)) !important; 76 | border-radius: 0.375rem !important; 77 | box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1) !important; 78 | z-index: 9999 !important; 79 | clip: unset !important; 80 | clip-path: unset !important; 81 | } 82 | `; 83 | document.head.appendChild(style); 84 | 85 | return () => { 86 | document.removeEventListener("keydown", handleKeyDown); 87 | document.removeEventListener("mousedown", handleMouseDown); 88 | document.body.classList.remove("using-keyboard"); 89 | document.head.removeChild(style); 90 | }; 91 | }, []); 92 | 93 | return ( 94 |
98 | {isKeyboardUser && "Keyboard navigation is active"} 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { readFile } from "fs/promises"; 3 | import { join } from "path"; 4 | 5 | interface Message { 6 | role: "user" | "assistant"; 7 | content: string; 8 | } 9 | 10 | interface ChatRequest { 11 | messages: Message[]; 12 | systemPrompt: string; 13 | } 14 | 15 | async function getKnowledgeBase(): Promise { 16 | try { 17 | const knowledgeBasePath = join(process.cwd(), "knowledge-base.md"); 18 | const readmePath = join(process.cwd(), "README.md"); 19 | 20 | const [knowledgeBase, readme] = await Promise.all([ 21 | readFile(knowledgeBasePath, "utf-8").catch(() => ""), 22 | readFile(readmePath, "utf-8").catch(() => ""), 23 | ]); 24 | 25 | return ` 26 | === KNOWLEDGE BASE === 27 | ${knowledgeBase} 28 | 29 | === README === 30 | ${readme} 31 | `.trim(); 32 | } catch (error) { 33 | console.error("Error loading knowledge base:", error); 34 | return ""; 35 | } 36 | } 37 | 38 | async function callGeminiAPI( 39 | messages: Message[], 40 | systemPrompt: string, 41 | knowledgeBase: string 42 | ): Promise { 43 | const apiKey = process.env.GEMINI_API_KEY; 44 | 45 | if (!apiKey) { 46 | throw new Error("Gemini API key not found"); 47 | } 48 | 49 | const fullSystemPrompt = `${systemPrompt} 50 | 51 | === AVAILABLE KNOWLEDGE === 52 | ${knowledgeBase} 53 | 54 | CRITICAL INSTRUCTIONS: 55 | - ONLY answer questions about the MultiSelect component 56 | - If the question is not related to MultiSelect, respond with: "I'm specialized in helping with the MultiSelect component only. Please ask me questions about MultiSelect usage, props, styling, integration, or troubleshooting." 57 | - Use this knowledge to provide accurate, helpful information EXCLUSIVELY about the MultiSelect component 58 | - Do NOT provide general programming help or advice about other topics`; 59 | 60 | const geminiMessages = [ 61 | { 62 | role: "user", 63 | parts: [{ text: fullSystemPrompt }], 64 | }, 65 | ...messages.map((msg) => ({ 66 | role: msg.role === "assistant" ? "model" : "user", 67 | parts: [{ text: msg.content }], 68 | })), 69 | ]; 70 | 71 | const response = await fetch( 72 | `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=${apiKey}`, 73 | { 74 | method: "POST", 75 | headers: { 76 | "Content-Type": "application/json", 77 | }, 78 | body: JSON.stringify({ 79 | contents: geminiMessages, 80 | generationConfig: { 81 | temperature: 0.7, 82 | topK: 40, 83 | topP: 0.95, 84 | maxOutputTokens: 1024, 85 | }, 86 | }), 87 | } 88 | ); 89 | 90 | if (!response.ok) { 91 | const error = await response.text(); 92 | console.error("Gemini API error:", error); 93 | throw new Error(`Gemini API error: ${response.status}`); 94 | } 95 | 96 | const data = await response.json(); 97 | 98 | if (!data.candidates || data.candidates.length === 0) { 99 | throw new Error("No response from Gemini API"); 100 | } 101 | 102 | return data.candidates[0].content.parts[0].text; 103 | } 104 | 105 | export async function POST(request: NextRequest) { 106 | try { 107 | const body: ChatRequest = await request.json(); 108 | const { messages, systemPrompt } = body; 109 | 110 | if (!messages || !Array.isArray(messages)) { 111 | return NextResponse.json( 112 | { error: "Invalid messages format" }, 113 | { status: 400 } 114 | ); 115 | } 116 | 117 | const knowledgeBase = await getKnowledgeBase(); 118 | 119 | const response = await callGeminiAPI(messages, systemPrompt, knowledgeBase); 120 | 121 | return NextResponse.json({ content: response }); 122 | } catch (error) { 123 | console.error("Chat API error:", error); 124 | 125 | const errorMessage = 126 | error instanceof Error ? error.message : "Unknown error"; 127 | 128 | return NextResponse.json( 129 | { 130 | error: "Failed to process chat request", 131 | details: errorMessage, 132 | }, 133 | { status: 500 } 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .chart-tooltip { 6 | z-index: 999999 !important; 7 | pointer-events: none !important; 8 | } 9 | 10 | .recharts-tooltip-wrapper { 11 | z-index: 999999 !important; 12 | } 13 | 14 | .recharts-legend-wrapper { 15 | z-index: 1 !important; 16 | } 17 | 18 | .recharts-wrapper { 19 | overflow: visible !important; 20 | } 21 | 22 | @layer base { 23 | :root { 24 | --background: 0 0% 100%; 25 | --foreground: 222.2 84% 4.9%; 26 | 27 | --card: 0 0% 100%; 28 | --card-foreground: 222.2 84% 4.9%; 29 | 30 | --popover: 0 0% 100%; 31 | --popover-foreground: 222.2 84% 4.9%; 32 | 33 | --primary: 222.2 47.4% 11.2%; 34 | --primary-foreground: 210 40% 98%; 35 | 36 | --secondary: 210 40% 96.1%; 37 | --secondary-foreground: 222.2 47.4% 11.2%; 38 | 39 | --muted: 210 40% 96.1%; 40 | --muted-foreground: 215.4 16.3% 46.9%; 41 | 42 | --accent: 210 40% 96.1%; 43 | --accent-foreground: 222.2 47.4% 11.2%; 44 | 45 | --destructive: 0 84.2% 60.2%; 46 | --destructive-foreground: 210 40% 98%; 47 | 48 | --border: 214.3 31.8% 91.4%; 49 | --input: 214.3 31.8% 91.4%; 50 | --ring: 222.2 84% 4.9%; 51 | 52 | --radius: 0.5rem; 53 | } 54 | 55 | .dark { 56 | --background: 222.2 84% 4.9%; 57 | --foreground: 210 40% 98%; 58 | 59 | --card: 222.2 84% 4.9%; 60 | --card-foreground: 210 40% 98%; 61 | 62 | --popover: 222.2 84% 4.9%; 63 | --popover-foreground: 210 40% 98%; 64 | 65 | --primary: 210 40% 98%; 66 | --primary-foreground: 222.2 47.4% 11.2%; 67 | 68 | --secondary: 217.2 32.6% 17.5%; 69 | --secondary-foreground: 210 40% 98%; 70 | 71 | --muted: 217.2 32.6% 17.5%; 72 | --muted-foreground: 215 20.2% 65.1%; 73 | 74 | --accent: 217.2 32.6% 17.5%; 75 | --accent-foreground: 210 40% 98%; 76 | 77 | --destructive: 0 62.8% 30.6%; 78 | --destructive-foreground: 210 40% 98%; 79 | 80 | --border: 217.2 32.6% 17.5%; 81 | --input: 217.2 32.6% 17.5%; 82 | --ring: 212.7 26.8% 83.9%; 83 | } 84 | } 85 | 86 | @layer base { 87 | * { 88 | @apply border-border; 89 | } 90 | body { 91 | @apply bg-background text-foreground; 92 | } 93 | } 94 | 95 | /* Thin scrollbar for Single Line Mode in MultiSelect */ 96 | .multiselect-singleline-scroll { 97 | scrollbar-width: thin; 98 | scrollbar-color: hsl(var(--border) / 0.4) transparent; 99 | } 100 | 101 | .multiselect-singleline-scroll::-webkit-scrollbar { 102 | height: 4px; 103 | } 104 | 105 | .multiselect-singleline-scroll::-webkit-scrollbar-track { 106 | background: transparent; 107 | } 108 | 109 | .multiselect-singleline-scroll::-webkit-scrollbar-thumb { 110 | background-color: hsl(var(--border) / 0.3); 111 | border-radius: 2px; 112 | transition: background-color 0.2s ease, opacity 0.2s ease; 113 | opacity: 0.7; 114 | } 115 | 116 | .multiselect-singleline-scroll::-webkit-scrollbar-thumb:hover { 117 | background-color: hsl(var(--border) / 0.6); 118 | opacity: 1; 119 | } 120 | 121 | /* Scrollbar becomes more visible when actively scrolling */ 122 | .multiselect-singleline-scroll:active::-webkit-scrollbar-thumb { 123 | background-color: hsl(var(--border) / 0.7); 124 | opacity: 1; 125 | } 126 | 127 | /* Dark theme adjustments - make scrollbar slightly more visible */ 128 | .dark .multiselect-singleline-scroll { 129 | scrollbar-color: hsl(var(--border) / 0.5) transparent; 130 | } 131 | 132 | .dark .multiselect-singleline-scroll::-webkit-scrollbar-thumb { 133 | background-color: hsl(var(--border) / 0.5); 134 | } 135 | 136 | .dark .multiselect-singleline-scroll::-webkit-scrollbar-thumb:hover { 137 | background-color: hsl(var(--border) / 0.8); 138 | } 139 | 140 | /* Table of Contents scrollbar */ 141 | .table-of-contents-scroll { 142 | scrollbar-width: thin; 143 | scrollbar-color: hsl(var(--border) / 0.3) transparent; 144 | } 145 | 146 | .table-of-contents-scroll::-webkit-scrollbar { 147 | width: 4px; 148 | } 149 | 150 | .table-of-contents-scroll::-webkit-scrollbar-track { 151 | background: transparent; 152 | } 153 | 154 | .table-of-contents-scroll::-webkit-scrollbar-thumb { 155 | background-color: hsl(var(--border) / 0.3); 156 | border-radius: 2px; 157 | transition: background-color 0.2s ease; 158 | } 159 | 160 | .table-of-contents-scroll::-webkit-scrollbar-thumb:hover { 161 | background-color: hsl(var(--border) / 0.6); 162 | } 163 | -------------------------------------------------------------------------------- /src/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 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /src/styles/markdown.css: -------------------------------------------------------------------------------- 1 | .prose { 2 | color: inherit; 3 | } 4 | 5 | .prose code { 6 | color: inherit; 7 | background-color: rgba(156, 163, 175, 0.2); 8 | padding: 0.125rem 0.25rem; 9 | border-radius: 0.25rem; 10 | font-size: 0.875em; 11 | } 12 | 13 | .prose pre { 14 | background-color: #1f2937 !important; 15 | color: #f9fafb !important; 16 | padding: 1rem !important; 17 | border-radius: 0.5rem !important; 18 | overflow-x: auto; 19 | margin: 0.5rem 0; 20 | } 21 | 22 | .prose pre code { 23 | background-color: transparent !important; 24 | padding: 0 !important; 25 | color: inherit !important; 26 | } 27 | 28 | .prose p { 29 | margin-bottom: 0.5rem; 30 | } 31 | 32 | .prose p:last-child { 33 | margin-bottom: 0; 34 | } 35 | 36 | .prose ul, 37 | .prose ol { 38 | margin: 0.5rem 0; 39 | padding-left: 1rem; 40 | } 41 | 42 | .prose li { 43 | margin-bottom: 0.25rem; 44 | } 45 | 46 | .prose h1, 47 | .prose h2, 48 | .prose h3 { 49 | margin-top: 1rem; 50 | margin-bottom: 0.5rem; 51 | font-weight: 600; 52 | } 53 | 54 | .prose h1:first-child, 55 | .prose h2:first-child, 56 | .prose h3:first-child { 57 | margin-top: 0; 58 | } 59 | 60 | .prose blockquote { 61 | border-left: 4px solid rgba(156, 163, 175, 0.5); 62 | padding-left: 0.75rem; 63 | font-style: italic; 64 | margin: 0.5rem 0; 65 | opacity: 0.8; 66 | } 67 | 68 | .dark .prose code { 69 | background-color: rgba(75, 85, 99, 0.3); 70 | } 71 | 72 | .dark .prose blockquote { 73 | border-left-color: rgba(156, 163, 175, 0.3); 74 | } 75 | 76 | .hljs { 77 | background: #1f2937 !important; 78 | color: #f9fafb !important; 79 | } 80 | 81 | .hljs-keyword { 82 | color: #c792ea !important; 83 | } 84 | 85 | .hljs-string { 86 | color: #c3e88d !important; 87 | } 88 | 89 | .hljs-number { 90 | color: #f78c6c !important; 91 | } 92 | 93 | .hljs-comment { 94 | color: #676e95 !important; 95 | } 96 | 97 | .hljs-function { 98 | color: #82aaff !important; 99 | } 100 | 101 | .hljs-variable { 102 | color: #ffcb6b !important; 103 | } 104 | 105 | .chat-scrollbar { 106 | scrollbar-width: thin; 107 | scrollbar-color: rgba(156, 163, 175, 0.3) transparent; 108 | } 109 | 110 | .chat-scrollbar::-webkit-scrollbar { 111 | width: 6px; 112 | } 113 | 114 | .chat-scrollbar::-webkit-scrollbar-track { 115 | background: transparent; 116 | } 117 | 118 | .chat-scrollbar::-webkit-scrollbar-thumb { 119 | background-color: rgba(156, 163, 175, 0.3); 120 | border-radius: 3px; 121 | transition: background-color 0.2s ease; 122 | } 123 | 124 | .chat-scrollbar::-webkit-scrollbar-thumb:hover { 125 | background-color: rgba(156, 163, 175, 0.5); 126 | } 127 | 128 | .chat-scrollbar-fullscreen { 129 | scrollbar-width: thin; 130 | scrollbar-color: rgba(156, 163, 175, 0.2) transparent; 131 | } 132 | 133 | .chat-scrollbar-fullscreen::-webkit-scrollbar { 134 | width: 4px; 135 | } 136 | 137 | .chat-scrollbar-fullscreen::-webkit-scrollbar-track { 138 | background: transparent; 139 | } 140 | 141 | .chat-scrollbar-fullscreen::-webkit-scrollbar-thumb { 142 | background-color: rgba(156, 163, 175, 0.2); 143 | border-radius: 2px; 144 | transition: background-color 0.2s ease; 145 | } 146 | 147 | .chat-scrollbar-fullscreen::-webkit-scrollbar-thumb:hover { 148 | background-color: rgba(156, 163, 175, 0.4); 149 | } 150 | 151 | .chat-scrollbar-hidden { 152 | scrollbar-width: none; 153 | -ms-overflow-style: none; 154 | } 155 | 156 | .chat-scrollbar-hidden::-webkit-scrollbar { 157 | display: none; 158 | } 159 | 160 | .copy-button { 161 | opacity: 0; 162 | transition: opacity 0.2s ease; 163 | } 164 | 165 | .group:hover .copy-button { 166 | opacity: 1; 167 | } 168 | 169 | .code-block-container { 170 | position: relative; 171 | } 172 | 173 | .code-block-container pre { 174 | padding-right: 3rem; 175 | } 176 | 177 | .relative.group.inline-block { 178 | position: relative; 179 | display: inline-block; 180 | } 181 | 182 | .relative.group.inline-block code { 183 | padding-right: 1.5rem; 184 | } 185 | 186 | .relative.group.inline-block button { 187 | position: absolute; 188 | right: 2px; 189 | top: 50%; 190 | transform: translateY(-50%); 191 | padding: 1px 2px; 192 | background: rgba(75, 85, 101, 0.9); 193 | border: none; 194 | border-radius: 2px; 195 | cursor: pointer; 196 | opacity: 0; 197 | transition: opacity 0.2s ease, background-color 0.2s ease; 198 | z-index: 10; 199 | } 200 | 201 | .relative.group.inline-block:hover button { 202 | opacity: 1; 203 | } 204 | 205 | .relative.group.inline-block button:hover { 206 | background: rgba(55, 65, 81, 1); 207 | } 208 | 209 | .dark .relative.group.inline-block button { 210 | background: rgba(156, 163, 175, 0.9); 211 | } 212 | 213 | .dark .relative.group.inline-block button:hover { 214 | background: rgba(209, 213, 219, 1); 215 | } 216 | -------------------------------------------------------------------------------- /src/components/table-of-contents.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { cn } from "@/lib/utils"; 5 | import { Icons } from "@/components/icons"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Card } from "@/components/ui/card"; 8 | 9 | interface Section { 10 | id: string; 11 | title: string; 12 | icon: React.ComponentType<{ className?: string }>; 13 | } 14 | 15 | const sections: Section[] = [ 16 | { 17 | id: "form-integration", 18 | title: "Form Integration", 19 | icon: Icons.wand, 20 | }, 21 | { 22 | id: "variants-section", 23 | title: "Component Variants", 24 | icon: Icons.star, 25 | }, 26 | { 27 | id: "animations-section", 28 | title: "Animations & Effects", 29 | icon: Icons.activity, 30 | }, 31 | { 32 | id: "responsive-behavior", 33 | title: "Responsive Behavior", 34 | icon: Icons.smartphone, 35 | }, 36 | { 37 | id: "grouped-options", 38 | title: "Grouped Options", 39 | icon: Icons.users, 40 | }, 41 | { 42 | id: "search-ui-configuration", 43 | title: "Search & UI Config", 44 | icon: Icons.search, 45 | }, 46 | { 47 | id: "layout-sizing-options", 48 | title: "Layout & Sizing", 49 | icon: Icons.monitor, 50 | }, 51 | { 52 | id: "custom-styling-colors", 53 | title: "Custom Styling", 54 | icon: Icons.wand, 55 | }, 56 | { 57 | id: "disabled-states", 58 | title: "Disabled States", 59 | icon: Icons.shield, 60 | }, 61 | { 62 | id: "advanced-features", 63 | title: "Advanced Features", 64 | icon: Icons.cpu, 65 | }, 66 | { 67 | id: "imperative-methods", 68 | title: "Imperative Methods", 69 | icon: Icons.target, 70 | }, 71 | { 72 | id: "data-visualization", 73 | title: "Charts Visualization", 74 | icon: Icons.pieChart, 75 | }, 76 | { 77 | id: "ai-configuration", 78 | title: "AI Integration", 79 | icon: Icons.bot, 80 | }, 81 | { 82 | id: "interactive-documentation", 83 | title: "Built in Content", 84 | icon: Icons.monitor, 85 | }, 86 | { 87 | id: "interactive-form-survey", 88 | title: "Interactive Survey", 89 | icon: Icons.heart, 90 | }, 91 | { 92 | id: "props-reference", 93 | title: "Props Reference", 94 | icon: Icons.code, 95 | }, 96 | ]; 97 | 98 | export function TableOfContents() { 99 | const [activeSection, setActiveSection] = useState(""); 100 | const [isVisible, setIsVisible] = useState(false); 101 | 102 | useEffect(() => { 103 | const handleScroll = () => { 104 | const scrollY = window.scrollY; 105 | setIsVisible(scrollY > 400); 106 | const sectionElements = sections.map((section) => ({ 107 | id: section.id, 108 | element: document.getElementById(section.id), 109 | })); 110 | for (let i = sectionElements.length - 1; i >= 0; i--) { 111 | const section = sectionElements[i]; 112 | if (section.element) { 113 | const rect = section.element.getBoundingClientRect(); 114 | if (rect.top <= 100) { 115 | setActiveSection(section.id); 116 | break; 117 | } 118 | } 119 | } 120 | }; 121 | window.addEventListener("scroll", handleScroll); 122 | handleScroll(); 123 | return () => window.removeEventListener("scroll", handleScroll); 124 | }, []); 125 | 126 | const scrollToSection = (sectionId: string) => { 127 | const element = document.getElementById(sectionId); 128 | if (element) { 129 | element.scrollIntoView({ behavior: "smooth", block: "start" }); 130 | } 131 | }; 132 | 133 | if (!isVisible) return null; 134 | 135 | return ( 136 |
137 | 138 |
139 |
140 | Quick Navigation ({sections.length}) 141 |
142 | {sections.map((section) => { 143 | const isActive = activeSection === section.id; 144 | const IconComponent = section.icon; 145 | return ( 146 | 160 | ); 161 | })} 162 |
163 |
164 |
165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /src/components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react" 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast" 10 | 11 | const TOAST_LIMIT = 1 12 | const TOAST_REMOVE_DELAY = 1000000 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string 16 | title?: React.ReactNode 17 | description?: React.ReactNode 18 | action?: ToastActionElement 19 | } 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const 27 | 28 | let count = 0 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER 32 | return count.toString() 33 | } 34 | 35 | type ActionType = typeof actionTypes 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"] 40 | toast: ToasterToast 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"] 44 | toast: Partial 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"] 48 | toastId?: ToasterToast["id"] 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"] 52 | toastId?: ToasterToast["id"] 53 | } 54 | 55 | interface State { 56 | toasts: ToasterToast[] 57 | } 58 | 59 | const toastTimeouts = new Map>() 60 | 61 | const addToRemoveQueue = (toastId: string) => { 62 | if (toastTimeouts.has(toastId)) { 63 | return 64 | } 65 | 66 | const timeout = setTimeout(() => { 67 | toastTimeouts.delete(toastId) 68 | dispatch({ 69 | type: "REMOVE_TOAST", 70 | toastId: toastId, 71 | }) 72 | }, TOAST_REMOVE_DELAY) 73 | 74 | toastTimeouts.set(toastId, timeout) 75 | } 76 | 77 | export const reducer = (state: State, action: Action): State => { 78 | switch (action.type) { 79 | case "ADD_TOAST": 80 | return { 81 | ...state, 82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 83 | } 84 | 85 | case "UPDATE_TOAST": 86 | return { 87 | ...state, 88 | toasts: state.toasts.map((t) => 89 | t.id === action.toast.id ? { ...t, ...action.toast } : t 90 | ), 91 | } 92 | 93 | case "DISMISS_TOAST": { 94 | const { toastId } = action 95 | 96 | // ! Side effects ! - This could be extracted into a dismissToast() action, 97 | // but I'll keep it here for simplicity 98 | if (toastId) { 99 | addToRemoveQueue(toastId) 100 | } else { 101 | state.toasts.forEach((toast) => { 102 | addToRemoveQueue(toast.id) 103 | }) 104 | } 105 | 106 | return { 107 | ...state, 108 | toasts: state.toasts.map((t) => 109 | t.id === toastId || toastId === undefined 110 | ? { 111 | ...t, 112 | open: false, 113 | } 114 | : t 115 | ), 116 | } 117 | } 118 | case "REMOVE_TOAST": 119 | if (action.toastId === undefined) { 120 | return { 121 | ...state, 122 | toasts: [], 123 | } 124 | } 125 | return { 126 | ...state, 127 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 128 | } 129 | } 130 | } 131 | 132 | const listeners: Array<(state: State) => void> = [] 133 | 134 | let memoryState: State = { toasts: [] } 135 | 136 | function dispatch(action: Action) { 137 | memoryState = reducer(memoryState, action) 138 | listeners.forEach((listener) => { 139 | listener(memoryState) 140 | }) 141 | } 142 | 143 | type Toast = Omit 144 | 145 | function toast({ ...props }: Toast) { 146 | const id = genId() 147 | 148 | const update = (props: ToasterToast) => 149 | dispatch({ 150 | type: "UPDATE_TOAST", 151 | toast: { ...props, id }, 152 | }) 153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 154 | 155 | dispatch({ 156 | type: "ADD_TOAST", 157 | toast: { 158 | ...props, 159 | id, 160 | open: true, 161 | onOpenChange: (open) => { 162 | if (!open) dismiss() 163 | }, 164 | }, 165 | }) 166 | 167 | return { 168 | id: id, 169 | dismiss, 170 | update, 171 | } 172 | } 173 | 174 | function useToast() { 175 | const [state, setState] = React.useState(memoryState) 176 | 177 | React.useEffect(() => { 178 | listeners.push(setState) 179 | return () => { 180 | const index = listeners.indexOf(setState) 181 | if (index > -1) { 182 | listeners.splice(index, 1) 183 | } 184 | } 185 | }, [state]) 186 | 187 | return { 188 | ...state, 189 | toast, 190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 191 | } 192 | } 193 | 194 | export { useToast, toast } 195 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | } 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef< 57 | React.ElementRef, 58 | SheetContentProps 59 | >(({ side = "right", className, children, ...props }, ref) => ( 60 | 61 | 62 | 67 | {children} 68 | 69 | 70 | Close 71 | 72 | 73 | 74 | )) 75 | SheetContent.displayName = SheetPrimitive.Content.displayName 76 | 77 | const SheetHeader = ({ 78 | className, 79 | ...props 80 | }: React.HTMLAttributes) => ( 81 |
88 | ) 89 | SheetHeader.displayName = "SheetHeader" 90 | 91 | const SheetFooter = ({ 92 | className, 93 | ...props 94 | }: React.HTMLAttributes) => ( 95 |
102 | ) 103 | SheetFooter.displayName = "SheetFooter" 104 | 105 | const SheetTitle = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | SheetTitle.displayName = SheetPrimitive.Title.displayName 116 | 117 | const SheetDescription = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, ...props }, ref) => ( 121 | 126 | )) 127 | SheetDescription.displayName = SheetPrimitive.Description.displayName 128 | 129 | export { 130 | Sheet, 131 | SheetPortal, 132 | SheetOverlay, 133 | SheetTrigger, 134 | SheetClose, 135 | SheetContent, 136 | SheetHeader, 137 | SheetFooter, 138 | SheetTitle, 139 | SheetDescription, 140 | } 141 | -------------------------------------------------------------------------------- /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 |