--- name: frontend-component description: Generate React components for IntelliFill following patterns (forwardRef, CVA variants, Radix UI, TailwindCSS). Use when creating UI components, forms, or pages. --- # Frontend Component Development Skill This skill provides comprehensive guidance for creating React components in the IntelliFill frontend (`quikadmin-web/`). ## Table of Contents 1. [Component Architecture](#component-architecture) 2. [UI Component Pattern](#ui-component-pattern) 3. [CVA Variants](#cva-variants) 4. [Form Components](#form-components) 5. [Page Components](#page-components) 6. [Radix UI Integration](#radix-ui-integration) 7. [Styling with TailwindCSS](#styling-with-tailwindcss) 8. [Testing Components](#testing-components) ## Component Architecture IntelliFill follows a clear component organization: ``` quikadmin-web/src/ ├── components/ │ ├── ui/ # Base UI components (shadcn-style) │ │ ├── button.tsx │ │ ├── input.tsx │ │ ├── dialog.tsx │ │ └── ... │ ├── forms/ # Form-specific components │ │ ├── LoginForm.tsx │ │ ├── RegistrationForm.tsx │ │ └── ... │ ├── layout/ # Layout components │ │ ├── Header.tsx │ │ ├── Sidebar.tsx │ │ └── ... │ └── [domain]/ # Feature-specific components │ ├── DocumentCard.tsx │ ├── TemplateList.tsx │ └── ... └── pages/ # Page-level components ├── Dashboard.tsx ├── Documents.tsx └── ... ``` ## UI Component Pattern IntelliFill uses the shadcn/ui pattern for base components. ### Base Component Template ```tsx // quikadmin-web/src/components/ui/button.tsx import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( 'inline-flex items-center justify-center 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', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default', }, } ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; return ( ); } ); Button.displayName = 'Button'; export { Button, buttonVariants }; ``` ### Input Component ```tsx // quikadmin-web/src/components/ui/input.tsx import * as React from 'react'; import { cn } from '@/lib/utils'; export interface InputProps extends React.InputHTMLAttributes { error?: string; label?: string; } const Input = React.forwardRef( ({ className, type, error, label, id, ...props }, ref) => { const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`; return (
{label && ( )} {error && (

{error}

)}
); } ); Input.displayName = 'Input'; export { Input }; ``` ## CVA Variants IntelliFill uses class-variance-authority (CVA) for variant management. ### CVA Pattern ```tsx import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const cardVariants = cva( // Base styles (always applied) 'rounded-lg border bg-card text-card-foreground shadow-sm', { variants: { // Variant definitions variant: { default: 'border-gray-200', elevated: 'border-gray-300 shadow-md', outlined: 'border-2 border-primary', }, size: { sm: 'p-4', md: 'p-6', lg: 'p-8', }, interactive: { true: 'cursor-pointer hover:shadow-lg transition-shadow', false: '', }, }, // Compound variants (combinations) compoundVariants: [ { variant: 'elevated', interactive: true, class: 'hover:shadow-xl', }, ], // Default values defaultVariants: { variant: 'default', size: 'md', interactive: false, }, } ); interface CardProps extends React.HTMLAttributes, VariantProps {} export function Card({ className, variant, size, interactive, ...props }: CardProps) { return (
); } ``` ### Using CVA in Components ```tsx // Document card with variants const documentCardVariants = cva( 'flex flex-col gap-4 rounded-lg border p-4', { variants: { status: { pending: 'border-yellow-500 bg-yellow-50', processing: 'border-blue-500 bg-blue-50', completed: 'border-green-500 bg-green-50', failed: 'border-red-500 bg-red-50', }, selected: { true: 'ring-2 ring-primary ring-offset-2', false: '', }, }, defaultVariants: { status: 'pending', selected: false, }, } ); interface DocumentCardProps extends VariantProps { document: Document; onClick?: () => void; } export function DocumentCard({ document, status, selected, onClick }: DocumentCardProps) { return (

{document.name}

