Files
zhaoguiyang.site/.agents/skills/shadcn/rules/composition.md
zguiyang bbb2f41591 feat: add new OpenSpec skills for change management and onboarding
- 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.
2026-03-13 13:18:03 +08:00

4.2 KiB

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:

<SelectContent>
  <SelectItem value="apple">Apple</SelectItem>
  <SelectItem value="banana">Banana</SelectItem>
</SelectContent>

Correct:

<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

<Alert>
  <AlertTitle>Warning</AlertTitle>
  <AlertDescription>Something needs attention.</AlertDescription>
</Alert>

Empty states use Empty component

<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

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.

<DialogContent>
  <DialogHeader>
    <DialogTitle>Edit Profile</DialogTitle>
    <DialogDescription>Update your profile.</DialogDescription>
  </DialogHeader>
  ...
</DialogContent>

Card structure

Use full composition — don't dump everything into CardContent:

<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:

<Button disabled>
  <Spinner data-icon="inline-start" />
  Saving...
</Button>

TabsTrigger must be inside TabsList

Never render TabsTrigger directly inside Tabs — always wrap in TabsList:

<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:

<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">