Files
kolaytercih/.agents/skills/shadcn/rules/composition.md
bilalgursen 87f265ec15 Build TercihAI MVP: landing page, YÖK Atlas data pipeline, rank-based results
- Add product vision/market research doc (VISION.md)
- Set up shadcn/ui (radix + nova preset) with blue/orange theme, Outfit + Work Sans fonts
- Landing page: hero with rank input, problem/steps sections, comparison table, FAQ
- Data pipeline: CSV archive ingest (2021-2024) + live YÖK Atlas API refresh (2025)
  into SQLite (npm run ingest / refresh); zeros normalized to NULL
- /sonuc page: hayal/dengeli/garanti buckets by COALESCE(sira2025, sira2024),
  score-type switcher, 2024→2025 trend indicators
- Add ui-ux-pro-max and shadcn agent skills, shadcn MCP config, launch.json

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:46:53 +03:00

4.6 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
MessageScrollerItem MessageScrollerContent
Message (consecutive, same sender) MessageGroup
Bubble (stacked) BubbleGroup
Attachment (in a row) AttachmentGroup

Chat components nest in a fixed order (MessageScrollerProviderMessageScrollerMessageScrollerViewportMessageScrollerContentMessageScrollerItem). See chat.md.


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