- Created `openspec-ff-change` skill for fast-forward artifact creation. - Introduced `openspec-new-change` skill for structured change creation. - Developed `openspec-onboard` skill for guided onboarding through OpenSpec workflow. - Added `openspec-sync-specs` skill for syncing delta specs to main specs. - Implemented `openspec-verify-change` skill for verifying implementation against change artifacts. - Updated `.gitignore` to exclude OpenSpec generated files. - Added `skills-lock.json` to manage skill dependencies.
196 lines
4.2 KiB
Markdown
196 lines
4.2 KiB
Markdown
# Component Composition
|
|
|
|
## Contents
|
|
|
|
- Items always inside their Group component
|
|
- Callouts use Alert
|
|
- Empty states use Empty component
|
|
- Toast notifications use sonner
|
|
- Choosing between overlay components
|
|
- Dialog, Sheet, and Drawer always need a Title
|
|
- Card structure
|
|
- Button has no isPending or isLoading prop
|
|
- TabsTrigger must be inside TabsList
|
|
- Avatar always needs AvatarFallback
|
|
- Use Separator instead of raw hr or border divs
|
|
- Use Skeleton for loading placeholders
|
|
- Use Badge instead of custom styled spans
|
|
|
|
---
|
|
|
|
## Items always inside their Group component
|
|
|
|
Never render items directly inside the content container.
|
|
|
|
**Incorrect:**
|
|
|
|
```tsx
|
|
<SelectContent>
|
|
<SelectItem value="apple">Apple</SelectItem>
|
|
<SelectItem value="banana">Banana</SelectItem>
|
|
</SelectContent>
|
|
```
|
|
|
|
**Correct:**
|
|
|
|
```tsx
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectItem value="apple">Apple</SelectItem>
|
|
<SelectItem value="banana">Banana</SelectItem>
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
```
|
|
|
|
This applies to all group-based components:
|
|
|
|
| Item | Group |
|
|
|------|-------|
|
|
| `SelectItem`, `SelectLabel` | `SelectGroup` |
|
|
| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |
|
|
| `MenubarItem` | `MenubarGroup` |
|
|
| `ContextMenuItem` | `ContextMenuGroup` |
|
|
| `CommandItem` | `CommandGroup` |
|
|
|
|
---
|
|
|
|
## Callouts use Alert
|
|
|
|
```tsx
|
|
<Alert>
|
|
<AlertTitle>Warning</AlertTitle>
|
|
<AlertDescription>Something needs attention.</AlertDescription>
|
|
</Alert>
|
|
```
|
|
|
|
---
|
|
|
|
## Empty states use Empty component
|
|
|
|
```tsx
|
|
<Empty>
|
|
<EmptyHeader>
|
|
<EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
|
|
<EmptyTitle>No projects yet</EmptyTitle>
|
|
<EmptyDescription>Get started by creating a new project.</EmptyDescription>
|
|
</EmptyHeader>
|
|
<EmptyContent>
|
|
<Button>Create Project</Button>
|
|
</EmptyContent>
|
|
</Empty>
|
|
```
|
|
|
|
---
|
|
|
|
## Toast notifications use sonner
|
|
|
|
```tsx
|
|
import { toast } from "sonner"
|
|
|
|
toast.success("Changes saved.")
|
|
toast.error("Something went wrong.")
|
|
toast("File deleted.", {
|
|
action: { label: "Undo", onClick: () => undoDelete() },
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Choosing between overlay components
|
|
|
|
| Use case | Component |
|
|
|----------|-----------|
|
|
| Focused task that requires input | `Dialog` |
|
|
| Destructive action confirmation | `AlertDialog` |
|
|
| Side panel with details or filters | `Sheet` |
|
|
| Mobile-first bottom panel | `Drawer` |
|
|
| Quick info on hover | `HoverCard` |
|
|
| Small contextual content on click | `Popover` |
|
|
|
|
---
|
|
|
|
## Dialog, Sheet, and Drawer always need a Title
|
|
|
|
`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden.
|
|
|
|
```tsx
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Profile</DialogTitle>
|
|
<DialogDescription>Update your profile.</DialogDescription>
|
|
</DialogHeader>
|
|
...
|
|
</DialogContent>
|
|
```
|
|
|
|
---
|
|
|
|
## Card structure
|
|
|
|
Use full composition — don't dump everything into `CardContent`:
|
|
|
|
```tsx
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Team Members</CardTitle>
|
|
<CardDescription>Manage your team.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>...</CardContent>
|
|
<CardFooter>
|
|
<Button>Invite</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
```
|
|
|
|
---
|
|
|
|
## Button has no isPending or isLoading prop
|
|
|
|
Compose with `Spinner` + `data-icon` + `disabled`:
|
|
|
|
```tsx
|
|
<Button disabled>
|
|
<Spinner data-icon="inline-start" />
|
|
Saving...
|
|
</Button>
|
|
```
|
|
|
|
---
|
|
|
|
## TabsTrigger must be inside TabsList
|
|
|
|
Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:
|
|
|
|
```tsx
|
|
<Tabs defaultValue="account">
|
|
<TabsList>
|
|
<TabsTrigger value="account">Account</TabsTrigger>
|
|
<TabsTrigger value="password">Password</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="account">...</TabsContent>
|
|
</Tabs>
|
|
```
|
|
|
|
---
|
|
|
|
## Avatar always needs AvatarFallback
|
|
|
|
Always include `AvatarFallback` for when the image fails to load:
|
|
|
|
```tsx
|
|
<Avatar>
|
|
<AvatarImage src="/avatar.png" alt="User" />
|
|
<AvatarFallback>JD</AvatarFallback>
|
|
</Avatar>
|
|
```
|
|
|
|
---
|
|
|
|
## Use existing components instead of custom markup
|
|
|
|
| Instead of | Use |
|
|
|---|---|
|
|
| `<hr>` or `<div className="border-t">` | `<Separator />` |
|
|
| `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` |
|
|
| `<span className="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` |
|