shadcn
Implement shadcn/ui components for PhotoVault with accessibility and theming. Use when working with UI components, forms, dialogs, buttons, cards, tables, Tailwind styling, or fixing accessibility issues. Includes PhotoVault theme system and established UI patterns.
安装与使用
请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。
查看来源 ↗使用指南技能说明
⚠️ MANDATORY WORKFLOW - DO NOT SKIP
When this skill activates, you MUST follow the expert workflow before writing any code:
-
Spawn Domain Expert using the Task tool with this prompt:
Read the expert prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\shadcn-expert.md Then research the codebase and write an implementation plan to: docs/claude/plans/ui-[task-name]-plan.md Task: [describe the user's request] -
Spawn QA Critic after expert returns, using Task tool:
Read the QA critic prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\qa-critic-expert.md Review the plan at: docs/claude/plans/ui-[task-name]-plan.md Write critique to: docs/claude/plans/ui-[task-name]-critique.md -
Present BOTH plan and critique to user - wait for approval before implementing
DO NOT read files and start coding. DO NOT rationalize that "this is simple." Follow the workflow.
shadcn/ui Integration
Core Principles
Accessibility First, Always
Every interactive component MUST be keyboard navigable and screen reader friendly. shadcn/ui is built on Radix primitives which handle most accessibility out of the box.
<Button aria-label="Close dialog" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
Composition Over Configuration
shadcn/ui components are meant to be composed, not configured with dozens of props.
// ✅ RIGHT: Compose components
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>Content here</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
// ❌ WRONG: Mega-component with too many props
<Card title="Title" description="..." content="..." footerButton="Action" />
Always Use Semantic Color Tokens
Never hardcode colors. Use semantic tokens for theming support.
// ❌ WRONG: Hardcoded colors break theming
<div className="bg-white text-black" />
// ✅ RIGHT: Semantic tokens
<div className="bg-background text-foreground" />
Anti-Patterns
Not forwarding refs on custom components
// WRONG: Refs don't work
const CustomButton = ({ className, ...props }) => {
return <Button className={className} {...props} />
}
// RIGHT: Forward refs
const CustomButton = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof Button>
>(({ className, ...props }, ref) => {
return <Button ref={ref} className={className} {...props} />
})
CustomButton.displayName = "CustomButton"
Not using cn() for class merging
// WRONG: Classes override unpredictably
<Button className={`${baseClasses} ${conditionalClasses}`}>
// RIGHT: Use cn() for proper merging
import { cn } from "@/lib/utils"
<Button className={cn(baseClasses, conditionalClasses)}>
Nesting interactive elements
// WRONG: Button inside clickable card (accessibility violation)
<Card onClick={handleClick}>
<Button onClick={handleAction}>Action</Button>
</Card>
// RIGHT: Separate interactive areas
<Card>
<CardContent onClick={handleClick}>...</CardContent>
<CardFooter>
<Button onClick={handleAction}>Action</Button>
</CardFooter>
</Card>
Not connecting labels to inputs
// WRONG: Label not connected
<Label>Email</Label>
<Input type="email" />
// RIGHT: Use FormField pattern
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Button Variants
<Button>Save Changes</Button> // Primary
<Button variant="secondary">Cancel</Button> // Secondary
<Button variant="destructive">Delete</Button> // Destructive
<Button variant="ghost">Edit</Button> // Subtle
<Button variant="outline">View Details</Button> // Outline
// Icon button with accessibility
<Button variant="ghost" size="icon" aria-label="Settings">
<Settings className="h-4 w-4" />
</Button>
// Loading state
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
Dialog Pattern
import {
Dialog, DialogContent, DialogDescription,
DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog"
function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive">Delete Item</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="destructive" onClick={() => { onConfirm(); setOpen(false) }}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Form Pattern (react-hook-form + zod)
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
const formSchema = z.object({
email: z.string().email("Please enter a valid email"),
name: z.string().min(2, "Name must be at least 2 characters"),
})
function ContactForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: "", name: "" },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Submitting..." : "Submit"}
</Button>
</form>
</Form>
)
}
Toast Notifications (Sonner)
import { toast } from "sonner"
toast.success("Changes saved successfully")
toast.error("Failed to save changes")
// Promise toast
toast.promise(saveData(), {
loading: "Saving...",
success: "Data saved!",
error: "Could not save data",
})
PhotoVault Configuration
Theme System
PhotoVault has 5 color themes in src/lib/themes.ts:
- Warm Gallery - Cream background, terracotta primary (default)
- Cool Professional - Cool slate, indigo primary
- Gallery Dark - Warm charcoal, amber accents
- Soft Sage - Sage green, emerald + pink accents
- Original Teal - Original PhotoVault theme
PhotoVault UI Patterns
Gallery Card:
<Card className="overflow-hidden transition-shadow hover:shadow-lg">
<div className="aspect-[4/3] relative">
<Image src={coverImage} alt={galleryName} fill className="object-cover" />
</div>
<CardContent className="p-4">
<h3 className="font-semibold truncate">{galleryName}</h3>
<p className="text-sm text-muted-foreground">{photoCount} photos</p>
</CardContent>
</Card>
Paywall UI:
<div className="text-center py-12 px-4">
<Lock className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h2 className="text-2xl font-bold mb-2">Gallery Access Required</h2>
<p className="text-muted-foreground mb-6">Pay to unlock all photos</p>
<Button size="lg" onClick={handlePayment}>Pay Now - ${price}</Button>
</div>
Semantic Color Tokens
| Token | Purpose |
|---|---|
bg-background |
Page background |
bg-card |
Card backgrounds |
bg-primary |
Primary buttons |
bg-secondary |
Secondary elements |
bg-destructive |
Delete/error actions |
text-muted-foreground |
Subtle text |
border-border |
Borders |
Component Installation
npx shadcn@latest add button card dialog form input
Debugging Checklist
- Are you using semantic color tokens? Check for hardcoded colors
- Is cn() being used for class merging?
- Are refs being forwarded in custom components?
- Is the component accessible? Test with keyboard
- Are form fields properly connected to labels?
使用示例
查看来源仓库中的使用示例与配置说明。
查看来源 ↗来源历史
- 收录时间
- 2026-01-20
- 索引更新时间
- 2026-01-26
- Branch
- main
收录与同步日期属于本站记录。源码修改历史请查看来源仓库。
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!