ProductReadyProductReady

UI 组件

ProductReady 的 UI 组件库 (KUI) 完整参考

UI 组件

ProductReady 使用 KUI - 一个基于 shadcn/ui 构建的综合 UI 组件库,并带有额外增强。所有组件都是:

  • 类型安全 - 完整的 TypeScript 支持
  • 可访问 - 符合 WCAG 2.1 AA
  • 可主题化 - CSS 变量用于自定义
  • 深色模式 - 内置深色模式支持
  • 可摇树优化 - 只导入你需要的

位置:所有组件位于 packages/kui/src/components/ui/


快速导入

import { Button } from 'kui/button';
import { Card } from 'kui/card';
import { Input } from 'kui/input';

export function MyComponent() {
  return (
    <Card>
      <Input placeholder="输入文本" />
      <Button>提交</Button>
    </Card>
  );
}

组件分类

表单组件

import { Input } from 'kui/input';

// 基本输入
<Input placeholder="邮箱" type="email" />

// 带标签
<div>
  <Label htmlFor="email">邮箱</Label>
  <Input id="email" type="email" />
</div>

// 禁用
<Input disabled placeholder="禁用" />
import { Textarea } from 'kui/textarea';

<Textarea placeholder="你的消息..." rows={4} />
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'kui/select';

<Select>
  <SelectTrigger>
    <SelectValue placeholder="选择选项" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">选项 1</SelectItem>
    <SelectItem value="option2">选项 2</SelectItem>
  </SelectContent>
</Select>
import { Checkbox } from 'kui/checkbox';
import { Label } from 'kui/label';

<div className="flex items-center gap-2">
  <Checkbox id="terms" />
  <Label htmlFor="terms">接受条款</Label>
</div>

可用的表单组件

  • Input - 带变体的文本输入
  • Textarea - 多行文本输入
  • Select - 下拉选择
  • Checkbox - 带标签的复选框
  • Radio Group - 单选按钮组
  • Switch - 切换开关
  • Slider - 范围滑块
  • Calendar - 日期选择器
  • Input OTP - 一次性密码输入
  • Form - 带验证的表单包装器

布局组件

Card

import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'kui/card';

<Card>
  <CardHeader>
    <CardTitle>卡片标题</CardTitle>
    <CardDescription>卡片描述在这里</CardDescription>
  </CardHeader>
  <CardContent>
    <p>卡片内容</p>
  </CardContent>
  <CardFooter>
    <Button>操作</Button>
  </CardFooter>
</Card>

Separator

import { Separator } from 'kui/separator';

<div>
  <p>上方内容</p>
  <Separator className="my-4" />
  <p>下方内容</p>
</div>

// 垂直分隔符
<Separator orientation="vertical" className="h-4" />

Tabs

import { Tabs, TabsContent, TabsList, TabsTrigger } from 'kui/tabs';

<Tabs defaultValue="tab1">
  <TabsList>
    <TabsTrigger value="tab1">标签 1</TabsTrigger>
    <TabsTrigger value="tab2">标签 2</TabsTrigger>
  </TabsList>
  <TabsContent value="tab1">内容 1</TabsContent>
  <TabsContent value="tab2">内容 2</TabsContent>
</Tabs>

Accordion

import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'kui/accordion';

<Accordion type="single" collapsible>
  <AccordionItem value="item-1">
    <AccordionTrigger>它是可访问的吗?</AccordionTrigger>
    <AccordionContent>
      是的。它遵循 WAI-ARIA 设计模式。
    </AccordionContent>
  </AccordionItem>
</Accordion>

按钮和操作

Button

import { Button } from 'kui/button';

// 变体
<Button variant="default">默认</Button>
<Button variant="destructive">破坏性</Button>
<Button variant="outline">轮廓</Button>
<Button variant="secondary">次要</Button>
<Button variant="ghost">幽灵</Button>
<Button variant="link">链接</Button>

// 尺寸
<Button size="default">默认</Button>
<Button size="sm">小</Button>
<Button size="lg">大</Button>
<Button size="icon"><Icon /></Button>

// 状态
<Button disabled>禁用</Button>
<Button loading>加载中...</Button>

Button Group

import { ButtonGroup } from 'kui/button-group';

<ButtonGroup>
  <Button>左</Button>
  <Button>中</Button>
  <Button>右</Button>
</ButtonGroup>

Toggle

import { Toggle } from 'kui/toggle';

<Toggle aria-label="切换斜体">
  <Italic className="h-4 w-4" />
</Toggle>

覆盖组件

Dialog

import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from 'kui/dialog';

