mcpField

shadcn

github.com/squirrelfishcityhall150设计与 UI48 次安装
代码仓库

关于 shadcn

shadcn 是面向 shadcn/ui、Tailwind CSS 和 Radix UI primitives 的界面开发指南技能,涵盖组件设置、组合、表单与主题。组件复制到项目中,便于直接定制。

主要功能

  1. 01将组件复制到项目中
  2. 02直接修改组件源码
  3. 03组合可访问的 UI 基元
  4. 04构建表单与数据展示
  5. 05配置主题与深色模式

适用场景

  1. 01为应用添加按钮、对话框和卡片
  2. 02使用 react-hook-form 与 Zod 创建表单
  3. 03构建表格和导航菜单
  4. 04调整组件变体与主题颜色

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/squirrelfishcityhall150/claude-code-kit --skill shadcn

安装后在 Claude Code 中输入/shadcn确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

skill-rules-fragment.json
resources/
component-catalog.md
form-patterns.md
theming.md

详细介绍

shadcn/ui 开发指南

使用 Tailwind CSS 和 Radix UI 原语时,采用 shadcn/ui 组件的最佳实践。

核心原则

  1. 复制,而非安装:组件会复制到你的项目中,而不是作为依赖项安装
  2. 可自定义:直接在代码库中修改组件
  3. 无障碍:基于支持 ARIA 的 Radix UI 原语构建
  4. 类型安全:提供完整的 TypeScript 支持
  5. 可组合:使用简单原语构建复杂 UI

安装

初始设置
npx shadcn@latest init
添加组件
# Add individual components
npx shadcn@latest add button
npx shadcn@latest add form
npx shadcn@latest add dialog

# Add multiple
npx shadcn@latest add button card dialog
故障排除
npm 缓存错误(ENOTEMPTY)

如果 npx shadcn@latest add 因 ENOTEMPTY 或 syscall rename 等 npm 缓存错误而失败:

解决方案 1:清除 npm 缓存

npm cache clean --force
npx shadcn@latest add table

解决方案 2:使用 pnpm(推荐)

pnpm dlx shadcn@latest add table

解决方案 3:使用 yarn

yarn dlx shadcn@latest add table

解决方案 4:手动安装组件

访问 shadcn/ui 文档,找到所需组件,并将代码直接复制到项目中。

组件用法

Button 和 Card
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';

// Variants
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>

// Card
<Card>
  <CardHeader>
    <CardTitle>{post.title}</CardTitle>
    <CardDescription>{post.author}</CardDescription>
  </CardHeader>
  <CardContent>
    <p>{post.excerpt}</p>
  </CardContent>
</Card>
Dialog
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';

export function CreatePostDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Create Post</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Create New Post</DialogTitle>
          <DialogDescription>
            Fill in the details below to create a new post.
          </DialogDescription>
        </DialogHeader>
        <PostForm />
      </DialogContent>
    </Dialog>
  );
}

表单

使用 react-hook-form 创建基本表单
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';

const formSchema = z.object({
  title: z.string().min(1, 'Title is required'),
  content: z.string().min(10, 'Content must be at least 10 characters')
});

export function PostForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: '',
      content: ''
    }
  });

  const onSubmit = (values: z.infer<typeof formSchema>) => {
    console.log(values);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="title"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Title</FormLabel>
              <FormControl>
                <Input placeholder="Post title" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="content"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Content</FormLabel>
              <FormControl>
                <Textarea placeholder="Write your post..." {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Create Post</Button>
      </form>
    </Form>
  );
}
Select 字段
<FormField
  control={form.control}
  name="category"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Category</FormLabel>
      <Select onValueChange={field.onChange} defaultValue={field.value}>
        <FormControl>
          <SelectTrigger>
            <SelectValue placeholder="Select a category" />
          </SelectTrigger>
        </FormControl>
        <SelectContent>
          <SelectItem value="tech">Technology</SelectItem>
          <SelectItem value="design">Design</SelectItem>
          <SelectItem value="business">Business</SelectItem>
        </SelectContent>
      </Select>
      <FormMessage />
    </FormItem>
  )}
/>

数据展示

Table
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';

