B.0 基础工具:cn 函数(必须有)
cn 是 shadcn/ui 的经典组合:clsx + tailwind-merge。
在所有组件里用于合并类名并解决冲突。
// src/lib/cn.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
后面所有组件都默认从这里引用 cn。
B.1 Button 组件(核心中的核心)
B.1.1 设计目标
- 支持
variant:primary / secondary / outline / ghost / destructive / link - 支持
size:sm / md / lg / icon - 支持
loading状态(显示 spinner) - 支持 disabled
- 支持
asChild(配合 Radix / Next Link) - 使用 Tailwind + shadcn 风格
B.1.2 代码示例
// src/components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/cn"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors " +
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 " +
"disabled:pointer-events-none disabled:opacity-50 " +
"ring-offset-background",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
loading?: boolean
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, loading, children, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(
buttonVariants({ variant, size, className }),
loading && "relative text-transparent cursor-wait"
)}
ref={ref}
disabled={props.disabled || loading}
{...props}
>
{loading && (
<span className="absolute inset-0 flex items-center justify-center">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent" />
</span>
)}
<span className={cn(loading && "invisible")}>{children}</span>
</Comp>
)
}
)
Button.displayName = "Button"
用法示例:
<Button>默认按钮</Button>
<Button variant="outline">Outline</Button>
<Button variant="destructive" size="sm">删除</Button>
<Button loading>保存中...</Button>
B.2 Input / Textarea 表单组件
B.2.1 Input 组件
// src/components/ui/input.tsx
import * as React from "react"
import { cn } from "@/lib/cn"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm " +
"ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium " +
"placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 " +
"focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
B.2.2 Textarea 组件
// src/components/ui/textarea.tsx
import * as React from "react"
import { cn } from "@/lib/cn"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm " +
"ring-offset-background placeholder:text-muted-foreground " +
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " +
"focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
B.3 表单字段组合:FormField / Label / Description / Error
一个常见模式:label + input + hint + error,统一间距和样式,避免每个页面都手写。
// src/components/ui/form-field.tsx
import { cn } from "@/lib/cn"
interface FormFieldProps {
label?: string
description?: string
error?: string
requiredMark?: boolean
className?: string
children: React.ReactNode
}
export function FormField({
label,
description,
error,
requiredMark,
className,
children,
}: FormFieldProps) {
return (
<div className={cn("space-y-1", className)}>
{label && (
<label className="flex items-center text-sm font-medium text-foreground">
<span>{label}</span>
{requiredMark && <span className="ml-1 text-destructive">*</span>}
</label>
)}
{children}
{description && !error && (
<p className="text-xs text-muted-foreground">{description}</p>
)}
{error && (
<p className="text-xs text-destructive flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-destructive" />
<span>{error}</span>
</p>
)}
</div>
)
}
使用示例:
<FormField label="邮箱" description="用于登录和通知">
<Input type="email" placeholder="you@example.com" />
</FormField>
<FormField label="密码" error="密码长度至少 8 位" requiredMark>
<Input type="password" />
</FormField>
B.4 Card(卡片组件)
用于后台 / 控制台的基础模块容器。
// src/components/ui/card.tsx
import * as React from "react"
import { cn } from "@/lib/cn"
export function Card({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"rounded-xl border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
)
}
export function CardHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("flex flex-col space-y-1.5 p-6 border-b", className)}
{...props}
/>
)
}
export function CardTitle({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<h3
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
)
}
export function CardDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export function CardContent({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn("p-6", className)} {...props} />
)
}
export function CardFooter({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("flex items-center justify-end gap-2 p-6 border-t", className)}
{...props}
/>
)
}
使用示例:
<Card>
<CardHeader>
<CardTitle>本月收入</CardTitle>
<CardDescription>按自然月统计,不含退款</CardDescription>
</CardHeader>
<CardContent>
<div className="text-3xl font-semibold">¥ 128,900</div>
</CardContent>
<CardFooter>
<Button variant="outline" size="sm">导出报表</Button>
<Button size="sm">查看详情</Button>
</CardFooter>
</Card>
B.5 Badge(小徽章)
统一用在标签、状态标识、计数等。
// src/components/ui/badge.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/cn"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold " +
"transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "text-foreground",
success:
"border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-300",
warning:
"border-transparent bg-amber-500/10 text-amber-600 dark:text-amber-300",
destructive:
"border-transparent bg-destructive/10 text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
export function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
示例:
<Badge>默认</Badge>
<Badge variant="success">已完成</Badge>
<Badge variant="warning">待审核</Badge>
<Badge variant="destructive">已关闭</Badge>
B.6 表格封装:DataTable(简化版)
只给你一个简化但实用的版本,适合后台常见列表:
- 支持 striped rows
- 支持 hover
- 支持空态
- Tailwind 样式可进一步抽象
// src/components/ui/simple-table.tsx
import { cn } from "@/lib/cn"
export interface Column<T> {
key: keyof T | string
header: React.ReactNode
render?: (row: T) => React.ReactNode
className?: string
}
interface SimpleTableProps<T> {
columns: Column<T>[]
data: T[]
emptyText?: string
className?: string
}
export function SimpleTable<T>({
columns,
data,
emptyText = "暂无数据",
className,
}: SimpleTableProps<T>) {
return (
<div className={cn("overflow-hidden rounded-xl border bg-card", className)}>
<table className="min-w-full divide-y divide-border">
<thead className="bg-muted/50">
<tr>
{columns.map((col, i) => (
<th
key={String(col.key) + i}
className={cn(
"px-4 py-3 text-left text-xs font-medium text-muted-foreground",
col.className
)}
>
{col.header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border bg-card">
{data.length === 0 && (
<tr>
<td
colSpan={columns.length}
className="px-4 py-6 text-center text-sm text-muted-foreground"
>
{emptyText}
</td>
</tr>
)}
{data.map((row, rowIndex) => (
<tr
key={rowIndex}
className="hover:bg-muted/40 transition-colors"
>
{columns.map((col, colIndex) => (
<td
key={String(col.key) + colIndex}
className={cn(
"px-4 py-3 text-sm text-foreground align-middle",
col.className
)}
>
{col.render
? col.render(row)
: (row as any)[col.key as keyof T]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
使用示例:
type User = {
id: string
name: string
email: string
status: "active" | "pending" | "suspended"
}
const columns: Column<User>[] = [
{ key: "name", header: "姓名" },
{ key: "email", header: "邮箱" },
{
key: "status",
header: "状态",
render: (row) => (
<Badge
variant={
row.status === "active"
? "success"
: row.status === "pending"
? "warning"
: "destructive"
}
>
{row.status === "active"
? "正常"
: row.status === "pending"
? "待激活"
: "已停用"}
</Badge>
),
},
]
<SimpleTable columns={columns} data={userList} />
B.7 Dialog / Modal(结合 Radix + Tailwind)
标准做法是用 Radix UI 的 Dialog,配合 Tailwind 封装。
// src/components/ui/dialog.tsx
import * as React from "react"
import * as RadixDialog from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/cn"
export const Dialog = RadixDialog.Root
export const DialogTrigger = RadixDialog.Trigger
export const DialogClose = RadixDialog.Close
export function DialogContent({
className,
children,
...props
}: React.ComponentPropsWithoutRef<typeof RadixDialog.Content>) {
return (
<RadixDialog.Portal>
<RadixDialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
<RadixDialog.Content
className={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 " +
"rounded-xl border bg-background p-6 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring",
className
)}
{...props}
>
{children}
<RadixDialog.Close className="absolute right-3 top-3 rounded-full p-1 text-muted-foreground hover:bg-muted">
<X className="h-4 w-4" />
</RadixDialog.Close>
</RadixDialog.Content>
</RadixDialog.Portal>
)
}
export function DialogHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn("space-y-1.5 mb-4", className)} {...props} />
)
}
export function DialogTitle({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h2
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
)
}
export function DialogDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
)
}
export function DialogFooter({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("mt-6 flex items-center justify-end gap-2", className)}
{...props}
/>
)
}
使用示例:
<Dialog>
<DialogTrigger asChild>
<Button>新建项目</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>新建项目</DialogTitle>
<DialogDescription>项目用于组织你的应用、数据和成员。</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<FormField label="项目名称" requiredMark>
<Input placeholder="例如:MiniPlay Studio" />
</FormField>
<FormField label="描述">
<Textarea rows={3} />
</FormField>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">取消</Button>
</DialogClose>
<Button>创建</Button>
</DialogFooter>
</DialogContent>
</Dialog>
B.8 Navbar / 顶部导航(SaaS 通用)
简化版:左边 Logo,中间导航,右边用户菜单。
// src/components/layout/top-nav.tsx
import Link from "next/link"
import { cn } from "@/lib/cn"
import { Button } from "@/components/ui/button"
interface NavItem {
label: string
href: string
active?: boolean
}
interface TopNavProps {
items: NavItem[]
className?: string
}
export function TopNav({ items, className }: TopNavProps) {
return (
<header className={cn("border-b bg-background", className)}>
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
<div className="flex items-center gap-2">
<div className="h-7 w-7 rounded-lg bg-gradient-to-br from-sky-500 to-indigo-600" />
<span className="text-sm font-semibold tracking-tight">
Birdor Studio
</span>
</div>
<nav className="hidden items-center gap-4 text-sm font-medium md:flex">
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"text-muted-foreground hover:text-foreground",
item.active && "text-foreground"
)}
>
{item.label}
</Link>
))}
</nav>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" className="hidden md:inline-flex">
文档
</Button>
<Button size="sm">控制台</Button>
</div>
</div>
</header>
)
}
使用:
<TopNav
items={[
{ label: "概览", href: "/", active: true },
{ label: "项目", href: "/projects" },
{ label: "账单", href: "/billing" },
]}
/>
B.8.5 Select 下拉选择组件
Select 是表单中最复杂的交互组件之一,需要处理展开/收起、选项列表、键盘导航和选中状态。
// src/components/ui/select.tsx
import * as React from "react"
import { ChevronDown, Check } from "lucide-react"
import { cn } from "@/lib/cn"
interface SelectOption {
value: string
label: string
disabled?: boolean
}
interface SelectProps {
options: SelectOption[]
value?: string
onChange?: (value: string) => void
placeholder?: string
disabled?: boolean
className?: string
}
export function Select({
options,
value,
onChange,
placeholder = "请选择...",
disabled,
className,
}: SelectProps) {
const [open, setOpen] = React.useState(false)
const [selected, setSelected] = React.useState(value)
const handleSelect = (val: string) => {
setSelected(val)
onChange?.(val)
setOpen(false)
}
const selectedLabel = options.find((o) => o.value === selected)?.label
return (
<div className={cn("relative", className)}>
<button
type="button"
onClick={() => !disabled && setOpen(!open)}
disabled={disabled}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm",
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
open && "ring-2 ring-ring ring-offset-2"
)}
>
<span className={cn(!selectedLabel && "text-muted-foreground")}>
{selectedLabel || placeholder}
</span>
<ChevronDown className={cn("h-4 w-4 shrink-0 transition-transform", open && "rotate-180")} />
</button>
{open && (
<div className="absolute z-50 mt-1 w-full rounded-md border bg-background shadow-lg">
<ul className="max-h-60 overflow-auto py-1">
{options.map((option) => (
<li
key={option.value}
onClick={() => !option.disabled && handleSelect(option.value)}
className={cn(
"flex cursor-pointer items-center justify-between px-3 py-2 text-sm",
"hover:bg-accent hover:text-accent-foreground",
option.disabled && "cursor-not-allowed opacity-50",
selected === option.value && "bg-accent text-accent-foreground"
)}
>
<span>{option.label}</span>
{selected === option.value && <Check className="h-4 w-4" />}
</li>
))}
</ul>
</div>
)}
</div>
)
}
用法示例:
const options = [
{ value: "active", label: "正常" },
{ value: "pending", label: "待审核" },
{ value: "suspended", label: "已停用" },
]
<Select
options={options}
value={status}
onChange={setStatus}
placeholder="选择状态"
/>
B.8.6 Tooltip 与 Popover
悬浮提示和下拉菜单是后台系统的必备交互元素。
// Tooltip 基础封装
import * as React from "react"
import * as RadixTooltip from "@radix-ui/react-tooltip"
import { cn } from "@/lib/cn"
export function TooltipProvider({ children }: { children: React.ReactNode }) {
return <RadixTooltip.Provider delayDuration={200}>{children}</RadixTooltip.Provider>
}
export function Tooltip({
children,
content,
side = "top",
}: {
children: React.ReactNode
content: string
side?: "top" | "bottom" | "left" | "right"
}) {
return (
<RadixTooltip.Root>
<RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>
<RadixTooltip.Portal>
<RadixTooltip.Content
side={side}
className={cn(
"z-50 rounded-md bg-foreground px-3 py-1.5 text-xs text-background shadow-md",
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out"
)}
>
{content}
<RadixTooltip.Arrow className="fill-foreground" />
</RadixTooltip.Content>
</RadixTooltip.Portal>
</RadixTooltip.Root>
)
}
B.9 PageShell:SaaS 控制台页面骨架
一个典型的布局:
- 顶部导航
- 左侧菜单
- 右侧主内容
- 响应式收缩
这里只给主要结构及 Tailwind 写法,便于你跟掌机业务或 Birdor 工具后台直接对接。
// src/components/layout/app-shell.tsx
import { ReactNode } from "react"
import { cn } from "@/lib/cn"
interface AppShellProps {
sidebar: ReactNode
header?: ReactNode
children: ReactNode
}
export function AppShell({ sidebar, header, children }: AppShellProps) {
return (
<div className="flex h-screen bg-background">
{/* Sidebar */}
<aside className="hidden w-60 border-r bg-muted/40 md:flex md:flex-col">
{sidebar}
</aside>
{/* Main */}
<div className="flex min-w-0 flex-1 flex-col">
{header && (
<header className="border-b bg-background">
<div className="mx-auto flex h-14 max-w-6xl items-center px-4">
{header}
</div>
</header>
)}
<main className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-6xl px-4 py-6">{children}</div>
</main>
</div>
</div>
)
}
用法示例:
<AppShell
sidebar={<YourSidebar />}
header={<div className="text-sm font-medium">项目概览</div>}
>
{/* 页面主体内容 */}
<DashboardContent />
</AppShell>
B.10 小结:如何把这些组件变成”你自己的 shadcn/ui”
你可以按下面思路整理成「企业内部 UI 库」:
Design Token 设计体系
一套成熟的 UI 库不只是组件堆砌,更需要底层的 Design Token 支撑:
| Token 类型 | 示例 | 说明 |
|---|---|---|
| Colors | --primary-500, --destructive | 品牌色、功能色、语义色 |
| Spacing | --space-1 ~ --space-16 | 4px 为基准的间距系统 |
| Typography | --font-sans, --text-sm | 字体族 + 字号层级 |
| Shadows | --shadow-sm, --shadow-lg | 层级感 |
| Border Radius | --radius-md, --radius-lg | 品牌调性 |
/* tailwind.config.ts 中的 theme 扩展 */
theme: {
extend: {
colors: {
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
// 与你的品牌色对齐
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
},
},
}
Monorepo 目录结构
packages/ui/
├── src/
│ ├── components/
│ │ ├── ui/ # 基础组件
│ │ │ ├── button.tsx
│ │ │ ├── input.tsx
│ │ │ ├── card.tsx
│ │ │ └── ...
│ │ └── layout/ # 布局组件
│ │ ├── app-shell.tsx
│ │ └── top-nav.tsx
│ ├── lib/
│ │ └── utils.ts # cn、variants 等工具
│ └── styles/
│ └── globals.css # Tailwind + CSS Variables
├── package.json
├── tailwind.config.ts
└── tsconfig.json
barrel 导出
// packages/ui/src/components/ui/index.ts
export { Button, buttonVariants } from './button'
export { Input } from './input'
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './card'
export { Badge, badgeVariants } from './badge'
export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from './dialog'
export { FormField } from './form-field'
export { SimpleTable, type Column } from './simple-table'
发布与使用
# 构建 UI 包
pnpm build
# 发布到私有 registry
pnpm publish --registry https://npm.your-company.com
# 项目中使用
import { Button, Card, Input } from '@birdor/ui'
B.11 组件库的工程化实践
测试策略
组件库的可靠性需要通过测试来保证:
| 测试类型 | 工具 | 覆盖率目标 |
|---|---|---|
| 单元测试 | Vitest / Jest | 核心逻辑 80%+ |
| 视觉回归 | Chromatic / Storybook | 关键组件 |
| 可访问性 | axe-core | WCAG 2.1 AA |
| 交互测试 | Testing Library | 用户操作流程 |
文档站点
使用 Storybook 作为组件文档站点:
npx storybook@latest init
每个组件编写 Stories 展示不同变体:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './button'
const meta: Meta<typeof Button> = {
component: Button,
tags: ['autodocs'],
}
export default meta
type Story = StoryObj<typeof Button>
export const Primary: Story = { args: { variant: 'default', children: '主按钮' } }
export const Outline: Story = { args: { variant: 'outline', children: '边框按钮' } }
export const Loading: Story = { args: { loading: true, children: '加载中' } }
export const Disabled: Story = { args: { disabled: true, children: '禁用' } }
版本发布与变更日志
# 使用 Changesets 管理版本
npx changeset
# 选择受影响包和变更类型 (major/minor/patch)
# 填写变更描述
# 版本提升和 changelog 生成
npx changeset version
# 发布
npx changeset publish
总结
Tailwind CSS 与 shadcn/ui 的组合代表了现代前端组件库的新范式:原子化 CSS 提供了极致的开发体验和运行时性能,Radix UI 的无头组件保证了可访问性和行为正确性,而开发者只需要专注于视觉层和主题的定制。从几个基础组件开始(Button、Input、Dialog),逐步扩展到自己的设计体系,是中小型团队构建一致 UI 体验的最佳路径。这套方案的核心优势在于零运行时开销、完全可控的样式和良好的 TypeScript 支持。
如果你愿意,我可以继续帮你:
- 把这些组件整理成一个完整的
ui目录结构清单 + barrel 文件 - 或者直接设计一套**”Birdor Admin UI Kit”**,按你现在的掌机 / Birdor 工具站 / Shunhei 品牌统一风格来定制 Tailwind 主题(颜色、圆角、阴影、排版),再配一套组件库代码骨架。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。