<Dialog>
  <DialogTrigger asChild>
    <Button>打开对话框</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>对话框标题</DialogTitle>
      <DialogDescription>对话框描述</DialogDescription>
    </DialogHeader>
    <p>对话框内容在这里</p>
  </DialogContent>
</Dialog>

Drawer

import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from 'kui/drawer';

<Drawer>
  <DrawerTrigger>打开抽屉</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>抽屉标题</DrawerTitle>
      <DrawerDescription>抽屉描述</DrawerDescription>
    </DrawerHeader>
  </DrawerContent>
</Drawer>

Sheet

import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from 'kui/sheet';

<Sheet>
  <SheetTrigger>打开表单</SheetTrigger>
  <SheetContent side="right">
    <SheetHeader>
      <SheetTitle>表单标题</SheetTitle>
      <SheetDescription>表单描述</SheetDescription>
    </SheetHeader>
  </SheetContent>
</Sheet>

Popover

import { Popover, PopoverContent, PopoverTrigger } from 'kui/popover';

<Popover>
  <PopoverTrigger>打开弹出框</PopoverTrigger>
  <PopoverContent>
    <p>弹出框内容</p>
  </PopoverContent>
</Popover>

Tooltip

import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'kui/tooltip';

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger>悬停我</TooltipTrigger>
    <TooltipContent>
      <p>工具提示文本</p>
    </TooltipContent>
  </Tooltip>
</TooltipProvider>

反馈组件

Alert

import { Alert, AlertDescription, AlertTitle } from 'kui/alert';
import { AlertCircle } from 'lucide-react';

<Alert>
  <AlertCircle className="h-4 w-4" />
  <AlertTitle>注意!</AlertTitle>
  <AlertDescription>
    这是一条警告消息。
  </AlertDescription>
</Alert>

// 变体
<Alert variant="default">默认警告</Alert>
<Alert variant="destructive">错误警告</Alert>

Toast (Sonner)

import { toast } from 'sonner';

// 成功
toast.success('任务创建成功!');

// 错误
toast.error('出了些问题');

// 信息
toast.info('有新更新可用');

// 自定义
toast('自定义消息', {
  description: '带描述',
  action: {
    label: '撤消',
    onClick: () => console.log('撤消'),
  },
});

Progress

import { Progress } from 'kui/progress';

<Progress value={60} /> // 60%

Spinner

import { Spinner } from 'kui/spinner';

<Spinner size="sm" />
<Spinner size="default" />
<Spinner size="lg" />

Skeleton

import { Skeleton } from 'kui/skeleton';

<div className="space-y-2">
  <Skeleton className="h-4 w-full" />
  <Skeleton className="h-4 w-3/4" />
</div>

数据展示

Table

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'kui/table';

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>名称</TableHead>
      <TableHead>邮箱</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>张三</TableCell>
      <TableCell>john@example.com</TableCell>
    </TableRow>
  </TableBody>
</Table>

Badge

import { Badge } from 'kui/badge';

<Badge>默认</Badge>
<Badge variant="secondary">次要</Badge>
<Badge variant="destructive">破坏性</Badge>
<Badge variant="outline">轮廓</Badge>

Avatar

import { Avatar, AvatarFallback, AvatarImage } from 'kui/avatar';

<Avatar>
  <AvatarImage src="/avatar.jpg" alt="用户" />
  <AvatarFallback>张三</AvatarFallback>
</Avatar>

Stats Card

import { StatsCard } from 'kui/stats-card';

<StatsCard
  title="总用户"
  value="1,234"
  change="+12%"
  trend="up"
/>

Chart

import { Chart } from 'kui/chart';

<Chart
  data={chartData}
  type="line"
  xKey="date"
  yKey="value"
/>

导航组件

Breadcrumb

import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from 'kui/breadcrumb';

<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/">首页</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>当前页面</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>

Sidebar

import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from 'kui/sidebar';

<Sidebar>
  <SidebarContent>
    <SidebarGroup>
      <SidebarGroupLabel>导航</SidebarGroupLabel>
      <SidebarGroupContent>
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton asChild>
              <a href="/dashboard">仪表板</a>
            </SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarGroupContent>
    </SidebarGroup>
  </SidebarContent>
</Sidebar>

Menubar

import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarTrigger } from 'kui/menubar';

<Menubar>
  <MenubarMenu>
    <MenubarTrigger>文件</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>新建文件</MenubarItem>
      <MenubarItem>保存</MenubarItem>
    </MenubarContent>
  </MenubarMenu>
</Menubar>

Pagination

import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from 'kui/pagination';

<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>

特殊组件

命令面板

import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from 'kui/command';