{document.description}

); } ``` ## Form Components IntelliFill forms use controlled components with validation. ### Form Pattern with React Hook Form ```tsx // quikadmin-web/src/components/forms/DocumentUploadForm.tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useDocumentStore } from '@/stores/documentStore'; import { toast } from 'sonner'; const formSchema = z.object({ name: z.string().min(1, 'Name is required').max(255), description: z.string().max(1000).optional(), file: z.instanceof(File).refine((file) => file.size <= 10 * 1024 * 1024, { message: 'File must be less than 10MB', }), }); type FormData = z.infer; export function DocumentUploadForm({ onSuccess }: { onSuccess?: () => void }) { const { register, handleSubmit, formState: { errors, isSubmitting }, reset, } = useForm({ resolver: zodResolver(formSchema), }); const { uploadDocument } = useDocumentStore(); const onSubmit = async (data: FormData) => { try { await uploadDocument({ name: data.name, description: data.description, file: data.file, }); toast.success('Document uploaded successfully'); reset(); onSuccess?.(); } catch (error) { toast.error('Failed to upload document'); console.error(error); } }; return (
{errors.file && (

{errors.file.message}

)}
); } ``` ### Form with Custom Validation ```tsx import { useState } from 'react'; export function LoginForm() { const [formData, setFormData] = useState({ email: '', password: '', }); const [errors, setErrors] = useState>({}); const [isLoading, setIsLoading] = useState(false); const validate = () => { const newErrors: Record = {}; if (!formData.email) { newErrors.email = 'Email is required'; } else if (!/\S+@\S+\.\S+/.test(formData.email)) { newErrors.email = 'Email is invalid'; } if (!formData.password) { newErrors.password = 'Password is required'; } else if (formData.password.length < 8) { newErrors.password = 'Password must be at least 8 characters'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validate()) return; setIsLoading(true); try { // Your login logic await login(formData); } catch (error) { setErrors({ submit: 'Login failed. Please try again.' }); } finally { setIsLoading(false); } }; return (
setFormData({ ...formData, email: e.target.value })} error={errors.email} /> setFormData({ ...formData, password: e.target.value })} error={errors.password} /> {errors.submit &&

{errors.submit}

}
); } ``` ## Page Components Page components are route-level components that compose smaller components. ### Page Template ```tsx // quikadmin-web/src/pages/Documents.tsx import { useEffect } from 'react'; import { useDocumentStore } from '@/stores/documentStore'; import { DocumentCard } from '@/components/documents/DocumentCard'; import { DocumentUploadForm } from '@/components/forms/DocumentUploadForm'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; import { Loader2 } from 'lucide-react'; export function DocumentsPage() { const { documents, loading, error, fetchDocuments } = useDocumentStore(); const [uploadOpen, setUploadOpen] = useState(false); useEffect(() => { fetchDocuments(); }, [fetchDocuments]); if (loading) { return (
); } if (error) { return (

{error}

); } return (
{/* Header */}

Documents

setUploadOpen(false)} />
{/* Document Grid */} {documents.length === 0 ? (

No documents yet. Upload your first one!

) : (
{documents.map((doc) => ( ))}
)}
); } ``` ## Radix UI Integration IntelliFill uses Radix UI primitives for accessible components. ### Dialog Component ```tsx // quikadmin-web/src/components/ui/dialog.tsx import * as React from 'react'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import { X } from 'lucide-react'; import { cn } from '@/lib/utils'; const Dialog = DialogPrimitive.Root; const DialogTrigger = DialogPrimitive.Trigger; const DialogPortal = DialogPrimitive.Portal; const DialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} Close )); DialogContent.displayName = DialogPrimitive.Content.displayName; export { Dialog, DialogTrigger, DialogContent }; ``` ### Dropdown Menu ```tsx import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import { MoreVertical } from 'lucide-react'; export function DocumentActions({ document }) { return ( handleEdit(document)} > Edit handleDownload(document)} > Download handleDelete(document)} > Delete ); } ``` ## Styling with TailwindCSS IntelliFill uses TailwindCSS 4.0 with custom design tokens. ### Design Tokens ```typescript // quikadmin-web/tailwind.config.js export default { theme: { extend: { colors: { border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))', }, destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))', }, muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))', }, accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))', }, }, }, }, }; ``` ### cn() Utility ```typescript // quikadmin-web/src/lib/utils.ts import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; /** * Merge class names with Tailwind CSS conflict resolution */ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` ### Responsive Patterns ```tsx
{/* Mobile: 1 col, Tablet: 2 cols, Desktop: 3 cols */}
{/* Mobile: vertical, Desktop: horizontal */}
``` ## Testing Components ### Vitest Component Test ```tsx // quikadmin-web/src/components/__tests__/DocumentCard.test.tsx import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { DocumentCard } from '../documents/DocumentCard'; describe('DocumentCard', () => { const mockDocument = { id: '1', name: 'Test Document', description: 'Test description', status: 'completed', }; it('renders document information', () => { render(); expect(screen.getByText('Test Document')).toBeInTheDocument(); expect(screen.getByText('Test description')).toBeInTheDocument(); }); it('calls onClick when clicked', () => { const onClick = vi.fn(); render(); fireEvent.click(screen.getByText('Test Document')); expect(onClick).toHaveBeenCalledTimes(1); }); it('applies correct status variant', () => { const { container } = render( ); const card = container.firstChild; expect(card).toHaveClass('border-green-500'); }); }); ``` ## Best Practices 1. **Use forwardRef for UI components** - Enables ref forwarding and composition 2. **Type all props** - Use TypeScript interfaces for all component props 3. **Use CVA for variants** - Consistent variant management 4. **Accessibility first** - Use Radix UI primitives and ARIA attributes 5. **Responsive by default** - Design for mobile first 6. **Use cn() utility** - Merge class names safely 7. **Error boundaries** - Wrap components in error boundaries 8. **Loading states** - Always show loading indicators 9. **Empty states** - Handle empty data gracefully 10. **Test interactivity** - Test user interactions and edge cases ## References - [React Documentation](https://react.dev/) - [Radix UI](https://www.radix-ui.com/) - [TailwindCSS](https://tailwindcss.com/) - [CVA Documentation](https://cva.style/docs) - [React Hook Form](https://react-hook-form.com/) - [Vitest](https://vitest.dev/)