Claude-skill-registry frontend-component
Generate React components for IntelliFill following patterns (forwardRef, CVA variants, Radix UI, TailwindCSS). Use when creating UI components, forms, or pages.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-component-intellifill-intellifill" ~/.claude/skills/majiayu000-claude-skill-registry-frontend-component && rm -rf "$T"
manifest:
skills/data/frontend-component-intellifill-intellifill/SKILL.mdsource content
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