<Command>
  <CommandInput placeholder="输入命令..." />
  <CommandList>
    <CommandEmpty>未找到结果。</CommandEmpty>
    <CommandGroup heading="建议">
      <CommandItem>仪表板</CommandItem>
      <CommandItem>设置</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>

轮播

import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from 'kui/carousel';

<Carousel>
  <CarouselContent>
    <CarouselItem>幻灯片 1</CarouselItem>
    <CarouselItem>幻灯片 2</CarouselItem>
    <CarouselItem>幻灯片 3</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

上下文菜单

import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from 'kui/context-menu';

<ContextMenu>
  <ContextMenuTrigger>右键点击我</ContextMenuTrigger>
  <ContextMenuContent>
    <ContextMenuItem>编辑</ContextMenuItem>
    <ContextMenuItem>删除</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

完整组件列表

组件描述类别
accordion可折叠内容部分布局
alert-dialog模态确认对话框覆盖
alert反馈消息反馈
aspect-ratio保持纵横比布局
avatar用户头像图片数据展示
badge状态指示器数据展示
breadcrumb导航面包屑导航
button-group分组按钮操作
button可点击按钮操作
calendar日期选择器表单
card内容容器布局
carousel图片/内容滑块特殊
chart数据可视化数据展示
checkbox复选框输入表单
collapsible可展开内容布局
command命令面板特殊
context-menu右键菜单导航
dialog模态对话框覆盖
drawer侧边面板覆盖
dropdown-menu下拉菜单导航
empty空状态反馈
field表单字段包装器表单
form表单容器表单
hover-card悬停预览卡片覆盖
input-group分组输入表单
input-otpOTP 输入表单
input文本输入表单
item列表项数据展示
kbd键盘快捷键数据展示
label输入标签表单
menubar菜单栏导航
navigation-menu导航菜单导航
pagination页面导航导航
popover浮动内容覆盖
progress进度条反馈
radio-group单选按钮表单
resizable可调整大小面板布局
scroll-area可滚动区域布局
select下拉选择表单
separator分隔线布局
sheet侧边表单覆盖
sidebar应用侧边栏导航
skeleton加载占位符反馈
slider范围滑块表单
sonnerToast 通知反馈
spinner加载旋转器反馈
stats-card指标卡片数据展示
switch切换开关表单
table-list-view表格/列表视图数据展示
table数据表格数据展示
tabs选项卡内容布局
textarea多行输入表单
toggle-group切换按钮组操作
toggle切换按钮操作
tooltip悬停工具提示覆盖

自定义

主题颜色

编辑 apps/productready/src/app/globals.css:

@layer base {
  :root {
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    /* ... 更多变量 */
  }
  
  .dark {
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    /* ... 更多变量 */
  }
}

组件变体

扩展组件变体:

// 创建自定义按钮变体
<Button className="bg-purple-600 hover:bg-purple-700">
  自定义颜色
</Button>

最佳实践

✅ 应该做

  • 使用语义 HTML - 组件渲染正确的 HTML
  • 只导入需要的 - 摇树优化包大小
  • 遵循可访问性 - 使用标签、ARIA 属性
  • 组合组件 - 从简单部分构建复杂 UI
  • 测试深色模式 - 所有组件支持深色模式

❌ 不应该做

  • 不要直接修改 KUI - 创建包装器组件
  • 不要跳过标签 - 始终标记表单输入
  • 不要内联样式 - 使用 className 和 Tailwind
  • 不要忘记加载状态 - 为异步操作显示旋转器

常见模式和实际示例

用户资料卡片

组合多个组件创建完整的用户资料卡片:

import { Card, CardContent, CardFooter, CardHeader } from 'kui/card';
import { Avatar, AvatarFallback, AvatarImage } from 'kui/avatar';
import { Button } from 'kui/button';
import { Badge } from 'kui/badge';

export function UserProfileCard({ user }: { user: User }) {
  return (
    <Card className="w-[350px]">
      <CardHeader className="flex flex-row items-center gap-4">
        <Avatar className="h-12 w-12">
          <AvatarImage src={user.avatar} alt={user.name} />
          <AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
        </Avatar>
        <div className="flex flex-col">
          <h3 className="font-semibold">{user.name}</h3>
          <p className="text-sm text-muted-foreground">{user.email}</p>
        </div>
      </CardHeader>
      <CardContent>
        <div className="flex gap-2 flex-wrap">
          <Badge variant="secondary">专业会员</Badge>
          <Badge variant="outline">活跃</Badge>
        </div>
        <p className="mt-4 text-sm">{user.bio}</p>
      </CardContent>
      <CardFooter className="flex gap-2">
        <Button variant="default" className="flex-1">发消息</Button>
        <Button variant="outline" className="flex-1">关注</Button>
      </CardFooter>
    </Card>
  );
}

