| 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
- Component Architecture
- UI Component Pattern
- CVA Variants
- Form Components
- Page Components
- Radix UI Integration
- Styling with TailwindCSS
- 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
// 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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
Input Component
// quikadmin-web/src/components/ui/input.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
error?: string;
label?: string;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, error, label, id, ...props }, ref) => {
const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
return (
<div className="flex flex-col gap-1">
{label && (
<label
htmlFor={inputId}
className="text-sm font-medium text-gray-700"
>
{label}
</label>
)}
<input
id={inputId}
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-red-500 focus-visible:ring-red-500',
className
)}
ref={ref}
aria-invalid={!!error}
aria-describedby={error ? `${inputId}-error` : undefined}
{...props}
/>
{error && (
<p id={`${inputId}-error`} className="text-sm text-red-500">
{error}
</p>
)}
</div>
);
}
);
Input.displayName = 'Input';
export { Input };
CVA Variants
IntelliFill uses class-variance-authority (CVA) for variant management.
CVA Pattern
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<HTMLDivElement>,
VariantProps<typeof cardVariants> {}
export function Card({ className, variant, size, interactive, ...props }: CardProps) {
return (
<div
className={cn(cardVariants({ variant, size, interactive }), className)}
{...props}
/>
);
}
Using CVA in Components
// 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<typeof documentCardVariants> {
document: Document;
onClick?: () => void;
}
export function DocumentCard({ document, status, selected, onClick }: DocumentCardProps) {
return (
<div
className={cn(documentCardVariants({ status, selected }))}
onClick={onClick}
>
<h3>{document.name}</h3>
<p>{document.description}</p>
</div>
);
}
Form Components
IntelliFill forms use controlled components with validation.
Form Pattern with React Hook Form
// 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<typeof formSchema>;
export function DocumentUploadForm({ onSuccess }: { onSuccess?: () => void }) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<FormData>({
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 (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input
label="Document Name"
{...register('name')}
error={errors.name?.message}
placeholder="Enter document name"
/>
<Input
label="Description"
{...register('description')}
error={errors.description?.message}
placeholder="Optional description"
/>
<div>
<label className="text-sm font-medium text-gray-700">File</label>
<input
type="file"
{...register('file')}
accept=".pdf,.png,.jpg,.jpeg"
className="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-primary-foreground hover:file:bg-primary/90"
/>
{errors.file && (
<p className="mt-1 text-sm text-red-500">{errors.file.message}</p>
)}
</div>
<Button type="submit" disabled={isSubmitting} className="w-full">
{isSubmitting ? 'Uploading...' : 'Upload Document'}
</Button>
</form>
);
}
Form with Custom Validation
import { useState } from 'react';
export function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const validate = () => {
const newErrors: Record<string, string> = {};
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 (
<form onSubmit={handleSubmit} className="space-y-4">
<Input
label="Email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
error={errors.email}
/>
<Input
label="Password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
error={errors.password}
/>
{errors.submit && <p className="text-sm text-red-500">{errors.submit}</p>}
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? 'Signing in...' : 'Sign In'}
</Button>
</form>
);
}
Page Components
Page components are route-level components that compose smaller components.
Page Template
// 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 (
<div className="flex h-screen items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
);
}
if (error) {
return (
<div className="flex h-screen items-center justify-center">
<p className="text-red-500">{error}</p>
</div>
);
}
return (
<div className="container mx-auto py-8">
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<h1 className="text-3xl font-bold">Documents</h1>
<Dialog open={uploadOpen} onOpenChange={setUploadOpen}>
<DialogTrigger asChild>
<Button>Upload Document</Button>
</DialogTrigger>
<DialogContent>
<DocumentUploadForm onSuccess={() => setUploadOpen(false)} />
</DialogContent>
</Dialog>
</div>
{/* Document Grid */}
{documents.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500">No documents yet. Upload your first one!</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{documents.map((doc) => (
<DocumentCard key={doc.id} document={doc} />
))}
</div>
)}
</div>
);
}
Radix UI Integration
IntelliFill uses Radix UI primitives for accessible components.
Dialog Component
// 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<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
export { Dialog, DialogTrigger, DialogContent };
Dropdown Menu
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { MoreVertical } from 'lucide-react';
export function DocumentActions({ document }) {
return (
<DropdownMenuPrimitive.Root>
<DropdownMenuPrimitive.Trigger asChild>
<button className="rounded p-2 hover:bg-gray-100">
<MoreVertical className="h-4 w-4" />
</button>
</DropdownMenuPrimitive.Trigger>
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
className="min-w-[220px] rounded-md border bg-white p-1 shadow-md"
sideOffset={5}
>
<DropdownMenuPrimitive.Item
className="cursor-pointer rounded px-3 py-2 text-sm hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
onSelect={() => handleEdit(document)}
>
Edit
</DropdownMenuPrimitive.Item>
<DropdownMenuPrimitive.Item
className="cursor-pointer rounded px-3 py-2 text-sm hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
onSelect={() => handleDownload(document)}
>
Download
</DropdownMenuPrimitive.Item>
<DropdownMenuPrimitive.Separator className="my-1 h-px bg-gray-200" />
<DropdownMenuPrimitive.Item
className="cursor-pointer rounded px-3 py-2 text-sm text-red-500 hover:bg-red-50 focus:bg-red-50 focus:outline-none"
onSelect={() => handleDelete(document)}
>
Delete
</DropdownMenuPrimitive.Item>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
);
}
Styling with TailwindCSS
IntelliFill uses TailwindCSS 4.0 with custom design tokens.
Design Tokens
// 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
// 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
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Mobile: 1 col, Tablet: 2 cols, Desktop: 3 cols */}
</div>
<div className="flex flex-col lg:flex-row gap-4">
{/* Mobile: vertical, Desktop: horizontal */}
</div>
<button className="w-full sm:w-auto">
{/* Full width on mobile, auto on larger screens */}
</button>
Testing Components
Vitest Component Test
// 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(<DocumentCard document={mockDocument} />);
expect(screen.getByText('Test Document')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const onClick = vi.fn();
render(<DocumentCard document={mockDocument} onClick={onClick} />);
fireEvent.click(screen.getByText('Test Document'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('applies correct status variant', () => {
const { container } = render(
<DocumentCard document={mockDocument} status="completed" />
);
const card = container.firstChild;
expect(card).toHaveClass('border-green-500');
});
});
Best Practices
- Use forwardRef for UI components - Enables ref forwarding and composition
- Type all props - Use TypeScript interfaces for all component props
- Use CVA for variants - Consistent variant management
- Accessibility first - Use Radix UI primitives and ARIA attributes
- Responsive by default - Design for mobile first
- Use cn() utility - Merge class names safely
- Error boundaries - Wrap components in error boundaries
- Loading states - Always show loading indicators
- Empty states - Handle empty data gracefully
- Test interactivity - Test user interactions and edge cases