@@ -143,59 +143,349 @@ export default function ProductCard({
143143- **Extract smaller components** when component becomes too large
144144- **Extract logic to custom hooks** or utility functions
145145
146- ## shadcn/ui Components
146+ ## shadcn/ui Component Architecture
147+
148+ We use **shadcn/ui** as our primary component library, built on **Radix UI** primitives with **Tailwind CSS** styling.
149+
150+ ### Installation & Setup
151+
152+ - **Initial Setup**: Run `npx shadcn@latest init` to create `components.json` configuration
153+ - **Add Components**: Use `npx shadcn@latest add <component>` to install components
154+ - **Component Location**: CLI adds components to `src/components/ui/` with full source code
155+ - **MCP Server**: Use shadcn MCP server for AI-assisted component discovery and installation
156+ - **Documentation**: https://ui.shadcn.com/docs/components
157+
158+ ### Component Installation Workflow
159+
160+ 1. **Search Components**:
161+ ```powershell
162+ # Interactive search
163+ npx shadcn@latest add
164+
165+ # Or use MCP server with natural language:
166+ # "Show me button components from shadcn/ui"
167+ # "Install the dialog component"
168+ ```
169+
170+ 2 . ** Install Component** :
171+ ``` powershell
172+ npx shadcn@latest add button
173+ npx shadcn@latest add dialog
174+ npx shadcn@latest add form
175+ ```
176+
177+ 3 . ** Import and Use** :
178+ ``` typescript
179+ import { Button } from ' @/components/ui/button' ;
180+ import { Dialog , DialogContent , DialogHeader , DialogTitle } from ' @/components/ui/dialog' ;
181+ ```
182+
183+ 4 . ** Customize** (edit ` src/components/ui/button.tsx ` for project-wide changes):
184+ ``` typescript
185+ // src/components/ui/button.tsx
186+ import { cva } from " class-variance-authority" ;
187+ const buttonVariants = cva (
188+ " inline-flex items-center justify-center rounded-md text-sm font-medium" ,
189+ {
190+ variants: {
191+ variant: {
192+ default: " bg-primary text-primary-foreground hover:bg-primary/90" ,
193+ destructive: " bg-destructive text-destructive-foreground hover:bg-destructive/90" ,
194+ // Add custom variants here
195+ },
196+ },
197+ }
198+ );
199+ ```
200+
201+ ### Composition Patterns
202+
203+ ** Composition over Inheritance** - Build complex components by composing primitives:
147204
148- We use **shadcn/ui** components built on **Radix UI**:
205+ ``` typescript
206+ // ✅ GOOD: Compose primitives at usage site
207+ import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogFooter } from ' @/components/ui/dialog' ;
208+ import { Button } from ' @/components/ui/button' ;
209+ import { Input } from ' @/components/ui/input' ;
210+ import { Label } from ' @/components/ui/label' ;
211+
212+ export function CreateProductDialog({ open , onOpenChange }: Props ) {
213+ return (
214+ < Dialog open = {open } onOpenChange = {onOpenChange }>
215+ <DialogContent >
216+ <DialogHeader >
217+ <DialogTitle >Create Product < / DialogTitle >
218+ < / DialogHeader >
219+ < div className = " space-y-4" >
220+ <div >
221+ < Label htmlFor = " name" > Product Name < / Label >
222+ < Input id = " name" placeholder = " Enter product name" / >
223+ < / div >
224+ < / div >
225+ <DialogFooter >
226+ < Button variant = " outline" onClick = {() => onOpenChange(false )}> Cancel < / Button >
227+ < Button type = " submit" > Create < / Button >
228+ < / DialogFooter >
229+ < / DialogContent >
230+ < / Dialog >
231+ );
232+ }
233+
234+ // ❌ BAD: Don't create wrapper components
235+ function CustomDialog({ children }) {
236+ return <Dialog >{children }< / Dialog > ; // Unnecessary abstraction
237+ }
238+ ```
239+
240+ ### Component Customization
241+
242+ - ** Project-wide changes** : Edit components directly in ` src/components/ui/ `
243+ - ** One-off styling** : Use ` className ` prop
244+ - ** Extend variants** : Use ` class-variance-authority ` (cva) pattern
149245
150- - **Import from `@/components/ui`**: All UI primitives (see `src/components/ui`)
151- - **Customize via Tailwind**: Modify classes in component files
152- - **Follow accessibility**: shadcn/ui components are accessible by default
153- - **Add new components**: Use `npx shadcn-ui@latest add [component-name]`
246+ ``` typescript
247+ // One-off customization
248+ < Button className = " w-full mt-4" > Submit < / Button >
249+
250+ // Using variants
251+ < Button variant = " destructive" size = " sm" > Delete < / Button >
252+
253+ // Conditional classes with cn() utility
254+ import { cn } from ' @/lib/utils' ;
255+
256+ < Button
257+ className = {cn(
258+ " w-full" ,
259+ isPending && " opacity-50 cursor-not-allowed"
260+ )}
261+ >
262+ Submit
263+ < / Button >
264+ ```
265+
266+ ### Accessibility Requirements
267+
268+ - ** WCAG 2.1 Level AA** : Radix UI primitives provide accessibility by default
269+ - ** Keyboard Navigation** : Tab, Enter, Escape work automatically
270+ - ** ARIA Labels** : Add descriptive labels for screen readers
271+ - ** Focus Management** : Visible focus indicators on all elements
272+ - ** Screen Reader Testing** : Test with NVDA, JAWS, VoiceOver quarterly
154273
155- Common components:
156274``` typescript
275+ // ✅ GOOD: Accessible button with proper labels
276+ < Button
277+ aria - label = {` Add ${product .name } to cart ` }
278+ className = " focus:ring-2 focus:ring-primary focus:outline-none"
279+ >
280+ Add to Cart
281+ < / Button >
282+ ```
283+
284+ ### Common shadcn/ui Components
285+
286+ ``` typescript
287+ // Forms & Inputs
157288import { Button } from ' @/components/ui/button' ;
158289import { Input } from ' @/components/ui/input' ;
159290import { Label } from ' @/components/ui/label' ;
160- import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
161- import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
291+ import { Textarea } from ' @/components/ui/textarea' ;
292+ import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from ' @/components/ui/select' ;
293+ import { Checkbox } from ' @/components/ui/checkbox' ;
294+ import { RadioGroup , RadioGroupItem } from ' @/components/ui/radio-group' ;
295+
296+ // Layout & Navigation
297+ import { Card , CardContent , CardHeader , CardTitle , CardDescription , CardFooter } from ' @/components/ui/card' ;
298+ import { Tabs , TabsContent , TabsList , TabsTrigger } from ' @/components/ui/tabs' ;
299+ import { Separator } from ' @/components/ui/separator' ;
300+ import { ScrollArea } from ' @/components/ui/scroll-area' ;
301+
302+ // Overlays & Dialogs
303+ import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogFooter } from ' @/components/ui/dialog' ;
304+ import { AlertDialog , AlertDialogAction , AlertDialogCancel } from ' @/components/ui/alert-dialog' ;
305+ import { Sheet , SheetContent , SheetHeader , SheetTitle } from ' @/components/ui/sheet' ;
306+ import { Popover , PopoverContent , PopoverTrigger } from ' @/components/ui/popover' ;
307+ import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from ' @/components/ui/tooltip' ;
308+
309+ // Feedback & Status
310+ import { Alert , AlertDescription , AlertTitle } from ' @/components/ui/alert' ;
311+ import { Badge } from ' @/components/ui/badge' ;
312+ import { Progress } from ' @/components/ui/progress' ;
313+ import { Skeleton } from ' @/components/ui/skeleton' ;
314+ import { toast } from ' @/components/ui/use-toast' ;
315+
316+ // Display & Data
317+ import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from ' @/components/ui/table' ;
318+ import { Avatar , AvatarFallback , AvatarImage } from ' @/components/ui/avatar' ;
162319```
163320
164321## Forms & Validation
165322
166- ### Use React Hook Form + Zod (see ` .specify/memory/constitution.md ` for validation standards)
323+ ### Use shadcn/ui Form + React Hook Form + Zod
324+
325+ ** REQUIRED** : Use shadcn/ui Form components for all forms. This provides consistent styling, accessibility, and validation.
326+
327+ > ** Note:** For simple forms such as single-field search bars or newsletter subscription inputs that do not require complex validation, it is acceptable to use shadcn/ui Input components directly without the full Form structure.
328+ > Examples include:
329+ > - Search input in a navbar or sidebar
330+ > - Newsletter signup with only an email field and basic validation
331+ > - Quick filter fields with no multi-field dependencies
332+ >
333+ > In these cases, ensure accessibility (label, ARIA attributes) and consistent styling are maintained. For multi-field forms or forms requiring advanced validation, always use the full shadcn/ui Form pattern.
334+ **Pattern**: `<Form> <FormField ><FormItem ><FormLabel ><FormControl ><Input /></FormControl ><FormMessage /></FormItem ></FormField ></Form >`
335+
336+ ** Reference** : https://ui.shadcn.com/docs/components/form
167337
168338``` typescript
169339' use client' ;
170340
171341import { useForm } from ' react-hook-form' ;
172342import { zodResolver } from ' @hookform/resolvers/zod' ;
173343import { z } from ' zod' ;
344+ import { Button } from ' @/components/ui/button' ;
345+ import {
346+ Form ,
347+ FormControl ,
348+ FormDescription ,
349+ FormField ,
350+ FormItem ,
351+ FormLabel ,
352+ FormMessage ,
353+ } from ' @/components/ui/form' ;
354+ import { Input } from ' @/components/ui/input' ;
355+ import { Textarea } from ' @/components/ui/textarea' ;
174356
175357const productSchema = z .object ({
176- name: z .string ().min (1 , ' Name is required' ),
177- price: z .number ().min (0 , ' Price must be positive' ),
178- description: z .string ().optional (),
358+ name: z .string ().min (1 , ' Name is required' ).max (200 , ' Name too long' ),
359+ price: z .number ().positive (' Price must be positive' ),
360+ sku: z .string ().min (1 , ' SKU is required' ),
361+ description: z .string ().max (5000 , ' Description too long' ).optional (),
179362});
180363
181364type ProductFormData = z .infer <typeof productSchema >;
182365
183366export default function ProductForm() {
184- const { register, handleSubmit, formState : { errors } } = useForm <ProductFormData >({
367+ const form = useForm <ProductFormData >({
185368 resolver: zodResolver (productSchema ),
369+ defaultValues: {
370+ name: ' ' ,
371+ price: 0 ,
372+ sku: ' ' ,
373+ description: ' ' ,
374+ },
186375 });
187376
188377 const onSubmit = (data : ProductFormData ) => {
189378 // Handle submission
379+ console .log (data );
190380 };
191381
192382 return (
193- < form onSubmit = {handleSubmit(onSubmit )}>
194- < Input {... register ('name ')} / >
195- {errors .name && <span className = " text-red-500" > {errors.name.message }< / span > }
383+ < Form {... form }>
384+ < form onSubmit = {form.handleSubmit(onSubmit)} className = " space-y-6" >
385+ < FormField
386+ control = {form.control }
387+ name = " name"
388+ render = {({ field }) => (
389+ <FormItem>
390+ <FormLabel>Product Name</FormLabel>
391+ <FormControl>
392+ <Input placeholder = " Enter product name" {... field } / >
393+ </FormControl>
394+ <FormDescription>
395+ This is the public display name for your product.
396+ </FormDescription>
397+ <FormMessage />
398+ </FormItem>
399+ )}
400+ / >
401+
402+ < FormField
403+ control = {form.control }
404+ name = " price"
405+ render = {({ field }) => (
406+ <FormItem>
407+ <FormLabel>Price</FormLabel>
408+ <FormControl>
409+ <Input
410+ type = " number"
411+ step = " 0.01"
412+ placeholder = " 0.00"
413+ {... field }
414+ onChange = {e => field .onChange (parseFloat (e .target .value ))}
415+ />
416+ </FormControl>
417+ <FormMessage />
418+ </FormItem>
419+ )}
420+ / >
421+
422+ < FormField
423+ control = {form.control }
424+ name = " description"
425+ render = {({ field }) => (
426+ <FormItem>
427+ <FormLabel>Description</FormLabel>
428+ <FormControl>
429+ <Textarea
430+ placeholder = " Enter product description"
431+ className = " resize-none"
432+ {... field }
433+ />
434+ </FormControl>
435+ <FormMessage />
436+ </FormItem>
437+ )}
438+ / >
439+
440+ < Button type = " submit" disabled = {form.formState.isSubmitting }>
441+ {form .formState .isSubmitting ? ' Creating...' : ' Create Product' }
442+ < / Button >
443+ < / form >
444+ < / Form >
445+ );
446+ }
447+ ```
448+
449+ ### Form with Server Actions
450+
451+ ``` typescript
452+ ' use client' ;
453+
454+ import { useFormState , useFormStatus } from ' react-dom' ;
455+ import { createProduct } from ' ./actions' ;
456+ import { Button } from ' @/components/ui/button' ;
457+ import { Input } from ' @/components/ui/input' ;
458+ import { Label } from ' @/components/ui/label' ;
459+
460+ export function CreateProductForm() {
461+ const [state, formAction] = useFormState (createProduct , null );
462+
463+ return (
464+ < form action = {formAction } className = " space-y-4" >
465+ <div >
466+ < Label htmlFor = " name" > Product Name < / Label >
467+ < Input id = " name" name = " name" required / >
468+ < / div >
469+
470+ {state ?.error && (
471+ <div className = " text-sm text-destructive" >
472+ {state .error .message }
473+ </div >
474+ )}
475+
476+ < SubmitButton / >
196477 < / form >
197478 );
198479}
480+
481+ function SubmitButton() {
482+ const { pending } = useFormStatus ();
483+ return (
484+ < Button type = " submit" disabled = {pending }>
485+ {pending ? ' Creating...' : ' Create Product' }
486+ < / Button >
487+ );
488+ }
199489```
200490
201491## Data Fetching
0 commit comments