带验证的设置表单

包含多种输入类型的完整表单示例:

'use client';

import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'kui/card';
import { Input } from 'kui/input';
import { Label } from 'kui/label';
import { Switch } from 'kui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'kui/select';
import { Button } from 'kui/button';
import { Separator } from 'kui/separator';
import { toast } from 'sonner';

export function SettingsForm() {
  const [settings, setSettings] = useState({
    name: '',
    email: '',
    theme: 'light',
    notifications: true,
  });

  const handleSave = () => {
    toast.success('设置已成功保存!');
  };

  return (
    <Card className="max-w-2xl">
      <CardHeader>
        <CardTitle>账户设置</CardTitle>
        <CardDescription>管理您的账户偏好设置</CardDescription>
      </CardHeader>
      <CardContent className="space-y-6">
        <div className="space-y-2">
          <Label htmlFor="name">显示名称</Label>
          <Input 
            id="name" 
            placeholder="输入您的名字"
            value={settings.name}
            onChange={(e) => setSettings({ ...settings, name: e.target.value })}
          />
        </div>

        <div className="space-y-2">
          <Label htmlFor="email">电子邮件地址</Label>
          <Input 
            id="email" 
            type="email"
            placeholder="your@email.com"
            value={settings.email}
            onChange={(e) => setSettings({ ...settings, email: e.target.value })}
          />
        </div>

        <Separator />

        <div className="space-y-2">
          <Label htmlFor="theme">主题</Label>
          <Select 
            value={settings.theme}
            onValueChange={(value) => setSettings({ ...settings, theme: value })}
          >
            <SelectTrigger id="theme">
              <SelectValue placeholder="选择主题" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="light">浅色</SelectItem>
              <SelectItem value="dark">深色</SelectItem>
              <SelectItem value="system">系统</SelectItem>
            </SelectContent>
          </Select>
        </div>

        <div className="flex items-center justify-between">
          <div className="space-y-0.5">
            <Label>邮件通知</Label>
            <p className="text-sm text-muted-foreground">
              接收有关账户活动的电子邮件
            </p>
          </div>
          <Switch
            checked={settings.notifications}
            onCheckedChange={(checked) => setSettings({ ...settings, notifications: checked })}
          />
        </div>

        <div className="flex gap-2 justify-end">
          <Button variant="outline">取消</Button>
          <Button onClick={handleSave}>保存更改</Button>
        </div>
      </CardContent>
    </Card>
  );
}

带操作的数据表格

带行操作的交互式表格:

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'kui/table';
import { Badge } from 'kui/badge';
import { Button } from 'kui/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from 'kui/dropdown-menu';
import { MoreHorizontal, Pencil, Trash } from 'lucide-react';

