Skip to content

Commit 83aec03

Browse files
001 multi tenant ecommerce (#70)
2 parents 0f3a3a4 + c72f4b4 commit 83aec03

8 files changed

Lines changed: 898 additions & 39 deletions

File tree

.github/instructions/components.instructions.md

Lines changed: 307 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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
157288
import { Button } from '@/components/ui/button';
158289
import { Input } from '@/components/ui/input';
159290
import { 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

171341
import { useForm } from 'react-hook-form';
172342
import { zodResolver } from '@hookform/resolvers/zod';
173343
import { 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

175357
const 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

181364
type ProductFormData = z.infer<typeof productSchema>;
182365

183366
export 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

Comments
 (0)