export function PostsTable({ posts }: { posts: Post[] }) {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Title</TableHead>
          <TableHead>Author</TableHead>
          <TableHead>Status</TableHead>
          <TableHead className="text-right">Actions</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {posts.map((post) => (
          <TableRow key={post.id}>
            <TableCell className="font-medium">{post.title}</TableCell>
            <TableCell>{post.author.name}</TableCell>
            <TableCell>
              <Badge variant={post.published ? 'default' : 'secondary'}>
                {post.published ? 'Published' : 'Draft'}
              </Badge>
            </TableCell>
            <TableCell className="text-right">
              <Button variant="ghost" size="sm">Edit</Button>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

导航

Badge 和 Dropdown Menu
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';

export function UserMenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost">
          <User className="h-4 w-4" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuLabel>My Account</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuItem>Profile</DropdownMenuItem>
        <DropdownMenuItem>Settings</DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem>Log out</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
Tabs
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

export function PostTabs() {
  return (
    <Tabs defaultValue="published">
      <TabsList>
        <TabsTrigger value="published">Published</TabsTrigger>
        <TabsTrigger value="drafts">Drafts</TabsTrigger>
        <TabsTrigger value="archived">Archived</TabsTrigger>
      </TabsList>
      <TabsContent value="published">
        <PublishedPosts />
      </TabsContent>
      <TabsContent value="drafts">
        <DraftPosts />
      </TabsContent>
      <TabsContent value="archived">
        <ArchivedPosts />
      </TabsContent>
    </Tabs>
  );
}

反馈

Toast
'use client';

import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';

export function ToastExample() {
  const { toast } = useToast();

  return (
    <Button
      onClick={() => {
        toast({
          title: 'Post created',
          description: 'Your post has been published successfully.'
        });
      }}
    >
      Create Post
    </Button>
  );
}

// With variant
toast({
  variant: 'destructive',
  title: 'Error',
  description: 'Failed to create post. Please try again.'
});
Alert
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';

export function AlertExample() {
  return (
    <Alert variant="destructive">
      <AlertCircle className="h-4 w-4" />
      <AlertTitle>Error</AlertTitle>
      <AlertDescription>
        Your session has expired. Please log in again.
      </AlertDescription>
    </Alert>
  );
}

加载状态

Skeleton
import { Skeleton } from '@/components/ui/skeleton';

export function PostCardSkeleton() {
  return (
    <div className="flex flex-col space-y-3">
      <Skeleton className="h-[125px] w-full rounded-xl" />
      <div className="space-y-2">
        <Skeleton className="h-4 w-[250px]" />
        <Skeleton className="h-4 w-[200px]" />
      </div>
    </div>
  );
}

自定义

修改组件

组件位于你的代码库中,可直接编辑:

// components/ui/button.tsx
export const buttonVariants = cva(
  "inline-flex items-center justify-center...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        // Add custom variant
        brand: "bg-gradient-to-r from-blue-500 to-purple-600 text-white"
      }
    }
  }
);
使用自定义变体
<Button variant="brand">Custom Brand Button</Button>

主题

CSS 变量(OKLCH 格式)

shadcn/ui 现在使用 OKLCH 颜色格式,以实现更准确的色彩和更均匀的感知效果:

/* app/globals.css */
@layer base {
  :root {
    --background: oklch(1 0 0);
    --foreground: oklch(0.145 0 0);
    --primary: oklch(0.205 0 0);
    --primary-foreground: oklch(0.985 0 0);
    /* ... */
  }

  .dark {
    --background: oklch(0.145 0 0);
    --foreground: oklch(0.985 0 0);
    --primary: oklch(0.598 0.15 264);
    --primary-foreground: oklch(0.205 0 0);
    /* ... */
  }
}
深色模式
// components/theme-toggle.tsx
'use client';

import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';

export function ThemeToggle() {
  const { setTheme, theme } = useTheme();

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

组合模式

组合组件
export function CreatePostCard() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Create Post</CardTitle>
        <CardDescription>Share your thoughts with the world</CardDescription>
      </CardHeader>
      <CardContent>
        <PostForm />
      </CardContent>
      <CardFooter className="flex justify-between">
        <Button variant="outline">Save Draft</Button>
        <Button>Publish</Button>
      </CardFooter>
    </Card>
  );
}
带表单的模态框
export function CreatePostModal() {
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button>New Post</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[600px]">
        <DialogHeader>
          <DialogTitle>Create Post</DialogTitle>
        </DialogHeader>
        <PostForm onSuccess={() => setOpen(false)} />
      </DialogContent>
    </Dialog>
  );
}

更多资源

如需详细信息,请参阅:

评论

支持 Markdown