export function UsersTable({ users }: { users: User[] }) {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>名称</TableHead>
          <TableHead>邮箱</TableHead>
          <TableHead>状态</TableHead>
          <TableHead>角色</TableHead>
          <TableHead className="text-right">操作</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {users.map((user) => (
          <TableRow key={user.id}>
            <TableCell className="font-medium">{user.name}</TableCell>
            <TableCell>{user.email}</TableCell>
            <TableCell>
              <Badge variant={user.active ? 'default' : 'secondary'}>
                {user.active ? '活跃' : '不活跃'}
              </Badge>
            </TableCell>
            <TableCell>{user.role}</TableCell>
            <TableCell className="text-right">
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <Button variant="ghost" size="icon">
                    <MoreHorizontal className="h-4 w-4" />
                  </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end">
                  <DropdownMenuItem>
                    <Pencil className="mr-2 h-4 w-4" />
                    编辑
                  </DropdownMenuItem>
                  <DropdownMenuItem className="text-destructive">
                    <Trash className="mr-2 h-4 w-4" />
                    删除
                  </DropdownMenuItem>
                </DropdownMenuContent>
              </DropdownMenu>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

确认对话框

带有良好用户体验的删除确认:

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from 'kui/alert-dialog';
import { Button } from 'kui/button';
import { Trash } from 'lucide-react';

export function DeleteButton({ itemName, onDelete }: { itemName: string; onDelete: () => void }) {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="destructive" size="sm">
          <Trash className="mr-2 h-4 w-4" />
          删除
        </Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>您确定要这样做吗?</AlertDialogTitle>
          <AlertDialogDescription>
            此操作无法撤消。这将永久删除 "{itemName}"
            并从我们的服务器中删除数据。
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>取消</AlertDialogCancel>
          <AlertDialogAction onClick={onDelete} className="bg-destructive text-destructive-foreground">
            删除
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}

仪表板统计卡片

使用统计卡片显示指标:

import { Card, CardContent, CardHeader, CardTitle } from 'kui/card';
import { ArrowUp, ArrowDown, Users, DollarSign, Activity, TrendingUp } from 'lucide-react';

export function DashboardStats() {
  const stats = [
    { title: '总收入', value: '¥352,341', change: '+20.1%', icon: DollarSign, positive: true },
    { title: '活跃用户', value: '2,345', change: '+12%', icon: Users, positive: true },
    { title: '转化率', value: '3.24%', change: '-2.4%', icon: TrendingUp, positive: false },
    { title: 'API 调用', value: '120万', change: '+45%', icon: Activity, positive: true },
  ];

  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
      {stats.map((stat) => (
        <Card key={stat.title}>
          <CardHeader className="flex flex-row items-center justify-between pb-2">
            <CardTitle className="text-sm font-medium text-muted-foreground">
              {stat.title}
            </CardTitle>
            <stat.icon className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{stat.value}</div>
            <p className={`text-xs flex items-center gap-1 ${stat.positive ? 'text-green-600' : 'text-red-600'}`}>
              {stat.positive ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />}
              较上月 {stat.change}
            </p>
          </CardContent>
        </Card>
      ))}
    </div>
  );
}

加载状态和骨架屏

显示加载占位符:

import { Card, CardContent, CardHeader } from 'kui/card';
import { Skeleton } from 'kui/skeleton';

export function UserCardSkeleton() {
  return (
    <Card>
      <CardHeader className="flex flex-row items-center gap-4">
        <Skeleton className="h-12 w-12 rounded-full" />
        <div className="space-y-2 flex-1">
          <Skeleton className="h-4 w-[200px]" />
          <Skeleton className="h-3 w-[150px]" />
        </div>
      </CardHeader>
      <CardContent>
        <Skeleton className="h-20 w-full" />
      </CardContent>
    </Card>
  );
}

// 在组件中使用
export function UsersList() {
  const { data: users, isLoading } = trpc.users.list.useQuery();

  if (isLoading) {
    return (
      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
        {Array.from({ length: 6 }).map((_, i) => (
          <UserCardSkeleton key={i} />
        ))}
      </div>
    );
  }

  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

组件组合技巧

构建复杂表单

有效组合表单组件:

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

const formSchema = z.object({
  username: z.string().min(3).max(20),
  email: z.string().email(),
});

export function SignupForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
  });

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>用户名</FormLabel>
              <FormControl>
                <Input placeholder="zhangsan" {...field} />
              </FormControl>
              <FormDescription>这是您的公开显示名称。</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">创建账户</Button>
      </form>
    </Form>
  );
}

响应式布局

使用卡片和网格实现响应式设计:

<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
  {items.map((item) => (
    <Card key={item.id} className="hover:shadow-lg transition-shadow">
      <CardHeader>
        <CardTitle>{item.title}</CardTitle>
      </CardHeader>
      <CardContent>{item.content}</CardContent>
    </Card>
  ))}
</div>

可访问性提示

键盘导航

所有 KUI 组件都支持键盘导航:

  • Tab - 在可聚焦元素之间移动
  • Enter/Space - 激活按钮和开关
  • 方向键 - 导航菜单、选择器、单选按钮组
  • Escape - 关闭对话框、弹出框、侧边栏
  • Home/End - 导航到列表中的第一个/最后一个项目

屏幕阅读器支持

组件包含 ARIA 属性:

// 带 aria-label 的按钮
<Button aria-label="关闭对话框">
  <X className="h-4 w-4" />
</Button>

// 正确标记的输入
<Label htmlFor="email">电子邮件</Label>
<Input id="email" type="email" aria-describedby="email-description" />
<p id="email-description" className="text-sm text-muted-foreground">
  我们永远不会分享您的电子邮件。
</p>

性能优化

懒加载组件

对于大型应用程序,懒加载重型组件:

import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('~/components/heavy-chart'), {
  loading: () => <Skeleton className="h-[400px] w-full" />,
  ssr: false,
});

记忆化

防止不必要的重新渲染:

import { memo } from 'react';

export const UserCard = memo(function UserCard({ user }: { user: User }) {
  return (
    <Card>
      {/* 组件内容 */}
    </Card>
  );
});

下一步


所有组件都已生产就绪并完全可访问! 🎨

如需实时组件演示和交互示例,请在开发模式下探索 ProductReady 仪表板。

On this page