Design System 重构完成

This commit is contained in:
2026-02-10 03:54:09 +08:00
parent fe5e8533b5
commit 73d0b0d5fe
51 changed files with 4915 additions and 8 deletions

View File

@@ -15,6 +15,8 @@
"@prisma/client": "^7.2.0",
"bcryptjs": "^3.0.3",
"better-auth": "^1.4.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"lucide-react": "^0.562.0",
"next": "16.1.1",
@@ -23,6 +25,7 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"unstorage": "^1.17.3",
"zod": "^4.3.5"
},

27
pnpm-lock.yaml generated
View File

@@ -24,6 +24,12 @@ importers:
better-auth:
specifier: ^1.4.10
version: 1.4.10(@prisma/client@7.2.0(prisma@7.2.0(@types/react@19.2.7)(better-sqlite3@12.5.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.5.0)(drizzle-orm@0.33.0(@electric-sql/pglite@0.3.2)(@prisma/client@5.22.0(prisma@7.2.0(@types/react@19.2.7)(better-sqlite3@12.5.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(@types/pg@8.15.6)(@types/react@19.2.7)(better-sqlite3@12.5.0)(kysely@0.28.8)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.2.0(@types/react@19.2.7)(better-sqlite3@12.5.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react@19.2.3))(mysql2@3.15.3)(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(pg@8.16.3)(prisma@7.2.0(@types/react@19.2.7)(better-sqlite3@12.5.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
dotenv:
specifier: ^17.2.3
version: 17.2.3
@@ -48,6 +54,9 @@ importers:
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
unstorage:
specifier: ^1.17.3
version: 1.17.3
@@ -1480,9 +1489,16 @@ packages:
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -3024,6 +3040,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
tailwindcss@4.1.18:
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
@@ -4778,8 +4797,14 @@ snapshots:
dependencies:
consola: 3.4.2
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
client-only@0.0.1: {}
clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -6407,6 +6432,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
tailwind-merge@3.4.0: {}
tailwindcss@4.1.18: {}
tapable@2.3.0: {}

View File

@@ -1,20 +1,154 @@
@import "tailwindcss";
/**
* Design System CSS 变量
*
* 定义全局 CSS 变量用于主题切换和动态样式
*/
:root {
/* 颜色系统 */
--color-primary-50: #f0f9f8;
--color-primary-100: #e0f2f0;
--color-primary-200: #bce6e1;
--color-primary-300: #8dd4cc;
--color-primary-400: #5ec2b7;
--color-primary-500: #35786f;
--color-primary-600: #2a605b;
--color-primary-700: #1f4844;
--color-primary-800: #183835;
--color-primary-900: #122826;
--color-primary-950: #0a1413;
/* 语义色 */
--color-success-500: #22c55e;
--color-warning-500: #f59e0b;
--color-error-500: #ef4444;
--color-info-500: #3b82f6;
/* 基础颜色 */
--background: #ffffff;
--foreground: #171717;
--foreground: #111827;
--foreground-secondary: #4b5563;
--foreground-tertiary: #6b7280;
--foreground-disabled: #9ca3af;
/* 背景 */
--background-secondary: #f3f4f6;
--background-tertiary: #e5e7eb;
/* 边框 */
--border: #d1d5db;
--border-secondary: #e5e7eb;
--border-focus: #35786f;
/* 圆角 */
--radius-sm: 0.125rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
--radius-full: 9999px;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
--shadow-primary: 0 4px 14px 0 rgba(53, 120, 111, 0.39);
/* 间距 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* 过渡 */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
}
/**
* 全局基础样式
*/
* {
box-sizing: border-box;
}
html {
height: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
height: 100%;
margin: 0;
padding: 0;
background: var(--background);
color: var(--foreground);
font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
font-family: var(--font-geist-sans), -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-size: 1rem;
line-height: 1.5;
text-rendering: optimizeLegibility;
}
.code-block {
font-family: var(--font-geist-mono), monospace;
/**
* 代码块字体
*/
.code-block,
code,
kbd,
pre,
samp {
font-family: var(--font-geist-mono), ui-monospace, SFMono-Regular, Monaco, Consolas, monospace;
}
/**
* 导航栏按钮样式
*/
.navbar-btn {
@apply border-0 bg-transparent hover:bg-black/30 shadow-none;
transition: background-color var(--transition-fast);
}
/**
* 焦点可见性优化
*/
:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
/**
* 选择文本样式
*/
::selection {
background-color: var(--color-primary-200);
color: var(--color-primary-900);
}
/**
* 滚动条样式
*/
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--background-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--foreground-tertiary);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--foreground-secondary);
}

585
src/design-system/README.md Normal file
View File

@@ -0,0 +1,585 @@
# Design System
完整的设计系统,提供可复用的 UI 组件和设计令牌,确保整个应用的一致性。
## 目录结构
```
src/design-system/
├── tokens/ # 设计令牌(颜色、间距、字体等)
├── lib/ # 工具函数
├── base/ # 基础组件
│ ├── button/
│ ├── input/
│ ├── textarea/
│ ├── card/
│ ├── checkbox/
│ ├── radio/
│ ├── switch/
│ └── select/
├── feedback/ # 反馈组件
│ ├── alert/
│ ├── progress/
│ ├── skeleton/
│ └── toast/
├── overlay/ # 覆盖组件
│ └── modal/
├── data-display/ # 数据展示组件
│ ├── badge/
│ └── divider/
├── layout/ # 布局组件
│ ├── container/
│ ├── grid/
│ └── stack/
├── navigation/ # 导航组件
│ └── tabs/
└── index.ts # 统一导出
```
## 快速开始
### 安装依赖
```bash
pnpm add class-variance-authority clsx tailwind-merge
```
### 导入组件
```tsx
// 方式 1: 从主入口导入(简单但 tree-shaking 较差)
import { Button, Input, Card } from '@/design-system';
// 方式 2: 从子路径导入(更好的 tree-shaking
import { Button } from '@/design-system/base/button';
import { Input } from '@/design-system/base/input';
import { Card } from '@/design-system/base/card';
```
### 使用组件
```tsx
import { Button, Card } from '@/design-system';
export function MyComponent() {
return (
<Card>
<h1>标题</h1>
<p>内容</p>
<Button variant="primary">点击我</Button>
</Card>
);
}
```
## 组件列表
### 基础组件
| 组件 | 说明 | 状态 |
|------|------|------|
| [Button](#button) | 按钮 | ✅ |
| [Input](#input) | 输入框 | ✅ |
| [Textarea](#textarea) | 多行文本输入 | ✅ |
| [Card](#card) | 卡片容器 | ✅ |
| [Checkbox](#checkbox) | 复选框 | ✅ |
| [Radio](#radio) | 单选按钮 | ✅ |
| [Switch](#switch) | 开关 | ✅ |
| [Select](#select) | 下拉选择框 | ✅ |
### 反馈组件
| 组件 | 说明 | 状态 |
|------|------|------|
| [Alert](#alert) | 警告提示 | ✅ |
| [Progress](#progress) | 进度条 | ✅ |
| [Skeleton](#skeleton) | 骨架屏 | ✅ |
| [Toast](#toast) | 通知提示 | ✅ |
### 覆盖组件
| 组件 | 说明 | 状态 |
|------|------|------|
| [Modal](#modal) | 模态框 | ✅ |
### 数据展示组件
| 组件 | 说明 | 状态 |
|------|------|------|
| [Badge](#badge) | 徽章 | ✅ |
| [Divider](#divider) | 分隔线 | ✅ |
### 布局组件
| 组件 | 说明 | 状态 |
|------|------|------|
| [Container](#container) | 容器 | ✅ |
| [Grid](#grid) | 网格布局 | ✅ |
| [Stack](#stack) | 堆叠布局 | ✅ |
### 导航组件
| 组件 | 说明 | 状态 |
|------|------|------|
| [Tabs](#tabs) | 标签页 | ✅ |
## 组件 API
### Button
按钮组件,支持多种变体和尺寸。
```tsx
import { Button } from '@/design-system';
<Button variant="primary" size="md" onClick={handleClick}>
点击我
</Button>
```
**变体 (variant)**: `primary` | `secondary` | `success` | `warning` | `error` | `ghost` | `outline` | `link`
**尺寸 (size)**: `sm` | `md` | `lg`
**快捷组件**: `PrimaryButton`, `SecondaryButton`, `SuccessButton`, `WarningButton`, `ErrorButton`, `GhostButton`, `OutlineButton`, `LinkButton`
### Input
输入框组件。
```tsx
import { Input } from '@/design-system';
<Input
variant="bordered"
placeholder="请输入内容"
error={hasError}
/>
```
**变体 (variant)**: `default` | `bordered` | `filled` | `search`
**尺寸 (size)**: `sm` | `md` | `lg`
### Textarea
多行文本输入组件。
```tsx
import { Textarea } from '@/design-system';
<Textarea
variant="bordered"
placeholder="请输入内容"
rows={4}
/>
```
**变体 (variant)**: `default` | `bordered` | `filled`
### Card
卡片容器组件。
```tsx
import { Card, CardHeader, CardTitle, CardBody, CardFooter } from '@/design-system';
<Card>
<CardHeader>
<CardTitle>标题</CardTitle>
</CardHeader>
<CardBody>
<p>内容</p>
</CardBody>
<CardFooter>
<Button>确定</Button>
</CardFooter>
</Card>
```
**变体 (variant)**: `default` | `bordered` | `elevated` | `flat`
**内边距 (padding)**: `none` | `xs` | `sm` | `md` | `lg` | `xl`
### Checkbox
复选框组件。
```tsx
import { Checkbox } from '@/design-system';
<Checkbox checked={checked} onChange={setChecked}>
同意条款
</Checkbox>
```
### Radio
单选按钮组件。
```tsx
import { Radio, RadioGroup } from '@/design-system';
<RadioGroup name="choice" value={value} onChange={setValue}>
<Radio value="1">选项 1</Radio>
<Radio value="2">选项 2</Radio>
</RadioGroup>
```
### Switch
开关组件。
```tsx
import { Switch } from '@/design-system';
<Switch checked={enabled} onChange={setEnabled} />
```
### Alert
警告提示组件。
```tsx
import { Alert } from '@/design-system';
<Alert variant="success" title="成功">
操作成功完成
</Alert>
```
**变体 (variant)**: `info` | `success` | `warning` | `error`
### Progress
进度条组件。
```tsx
import { Progress } from '@/design-system';
<Progress value={60} showLabel />
```
### Skeleton
骨架屏组件。
```tsx
import { Skeleton, TextSkeleton, CardSkeleton } from '@/design-system';
<Skeleton className="h-4 w-32" />
<TextSkeleton lines={3} />
<CardSkeleton />
```
### Toast
通知提示组件(基于 sonner
```tsx
import { toast } from '@/design-system';
toast.success("操作成功!");
toast.error("发生错误");
toast.promise(promise, {
loading: "加载中...",
success: "加载成功",
error: "加载失败",
});
```
### Modal
模态框组件。
```tsx
import { Modal } from '@/design-system';
<Modal open={open} onClose={() => setOpen(false)}>
<Modal.Header>
<Modal.Title>标题</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>内容</p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setOpen(false)}>
取消
</Button>
<Button variant="primary">确定</Button>
</Modal.Footer>
</Modal>
```
### Badge
徽章组件。
```tsx
import { Badge } from '@/design-system';
<Badge variant="success">成功</Badge>
<Badge dot />
```
**变体 (variant)**: `default` | `primary` | `success` | `warning` | `error` | `info`
### Divider
分隔线组件。
```tsx
import { Divider } from '@/design-system';
<Divider />
<Divider>或者</Divider>
<Divider orientation="vertical" />
```
### Container
容器组件。
```tsx
import { Container } from '@/design-system';
<Container size="lg" padding="xl">
<p>内容</p>
</Container>
```
### Grid
网格布局组件。
```tsx
import { Grid } from '@/design-system';
<Grid cols={3} gap={4}>
<div>项目 1</div>
<div>项目 2</div>
<div>项目 3</div>
</Grid>
```
### Stack
堆叠布局组件。
```tsx
import { Stack, VStack, HStack } from '@/design-system';
<VStack gap={4}>
<div>项目 1</div>
<div>项目 2</div>
</VStack>
```
### Tabs
标签页组件。
```tsx
import { Tabs } from '@/design-system';
<Tabs value={activeTab} onValueChange={setActiveTab}>
<Tabs.List>
<Tabs.Trigger value="tab1">标签 1</Tabs.Trigger>
<Tabs.Trigger value="tab2">标签 2</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">
<p>内容 1</p>
</Tabs.Content>
<Tabs.Content value="tab2">
<p>内容 2</p>
</Tabs.Content>
</Tabs>
```
## 设计令牌
### 颜色
```tsx
import { colors } from '@/design-system/tokens';
// 主色
colors.primary.500 // #35786f
// 语义色
colors.success.500 // #22c55e
colors.warning.500 // #f59e0b
colors.error.500 // #ef4444
colors.info.500 // #3b82f6
```
在组件中使用:
```tsx
<div className="bg-primary-500 text-white">主色背景</div>
<div className="text-success-600">成功文本</div>
```
### 间距
基于 8pt 网格系统:
```tsx
<div className="p-4"> // 16px
<div className="p-6"> // 24px
<div className="p-8"> // 32px
```
### 字体
```tsx
<div className="text-sm">小文本</div>
<div className="text-base">正常文本</div>
<div className="text-lg">大文本</div>
<div className="font-semibold">半粗体</div>
<div className="font-bold">粗体</div>
```
### 圆角
```tsx
<div className="rounded-lg"> // 8px
<div className="rounded-xl"> // 12px
<div className="rounded-2xl"> // 16px
```
### 阴影
```tsx
<div className="shadow-sm"> // 小阴影
<div className="shadow-md"> // 中阴影
<div className="shadow-lg"> // 大阴影
<div className="shadow-xl"> // 超大阴影
```
## 工具函数
### cn
合并 Tailwind CSS 类名的工具函数。
```tsx
import { cn } from '@/design-system';
const className = cn(
'base-class',
isActive && 'active-class',
'another-class'
);
```
## 最佳实践
### 1. 组件导入
对于更好的 tree-shaking建议从子路径导入
```tsx
// ✅ 推荐
import { Button } from '@/design-system/base/button';
// ❌ 不推荐(但也可以)
import { Button } from '@/design-system';
```
### 2. 样式覆盖
使用 `className` 属性覆盖样式:
```tsx
<Button className="w-full">全宽按钮</Button>
```
### 3. 组合组件
利用组件组合来构建复杂 UI
```tsx
<Card>
<CardHeader>
<CardTitle>标题</CardTitle>
</CardHeader>
<CardBody>
<VStack gap={4}>
<Input placeholder="输入框" />
<Button>提交</Button>
</VStack>
</CardBody>
</Card>
```
### 4. 可访问性
所有组件都内置了可访问性支持:
- 正确的 ARIA 属性
- 键盘导航支持
- 焦点管理
- 屏幕阅读器友好
## 迁移指南
### 从旧组件迁移
旧的组件路径:
```tsx
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
```
新的组件路径:
```tsx
import { Button } from '@/design-system/base/button';
import { Input } from '@/design-system/base/input';
```
### API 变化
大部分 API 保持兼容,但有以下变化:
1. **颜色不再使用硬编码值**
```tsx
// 旧
style={{ backgroundColor: '#35786f' }}
// 新
className="bg-primary-500"
```
2. **变体命名更加一致**
```tsx
// 旧
<Button variant="icon" />
// 新
<Button variant="ghost" />
```
3. **新增语义色变体**
```tsx
<Button variant="success">成功</Button>
<Button variant="warning">警告</Button>
<Button variant="error">错误</Button>
```
## 贡献
添加新组件时,请遵循以下规范:
1. 在对应的目录下创建组件
2. 使用 `cva` 定义变体样式
3. 使用 `forwardRef` 支持 ref 转发
4. 添加完整的 TypeScript 类型
5. 编写详细的 JSDoc 注释和示例
6. 在导出文件中添加导出
## 许可证
AGPL-3.0-only

View File

@@ -0,0 +1,273 @@
"use client";
import React from "react";
import Link from "next/link";
import Image from "next/image";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Button 组件
*
* Design System 中的按钮组件,支持多种变体、尺寸和状态。
* 自动处理 Link/button 切换,支持图标和加载状态。
*
* @example
* ```tsx
* // Primary 按钮
* <Button variant="primary" onClick={handleClick}>
* 点击我
* </Button>
*
* // 带图标的按钮
* <Button variant="secondary" leftIcon={<Icon />}>
* 带图标
* </Button>
*
* // 作为链接使用
* <Button variant="primary" href="/path">
* 链接按钮
* </Button>
*
* // 加载状态
* <Button variant="primary" loading>
* 提交中...
* </Button>
* ```
*/
/**
* 按钮变体样式
*/
const buttonVariants = cva(
// 基础样式
"inline-flex items-center justify-center gap-2 rounded-xl font-semibold shadow transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary: "bg-primary-500 text-white hover:bg-primary-600 shadow-md",
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 shadow-sm",
success: "bg-success-500 text-white hover:bg-success-600 shadow-md",
warning: "bg-warning-500 text-white hover:bg-warning-600 shadow-md",
error: "bg-error-500 text-white hover:bg-error-600 shadow-md",
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 shadow-none",
outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50 shadow-none",
link: "text-primary-500 hover:text-primary-600 hover:underline shadow-none px-0",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-6 text-lg",
},
fullWidth: {
true: "w-full",
false: "",
},
},
compoundVariants: [
// 链接变体不应用高度和圆角
{
variant: "link",
size: "sm",
className: "h-auto px-0",
},
{
variant: "link",
size: "md",
className: "h-auto px-0",
},
{
variant: "link",
size: "lg",
className: "h-auto px-0",
},
],
defaultVariants: {
variant: "secondary",
size: "md",
fullWidth: false,
},
}
);
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
// 内容
children?: React.ReactNode;
// 导航
href?: string;
openInNewTab?: boolean;
// 图标
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
iconSrc?: string; // For Next.js Image icons
iconAlt?: string;
// 状态
loading?: boolean;
selected?: boolean;
// 样式
className?: string;
}
/**
* Button 组件
*/
export function Button({
variant = "secondary",
size = "md",
fullWidth = false,
href,
openInNewTab = false,
iconSrc,
iconAlt,
leftIcon,
rightIcon,
children,
className,
loading = false,
selected = false,
disabled,
type = "button",
...props
}: ButtonProps) {
// 计算样式
const computedClass = cn(
buttonVariants({ variant, size, fullWidth }),
selected && variant === "secondary" && "bg-gray-200",
className
);
// 图标尺寸映射
const iconSize = { sm: 14, md: 16, lg: 20 }[size];
// 渲染 SVG 图标
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
if (!icon) return null;
return (
<span className={`flex items-center shrink-0 ${position === "left" ? "-ml-1 mr-2" : "-mr-1 ml-2"}`}>
{icon}
</span>
);
};
// 渲染 Next.js Image 图标
const renderImageIcon = () => {
if (!iconSrc) return null;
return (
<Image
src={iconSrc}
width={iconSize}
height={iconSize}
alt={iconAlt || "icon"}
className="shrink-0"
/>
);
};
// 渲染加载图标
const renderLoadingIcon = () => {
if (!loading) return null;
return (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
};
// 组装内容
const content = (
<>
{loading && renderLoadingIcon()}
{renderImageIcon()}
{renderSvgIcon(leftIcon, "left")}
{children}
{renderSvgIcon(rightIcon, "right")}
</>
);
// 如果提供了 href渲染为 Link
if (href) {
return (
<Link
href={href}
className={computedClass}
target={openInNewTab ? "_blank" : undefined}
rel={openInNewTab ? "noopener noreferrer" : undefined}
>
{content}
</Link>
);
}
// 否则渲染为 button
return (
<button
type={type}
disabled={disabled || loading}
className={computedClass}
{...props}
>
{content}
</button>
);
}
/**
* 预定义的按钮快捷组件
*/
export const PrimaryButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="primary" {...props} />
);
export const SecondaryButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="secondary" {...props} />
);
export const SuccessButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="success" {...props} />
);
export const WarningButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="warning" {...props} />
);
export const ErrorButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="error" {...props} />
);
export const GhostButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="ghost" {...props} />
);
export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="outline" {...props} />
);
export const LinkButton = (props: Omit<ButtonProps, "variant">) => (
<Button variant="link" {...props} />
);

View File

@@ -0,0 +1 @@
export * from './button';

View File

@@ -0,0 +1,198 @@
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Card 卡片组件
*
* Design System 中的卡片容器组件,提供统一的内容包装样式。
*
* @example
* ```tsx
* // 默认卡片
* <Card>
* <p>卡片内容</p>
* </Card>
*
* // 带边框的卡片
* <Card variant="bordered" padding="lg">
* <p>带边框的内容</p>
* </Card>
*
* // 无内边距卡片
* <Card padding="none">
* <img src="image.jpg" alt="完全填充的图片" />
* </Card>
*
* // 可点击的卡片
* <Card clickable onClick={handleClick}>
* <p>点击我</p>
* </Card>
* ```
*/
/**
* 卡片变体样式
*/
const cardVariants = cva(
// 基础样式
"rounded-2xl bg-white transition-all duration-250",
{
variants: {
variant: {
default: "shadow-xl",
bordered: "border-2 border-gray-200 shadow-sm",
elevated: "shadow-2xl",
flat: "border border-gray-200 shadow-none",
},
padding: {
none: "",
xs: "p-3",
sm: "p-4",
md: "p-6",
lg: "p-8",
xl: "p-10",
},
clickable: {
true: "cursor-pointer hover:shadow-primary/25 hover:-translate-y-0.5 active:translate-y-0",
false: "",
},
},
defaultVariants: {
variant: "default",
padding: "md",
clickable: false,
},
}
);
export type CardVariant = VariantProps<typeof cardVariants>["variant"];
export type CardPadding = VariantProps<typeof cardVariants>["padding"];
export interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {
// 子元素
children: React.ReactNode;
}
/**
* Card 卡片组件
*/
export function Card({
variant = "default",
padding = "md",
clickable = false,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(cardVariants({ variant, padding, clickable }), className)}
{...props}
>
{children}
</div>
);
}
/**
* CardSection - 卡片内容区块
* 用于组织卡片内部的多个内容区块
*/
export interface CardSectionProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
noPadding?: boolean;
}
export function CardSection({
noPadding = false,
className,
children,
...props
}: CardSectionProps) {
return (
<div
className={cn(
!noPadding && "p-6",
"first:rounded-t-2xl last:rounded-b-2xl",
className
)}
{...props}
>
{children}
</div>
);
}
/**
* CardHeader - 卡片头部
*/
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function CardHeader({
className,
children,
...props
}: CardHeaderProps) {
return (
<div
className={cn("flex items-center justify-between p-6 border-b border-gray-200", className)}
{...props}
>
{children}
</div>
);
}
/**
* CardTitle - 卡片标题
*/
export interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
children: React.ReactNode;
}
export function CardTitle({
className,
children,
...props
}: CardTitleProps) {
return (
<h3
className={cn("text-lg font-semibold text-gray-900", className)}
{...props}
>
{children}
</h3>
);
}
/**
* CardBody - 卡片主体
*/
export const CardBody = CardSection;
/**
* CardFooter - 卡片底部
*/
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function CardFooter({
className,
children,
...props
}: CardFooterProps) {
return (
<div
className={cn("flex items-center justify-end gap-2 p-6 border-t border-gray-200", className)}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './card';

View File

@@ -0,0 +1,170 @@
"use client";
import React, { forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Checkbox 复选框组件
*
* Design System 中的复选框组件,支持多种状态和尺寸。
*
* @example
* ```tsx
* // 默认复选框
* <Checkbox>同意条款</Checkbox>
*
* // 受控组件
* <Checkbox checked={checked} onChange={handleChange}>
* 同意条款
* </Checkbox>
*
* // 错误状态
* <Checkbox error>必选项</Checkbox>
* ```
*/
/**
* 复选框变体样式
*/
const checkboxVariants = cva(
// 基础样式
"peer h-4 w-4 shrink-0 rounded border-2 transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
variant: {
default: "border-gray-300 checked:bg-primary-500 checked:border-primary-500",
success: "border-gray-300 checked:bg-success-500 checked:border-success-500",
warning: "border-gray-300 checked:bg-warning-500 checked:border-warning-500",
error: "border-gray-300 checked:bg-error-500 checked:border-error-500",
},
size: {
sm: "h-3.5 w-3.5",
md: "h-4 w-4",
lg: "h-5 w-5",
},
error: {
true: "border-error-500",
false: "",
},
},
defaultVariants: {
variant: "default",
size: "md",
error: false,
},
}
);
export type CheckboxVariant = VariantProps<typeof checkboxVariants>["variant"];
export type CheckboxSize = VariantProps<typeof checkboxVariants>["size"];
export interface CheckboxProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
VariantProps<typeof checkboxVariants> {
// 标签文本
label?: React.ReactNode;
// 标签位置
labelPosition?: "left" | "right";
// 自定义复选框类名
checkboxClassName?: string;
}
/**
* Checkbox 复选框组件
*/
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(
{
variant = "default",
size = "md",
error = false,
label,
labelPosition = "right",
className,
checkboxClassName,
disabled,
...props
},
ref
) => {
const checkboxId = React.useId();
const renderCheckbox = () => (
<input
ref={ref}
type="checkbox"
id={checkboxId}
disabled={disabled}
className={cn(
checkboxVariants({ variant, size, error }),
checkboxClassName
)}
{...props}
/>
);
const renderLabel = () => {
if (!label) return null;
return (
<label
htmlFor={checkboxId}
className={cn(
"text-base font-normal leading-none",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
labelPosition === "left" ? "mr-2" : "ml-2"
)}
>
{label}
</label>
);
};
if (!label) {
return renderCheckbox();
}
return (
<div className={cn("inline-flex items-center", className)}>
{labelPosition === "left" && renderLabel()}
{renderCheckbox()}
{labelPosition === "right" && renderLabel()}
</div>
);
}
);
Checkbox.displayName = "Checkbox";
/**
* CheckboxGroup - 复选框组
*/
export interface CheckboxGroupProps {
children: React.ReactNode;
label?: string;
error?: string;
required?: boolean;
className?: string;
}
export function CheckboxGroup({
children,
label,
error,
required,
className,
}: CheckboxGroupProps) {
return (
<div className={cn("space-y-2", className)}>
{label && (
<div className="text-base font-medium text-gray-900">
{label}
{required && <span className="text-error-500 ml-1">*</span>}
</div>
)}
<div className="space-y-2">{children}</div>
{error && <p className="text-sm text-error-500">{error}</p>}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './checkbox';

View File

@@ -0,0 +1 @@
export * from './input';

View File

@@ -0,0 +1,151 @@
"use client";
import React, { forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Input 输入框组件
*
* Design System 中的输入框组件,支持多种样式变体和尺寸。
* 完全可访问,支持焦点状态和错误状态。
*
* @example
* ```tsx
* // 默认样式
* <Input placeholder="请输入内容" />
*
* // 带边框样式
* <Input variant="bordered" placeholder="带边框的输入框" />
*
* // 填充样式
* <Input variant="filled" placeholder="填充背景的输入框" />
*
* // 错误状态
* <Input variant="bordered" error placeholder="有错误的输入框" />
*
* // 禁用状态
* <Input disabled placeholder="禁用的输入框" />
* ```
*/
/**
* 输入框变体样式
*/
const inputVariants = cva(
// 基础样式
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
variant: {
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
bordered: "border-gray-300 bg-white",
filled: "border-transparent bg-gray-100",
search: "border-gray-200 bg-white pl-10 rounded-full",
},
size: {
sm: "h-9 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-5 text-lg",
},
error: {
true: "border-error-500 focus-visible:ring-error-500",
false: "",
},
},
compoundVariants: [
// 填充变体的错误状态
{
variant: "filled",
error: true,
className: "bg-error-50",
},
],
defaultVariants: {
variant: "default",
size: "md",
error: false,
},
}
);
export type InputVariant = VariantProps<typeof inputVariants>["variant"];
export type InputSize = VariantProps<typeof inputVariants>["size"];
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
VariantProps<typeof inputVariants> {
// 左侧图标(通常用于搜索框)
leftIcon?: React.ReactNode;
// 右侧图标(例如清除按钮)
rightIcon?: React.ReactNode;
// 容器类名(用于包裹图标和输入框)
containerClassName?: string;
}
/**
* Input 输入框组件
*/
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
variant = "default",
size = "md",
error = false,
className,
containerClassName,
leftIcon,
rightIcon,
type = "text",
...props
},
ref
) => {
// 如果有左侧图标,使用相对定位的容器
if (leftIcon) {
return (
<div className={cn("relative", containerClassName)}>
{/* 左侧图标 */}
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
{leftIcon}
</div>
{/* 输入框 */}
<input
ref={ref}
type={type}
className={cn(
inputVariants({ variant, size, error }),
leftIcon && "pl-10"
)}
{...props}
/>
{/* 右侧图标 */}
{rightIcon && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
{rightIcon}
</div>
)}
</div>
);
}
// 普通输入框
return (
<div className={cn("relative", containerClassName)}>
<input
ref={ref}
type={type}
className={cn(inputVariants({ variant, size, error }), className)}
{...props}
/>
{rightIcon && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
{rightIcon}
</div>
)}
</div>
);
}
);
Input.displayName = "Input";

View File

@@ -0,0 +1 @@
export * from './radio';

View File

@@ -0,0 +1,219 @@
"use client";
import React, { forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Radio 单选按钮组件
*
* Design System 中的单选按钮组件,支持多种状态和尺寸。
*
* @example
* ```tsx
* // 默认单选按钮
* <Radio name="choice" value="1">选项 1</Radio>
* <Radio name="choice" value="2">选项 2</Radio>
*
* // 受控组件
* <Radio
* name="choice"
* value="1"
* checked={value === "1"}
* onChange={(e) => setValue(e.target.value)}
* >
* 选项 1
* </Radio>
* ```
*/
/**
* 单选按钮变体样式
*/
const radioVariants = cva(
// 基础样式
"peer h-4 w-4 shrink-0 rounded-full border-2 transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 appearance-none cursor-pointer",
{
variants: {
variant: {
default: "border-gray-300 checked:border-primary-500",
success: "border-gray-300 checked:border-success-500",
warning: "border-gray-300 checked:border-warning-500",
error: "border-gray-300 checked:border-error-500",
},
size: {
sm: "h-3.5 w-3.5",
md: "h-4 w-4",
lg: "h-5 w-5",
},
error: {
true: "border-error-500",
false: "",
},
},
defaultVariants: {
variant: "default",
size: "md",
error: false,
},
}
);
export type RadioVariant = VariantProps<typeof radioVariants>["variant"];
export type RadioSize = VariantProps<typeof radioVariants>["size"];
export interface RadioProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
VariantProps<typeof radioVariants> {
// 标签文本
label?: React.ReactNode;
// 标签位置
labelPosition?: "left" | "right";
// 自定义单选按钮类名
radioClassName?: string;
}
/**
* Radio 单选按钮组件
*/
export const Radio = forwardRef<HTMLInputElement, RadioProps>(
(
{
variant = "default",
size = "md",
error = false,
label,
labelPosition = "right",
className,
radioClassName,
disabled,
...props
},
ref
) => {
const radioId = React.useId();
const renderRadio = () => (
<div className="relative">
<input
ref={ref}
type="radio"
id={radioId}
disabled={disabled}
className={cn(
radioVariants({ variant, size, error }),
"peer/radio",
radioClassName
)}
{...props}
/>
{/* 选中状态的圆点 */}
<div
className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full pointer-events-none transition-all duration-250",
"peer-checked/radio:bg-current",
size === "sm" && "h-1.5 w-1.5",
size === "md" && "h-2 w-2",
size === "lg" && "h-2.5 w-2.5",
variant === "default" && "text-primary-500",
variant === "success" && "text-success-500",
variant === "warning" && "text-warning-500",
variant === "error" && "text-error-500"
)}
/>
</div>
);
const renderLabel = () => {
if (!label) return null;
return (
<label
htmlFor={radioId}
className={cn(
"text-base font-normal leading-none",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
labelPosition === "left" ? "mr-2" : "ml-2"
)}
>
{label}
</label>
);
};
if (!label) {
return renderRadio();
}
return (
<div className={cn("inline-flex items-center", className)}>
{labelPosition === "left" && renderLabel()}
{renderRadio()}
{labelPosition === "right" && renderLabel()}
</div>
);
}
);
Radio.displayName = "Radio";
/**
* RadioGroup - 单选按钮组
*/
export interface RadioGroupProps {
children: React.ReactNode;
name: string;
label?: string;
error?: string;
required?: boolean;
value?: string;
onChange?: (value: string) => void;
className?: string;
orientation?: "vertical" | "horizontal";
}
export function RadioGroup({
children,
name,
label,
error,
required,
value,
onChange,
className,
orientation = "vertical",
}: RadioGroupProps) {
// 为每个 Radio 注入 name 和 onChange
const enhancedChildren = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
name,
checked: value !== undefined ? child.props.value === value : undefined,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e.target.value);
child.props.onChange?.(e);
},
});
}
return child;
});
return (
<div className={cn("space-y-2", className)}>
{label && (
<div className="text-base font-medium text-gray-900">
{label}
{required && <span className="text-error-500 ml-1">*</span>}
</div>
)}
<div
className={cn(
orientation === "vertical" ? "space-y-2" : "flex gap-4"
)}
>
{enhancedChildren}
</div>
{error && <p className="text-sm text-error-500">{error}</p>}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './select';

View File

@@ -0,0 +1,112 @@
"use client";
import React, { forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Select 下拉选择框组件
*
* Design System 中的下拉选择框组件。
*
* @example
* ```tsx
* <Select>
* <option value="">请选择</option>
* <option value="1">选项 1</option>
* <option value="2">选项 2</option>
* </Select>
* ```
*/
/**
* Select 变体样式
*/
const selectVariants = cva(
// 基础样式
"flex w-full appearance-none items-center justify-between rounded-xl border px-3 py-2 pr-8 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
variant: {
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
bordered: "border-gray-300 bg-white",
filled: "border-transparent bg-gray-100",
},
size: {
sm: "h-9 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-5 text-lg",
},
error: {
true: "border-error-500 focus-visible:ring-error-500",
false: "",
},
},
compoundVariants: [
{
variant: "filled",
error: true,
className: "bg-error-50",
},
],
defaultVariants: {
variant: "default",
size: "md",
error: false,
},
}
);
export type SelectVariant = VariantProps<typeof selectVariants>["variant"];
export type SelectSize = VariantProps<typeof selectVariants>["size"];
export interface SelectProps
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size">,
VariantProps<typeof selectVariants> {}
/**
* Select 下拉选择框组件
*/
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
(
{
variant = "default",
size = "md",
error = false,
className,
children,
...props
},
ref
) => {
return (
<div className="relative">
<select
ref={ref}
className={cn(selectVariants({ variant, size, error }), className)}
{...props}
>
{children}
</select>
{/* 下拉箭头图标 */}
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
);
}
);
Select.displayName = "Select";

View File

@@ -0,0 +1 @@
export * from './switch';

View File

@@ -0,0 +1,179 @@
"use client";
import React, { forwardRef, useState } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Switch 开关组件
*
* Design System 中的开关组件,用于二进制状态切换。
*
* @example
* ```tsx
* // 默认开关
* <Switch checked={checked} onChange={setChecked} />
*
* // 带标签
* <Switch label="启用通知" checked={checked} onChange={setChecked} />
*
* // 不同尺寸
* <Switch size="sm" checked={checked} onChange={setChecked} />
* <Switch size="lg" checked={checked} onChange={setChecked} />
* ```
*/
/**
* 开关变体样式
*/
const switchVariants = cva(
// 基础样式
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 appearance-none",
{
variants: {
variant: {
default:
"border-gray-300 bg-gray-100 checked:border-primary-500 checked:bg-primary-500",
success:
"border-gray-300 bg-gray-100 checked:border-success-500 checked:bg-success-500",
warning:
"border-gray-300 bg-gray-100 checked:border-warning-500 checked:bg-warning-500",
error:
"border-gray-300 bg-gray-100 checked:border-error-500 checked:bg-error-500",
},
size: {
sm: "h-5 w-9",
md: "h-6 w-11",
lg: "h-7 w-13",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
);
export type SwitchVariant = VariantProps<typeof switchVariants>["variant"];
export type SwitchSize = VariantProps<typeof switchVariants>["size"];
export interface SwitchProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
VariantProps<typeof switchVariants> {
// 标签文本
label?: React.ReactNode;
// 标签位置
labelPosition?: "left" | "right";
// 自定义开关类名
switchClassName?: string;
}
/**
* Switch 开关组件
*/
export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
(
{
variant = "default",
size = "md",
label,
labelPosition = "right",
className,
switchClassName,
disabled,
checked,
defaultChecked,
onChange,
...props
},
ref
) => {
const switchId = React.useId();
const [internalChecked, setInternalChecked] = useState(
checked ?? defaultChecked ?? false
);
// 处理受控和非受控模式
const isControlled = checked !== undefined;
const isChecked = isControlled ? checked : internalChecked;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isControlled) {
setInternalChecked(e.target.checked);
}
onChange?.(e);
};
// 滑块大小
const thumbSize = {
sm: "h-3.5 w-3.5",
md: "h-4 w-4",
lg: "h-5 w-5",
}[size];
// 滑块位移
const thumbTranslate = {
sm: isChecked ? "translate-x-4" : "translate-x-0.5",
md: isChecked ? "translate-x-5" : "translate-x-0.5",
lg: isChecked ? "translate-x-6" : "translate-x-0.5",
}[size];
const renderSwitch = () => (
<div className="relative inline-block">
<input
ref={ref}
type="checkbox"
id={switchId}
disabled={disabled}
checked={isChecked}
onChange={handleChange}
className={cn(
switchVariants({ variant, size }),
"peer/switch",
switchClassName
)}
{...props}
/>
{/* 滑块 */}
<div
className={cn(
"pointer-events-none absolute top-1/2 -translate-y-1/2 rounded-full bg-white shadow-sm transition-transform duration-250",
thumbSize,
thumbTranslate
)}
/>
</div>
);
const renderLabel = () => {
if (!label) return null;
return (
<label
htmlFor={switchId}
className={cn(
"text-base font-normal leading-none",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
labelPosition === "left" ? "mr-3" : "ml-3"
)}
>
{label}
</label>
);
};
if (!label) {
return renderSwitch();
}
return (
<div className={cn("inline-flex items-center", className)}>
{labelPosition === "left" && renderLabel()}
{renderSwitch()}
{labelPosition === "right" && renderLabel()}
</div>
);
}
);
Switch.displayName = "Switch";

View File

@@ -0,0 +1 @@
export * from './textarea';

View File

@@ -0,0 +1,104 @@
"use client";
import React, { forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Textarea 多行文本输入组件
*
* Design System 中的多行文本输入组件,支持多种样式变体。
*
* @example
* ```tsx
* // 默认样式
* <Textarea placeholder="请输入内容" rows={4} />
*
* // 带边框样式
* <Textarea variant="bordered" placeholder="带边框的文本域" />
*
* // 填充样式
* <Textarea variant="filled" placeholder="填充背景的文本域" />
* ```
*/
/**
* Textarea 变体样式
*/
const textareaVariants = cva(
// 基础样式
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
{
variants: {
variant: {
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
bordered: "border-gray-300 bg-white",
filled: "border-transparent bg-gray-100",
},
error: {
true: "border-error-500 focus-visible:ring-error-500",
false: "",
},
},
compoundVariants: [
{
variant: "filled",
error: true,
className: "bg-error-50",
},
],
defaultVariants: {
variant: "default",
error: false,
},
}
);
export type TextareaVariant = VariantProps<typeof textareaVariants>["variant"];
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
VariantProps<typeof textareaVariants> {
// 自动调整高度
autoResize?: boolean;
}
/**
* Textarea 多行文本输入组件
*/
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
(
{
variant = "default",
error = false,
className,
autoResize = false,
onChange,
rows = 3,
...props
},
ref
) => {
// 自动调整高度的 change 处理
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (autoResize) {
const target = e.target;
target.style.height = "auto";
target.style.height = `${target.scrollHeight}px`;
}
onChange?.(e);
};
return (
<textarea
ref={ref}
rows={rows}
className={cn(textareaVariants({ variant, error }), className)}
onChange={handleChange}
{...props}
/>
);
}
);
Textarea.displayName = "Textarea";

View File

@@ -0,0 +1,157 @@
"use client";
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Badge 徽章组件
*
* Design System 中的徽章组件,用于显示状态、标签等信息。
*
* @example
* ```tsx
* // 默认徽章
* <Badge>新</Badge>
*
* // 不同变体
* <Badge variant="success">成功</Badge>
* <Badge variant="warning">警告</Badge>
* <Badge variant="error">错误</Badge>
*
* // 不同尺寸
* <Badge size="sm">小</Badge>
* <Badge size="lg">大</Badge>
*
* // 圆形徽章
* <Badge variant="primary" dot />
* ```
*/
/**
* Badge 变体样式
*/
const badgeVariants = cva(
// 基础样式
"inline-flex items-center justify-center rounded-full font-medium transition-colors duration-250",
{
variants: {
variant: {
default: "bg-gray-100 text-gray-800",
primary: "bg-primary-100 text-primary-800",
success: "bg-success-100 text-success-800",
warning: "bg-warning-100 text-warning-800",
error: "bg-error-100 text-error-800",
info: "bg-info-100 text-info-800",
},
size: {
sm: "px-2 py-0.5 text-xs",
md: "px-2.5 py-1 text-sm",
lg: "px-3 py-1.5 text-base",
},
dot: {
true: "px-2 py-1",
false: "",
},
},
defaultVariants: {
variant: "default",
size: "md",
dot: false,
},
}
);
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
export type BadgeSize = VariantProps<typeof badgeVariants>["size"];
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {
// 子元素
children?: React.ReactNode;
// 是否为圆点样式(不显示文字)
dot?: boolean;
// 圆点颜色(仅当 dot=true 时有效)
dotColor?: string;
}
/**
* Badge 徽章组件
*/
export function Badge({
variant = "default",
size = "md",
dot = false,
dotColor,
className,
children,
...props
}: BadgeProps) {
// 圆点颜色映射
const dotColors = {
default: "bg-gray-400",
primary: "bg-primary-500",
success: "bg-success-500",
warning: "bg-warning-500",
error: "bg-error-500",
info: "bg-info-500",
};
return (
<div className={cn(badgeVariants({ variant, size, dot }), className)} {...props}>
{dot && (
<span
className={cn(
"h-2 w-2 rounded-full",
dotColor || dotColors[variant]
)}
/>
)}
{!dot && children}
</div>
);
}
/**
* StatusBadge - 状态徽章
*/
export interface StatusBadgeProps extends Omit<BadgeProps, "variant" | "children"> {
status: "online" | "offline" | "busy" | "away";
label?: string;
}
export function StatusBadge({ status, label, ...props }: StatusBadgeProps) {
const statusConfig = {
online: { variant: "success" as const, defaultLabel: "在线" },
offline: { variant: "default" as const, defaultLabel: "离线" },
busy: { variant: "error" as const, defaultLabel: "忙碌" },
away: { variant: "warning" as const, defaultLabel: "离开" },
};
const config = statusConfig[status];
return (
<Badge variant={config.variant} {...props}>
{label || config.defaultLabel}
</Badge>
);
}
/**
* CounterBadge - 计数徽章
*/
export interface CounterBadgeProps extends Omit<BadgeProps, "children"> {
count: number;
max?: number;
}
export function CounterBadge({ count, max = 99, ...props }: CounterBadgeProps) {
const displayCount = count > max ? `${max}+` : count;
return (
<Badge variant="error" size="sm" {...props}>
{displayCount}
</Badge>
);
}

View File

@@ -0,0 +1 @@
export * from './badge';

View File

@@ -0,0 +1,102 @@
"use client";
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Divider 分隔线组件
*
* Design System 中的分隔线组件,用于分隔内容区域。
*
* @example
* ```tsx
* // 水平分隔线
* <Divider />
*
* // 带文字的分隔线
* <Divider>或者</Divider>
*
* // 垂直分隔线
* <Divider orientation="vertical" />
*
* // 不同样式
* <Divider variant="dashed" />
* <Divider variant="dotted" />
* ```
*/
/**
* Divider 变体样式
*/
const dividerVariants = cva(
// 基础样式
"border-gray-300",
{
variants: {
variant: {
solid: "border-solid",
dashed: "border-dashed",
dotted: "border-dotted",
},
orientation: {
horizontal: "w-full border-t",
vertical: "h-full border-l",
},
},
defaultVariants: {
variant: "solid",
orientation: "horizontal",
},
}
);
export type DividerVariant = VariantProps<typeof dividerVariants>["variant"];
export type DividerOrientation = VariantProps<typeof dividerVariants>["orientation"];
export interface DividerProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof dividerVariants> {
// 子元素(用于带文字的分隔线)
children?: React.ReactNode;
// 文字位置(仅水平分隔线有效)
labelPosition?: "center" | "left" | "right";
}
/**
* Divider 分隔线组件
*/
export function Divider({
variant = "solid",
orientation = "horizontal",
labelPosition = "center",
children,
className,
...props
}: DividerProps) {
// 带文字的水平分隔线
if (children && orientation === "horizontal") {
const labelAlignment = {
left: "justify-start",
center: "justify-center",
right: "justify-end",
}[labelPosition];
return (
<div className={cn("flex items-center gap-4 w-full", className)} {...props}>
<div className={cn("flex-1 border-t", `border-${variant}`)} />
<span className="text-sm text-gray-500 whitespace-nowrap">{children}</span>
<div className={cn("flex-1 border-t", `border-${variant}`)} />
</div>
);
}
return (
<div
className={cn(dividerVariants({ variant, orientation }), className)}
role="separator"
aria-orientation={orientation}
{...props}
/>
);
}

View File

@@ -0,0 +1 @@
export * from './divider';

View File

@@ -0,0 +1,202 @@
"use client";
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Alert 警告提示组件
*
* Design System 中的警告提示组件,用于显示重要信息。
*
* @example
* ```tsx
* // 默认提示
* <Alert>这是一条普通提示</Alert>
*
* // 成功提示
* <Alert variant="success">操作成功!</Alert>
*
* // 错误提示(带标题)
* <Alert variant="error" title="错误">
* 发生了一些问题
* </Alert>
*
* // 可关闭的提示
* <Alert variant="warning" closable onClose={handleClose}>
* 请注意此警告
* </Alert>
* ```
*/
/**
* Alert 变体样式
*/
const alertVariants = cva(
// 基础样式
"rounded-xl border-2 px-4 py-3 shadow-sm transition-all duration-250",
{
variants: {
variant: {
info: "border-info-500 bg-info-50 text-info-900",
success: "border-success-500 bg-success-50 text-success-900",
warning: "border-warning-500 bg-warning-50 text-warning-900",
error: "border-error-500 bg-error-50 text-error-900",
},
},
defaultVariants: {
variant: "info",
},
}
);
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
export interface AlertProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof alertVariants> {
// 标题
title?: string;
// 是否可关闭
closable?: boolean;
// 关闭回调
onClose?: () => void;
// 自定义图标
icon?: React.ReactNode;
}
// 默认图标
const defaultIcons = {
info: (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
),
success: (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
),
warning: (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
),
error: (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
),
};
/**
* Alert 警告提示组件
*/
export function Alert({
variant = "info",
title,
closable = false,
onClose,
icon,
className,
children,
...props
}: AlertProps) {
const [visible, setVisible] = React.useState(true);
const handleClose = () => {
setVisible(false);
onClose?.();
};
if (!visible) return null;
// 图标颜色
const iconColors = {
info: "text-info-500",
success: "text-success-500",
warning: "text-warning-500",
error: "text-error-500",
};
return (
<div
className={cn(alertVariants({ variant }), className)}
role="alert"
{...props}
>
<div className="flex items-start gap-3">
{/* 图标 */}
<div className={cn("shrink-0", iconColors[variant])}>
{icon || defaultIcons[variant]}
</div>
{/* 内容 */}
<div className="flex-1 min-w-0">
{title && (
<h5 className="mb-1 font-semibold leading-tight">{title}</h5>
)}
<div className="text-sm leading-relaxed">{children}</div>
</div>
{/* 关闭按钮 */}
{closable && (
<button
onClick={handleClose}
className="shrink-0 rounded-lg p-1 hover:bg-black/5 transition-colors"
aria-label="Close"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
</div>
);
}
/**
* 快捷组件
*/
export const InfoAlert = (props: Omit<AlertProps, "variant">) => (
<Alert variant="info" {...props} />
);
export const SuccessAlert = (props: Omit<AlertProps, "variant">) => (
<Alert variant="success" {...props} />
);
export const WarningAlert = (props: Omit<AlertProps, "variant">) => (
<Alert variant="warning" {...props} />
);
export const ErrorAlert = (props: Omit<AlertProps, "variant">) => (
<Alert variant="error" {...props} />
);

View File

@@ -0,0 +1 @@
export * from './alert';

View File

@@ -0,0 +1 @@
export * from './progress';

View File

@@ -0,0 +1,214 @@
"use client";
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Progress 进度条组件
*
* Design System 中的进度条组件,用于显示任务完成进度。
*
* @example
* ```tsx
* // 默认进度条
* <Progress value={60} />
*
* // 不同尺寸
* <Progress value={60} size="sm" />
* <Progress value={60} size="lg" />
*
* // 不同变体
* <Progress variant="success" value={100} />
* <Progress variant="warning" value={75} />
* <Progress variant="error" value={30} />
*
* // 无标签
* <Progress value={60} showLabel={false} />
*
* // 自定义颜色
* <Progress value={60} color="#35786f" />
* ```
*/
/**
* Progress 变体样式
*/
const progressVariants = cva(
// 基础样式
"overflow-hidden rounded-full bg-gray-200 transition-all duration-250",
{
variants: {
size: {
sm: "h-1.5",
md: "h-2",
lg: "h-3",
},
variant: {
default: "",
success: "",
warning: "",
error: "",
},
},
defaultVariants: {
size: "md",
variant: "default",
},
}
);
export type ProgressSize = VariantProps<typeof progressVariants>["size"];
export type ProgressVariant = VariantProps<typeof progressVariants>["variant"];
export interface ProgressProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof progressVariants> {
// 进度值0-100
value: number;
// 是否显示百分比标签
showLabel?: boolean;
// 自定义标签
label?: string;
// 是否显示动画
animated?: boolean;
// 自定义颜色(覆盖 variant
color?: string;
}
/**
* Progress 进度条组件
*/
export function Progress({
value = 0,
size = "md",
variant = "default",
showLabel = true,
label,
animated = true,
color,
className,
...props
}: ProgressProps) {
// 确保值在 0-100 之间
const clampedValue = Math.min(100, Math.max(0, value));
// 计算颜色
const getColor = () => {
if (color) return color;
const colors = {
default: "bg-primary-500",
success: "bg-success-500",
warning: "bg-warning-500",
error: "bg-error-500",
};
return colors[variant];
};
// 格式化标签
const formatLabel = () => {
if (label !== undefined) return label;
return `${Math.round(clampedValue)}%`;
};
return (
<div className={cn("w-full", className)} {...props}>
<div className="flex items-center justify-between mb-1">
<div className="flex-1">
<div
className={cn(progressVariants({ size, variant }))}
role="progressbar"
aria-valuenow={clampedValue}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className={cn(
"h-full rounded-full transition-all duration-500 ease-out",
getColor(),
animated && "animate-pulse"
)}
style={{ width: `${clampedValue}%` }}
/>
</div>
</div>
{showLabel && (
<div className="ml-3 text-sm font-medium text-gray-700 min-w-[3rem] text-right">
{formatLabel()}
</div>
)}
</div>
</div>
);
}
/**
* CircularProgress - 环形进度条
*/
export interface CircularProgressProps extends React.SVGProps<SVGSVGElement> {
value: number;
size?: number;
strokeWidth?: number;
variant?: ProgressVariant;
showLabel?: boolean;
label?: string;
}
export function CircularProgress({
value = 0,
size = 120,
strokeWidth = 8,
variant = "default",
showLabel = true,
label,
className,
...props
}: CircularProgressProps) {
const clampedValue = Math.min(100, Math.max(0, value));
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (clampedValue / 100) * circumference;
const colors = {
default: "#35786f",
success: "#22c55e",
warning: "#f59e0b",
error: "#ef4444",
};
const strokeColor = colors[variant];
return (
<div className={cn("inline-flex items-center justify-center", className)}>
<svg width={size} height={size} {...props}>
{/* 背景圆 */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="#e5e7eb"
strokeWidth={strokeWidth}
/>
{/* 进度圆 */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={strokeColor}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
transform={`rotate(-90 ${size / 2} ${size / 2})`}
className="transition-all duration-500 ease-out"
/>
</svg>
{showLabel && (
<div className="absolute text-base font-semibold text-gray-700">
{label !== undefined ? label : `${Math.round(clampedValue)}%`}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './skeleton';

View File

@@ -0,0 +1,192 @@
"use client";
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Skeleton 骨架屏组件
*
* Design System 中的骨架屏组件,用于内容加载时的占位显示。
*
* @example
* ```tsx
* // 默认骨架屏
* <Skeleton className="h-4 w-32" />
*
* // 不同变体
* <Skeleton variant="text" />
* <Skeleton variant="circular" className="h-12 w-12" />
* <Skeleton variant="rectangular" className="h-32 w-full" />
*
* // 自定义动画
* <Skeleton animated={false} />
* ```
*/
/**
* Skeleton 变体样式
*/
const skeletonVariants = cva(
// 基础样式
"shrink-0 animate-pulse rounded",
{
variants: {
variant: {
text: "h-4 w-full",
circular: "rounded-full",
rectangular: "rounded-lg",
},
animated: {
true: "animate-pulse",
false: "",
},
},
defaultVariants: {
variant: "text",
animated: true,
},
}
);
export type SkeletonVariant = VariantProps<typeof skeletonVariants>["variant"];
export interface SkeletonProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof skeletonVariants> {}
/**
* Skeleton 骨架屏组件
*/
export function Skeleton({
variant = "text",
animated = true,
className,
...props
}: SkeletonProps) {
return (
<div
className={cn(
"bg-gray-200",
skeletonVariants({ variant, animated }),
className
)}
{...props}
/>
);
}
/**
* 预设的骨架屏组合
*/
/**
* 文本骨架屏(多行)
*/
export interface TextSkeletonProps {
lines?: number;
className?: string;
}
export function TextSkeleton({ lines = 3, className }: TextSkeletonProps) {
return (
<div className={cn("space-y-2", className)}>
{Array.from({ length: lines }).map((_, i) => (
<Skeleton
key={i}
variant="text"
className={cn(i === lines - 1 && "w-3/4")}
/>
))}
</div>
);
}
/**
* 卡片骨架屏
*/
export interface CardSkeletonProps {
showAvatar?: boolean;
className?: string;
}
export function CardSkeleton({
showAvatar = false,
className,
}: CardSkeletonProps) {
return (
<div className={cn("p-6 space-y-4", className)}>
{showAvatar && (
<div className="flex items-center space-x-4">
<Skeleton variant="circular" className="h-12 w-12" />
<div className="space-y-2 flex-1">
<Skeleton variant="text" className="w-1/3" />
<Skeleton variant="text" className="w-1/4" />
</div>
</div>
)}
<TextSkeleton lines={3} />
</div>
);
}
/**
* 列表项骨架屏
*/
export interface ListItemSkeletonProps {
showAvatar?: boolean;
className?: string;
}
export function ListItemSkeleton({
showAvatar = true,
className,
}: ListItemSkeletonProps) {
return (
<div className={cn("flex items-center space-x-4 p-4", className)}>
{showAvatar && <Skeleton variant="circular" className="h-10 w-10" />}
<div className="space-y-2 flex-1">
<Skeleton variant="text" className="w-3/4" />
<Skeleton variant="text" className="w-1/2" />
</div>
</div>
);
}
/**
* 表格骨架屏
*/
export interface TableSkeletonProps {
rows?: number;
columns?: number;
className?: string;
}
export function TableSkeleton({
rows = 5,
columns = 4,
className,
}: TableSkeletonProps) {
return (
<div className={cn("space-y-2", className)}>
{/* 表头 */}
<div className="flex space-x-4">
{Array.from({ length: columns }).map((_, i) => (
<Skeleton key={`header-${i}`} variant="text" className="flex-1" />
))}
</div>
{/* 表体 */}
{Array.from({ length: rows }).map((_, rowIndex) => (
<div key={`row-${rowIndex}`} className="flex space-x-4">
{Array.from({ length: columns }).map((_, colIndex) => (
<Skeleton
key={`cell-${rowIndex}-${colIndex}`}
variant="text"
className="flex-1"
/>
))}
</div>
))}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './toast';

View File

@@ -0,0 +1,108 @@
"use client";
/**
* Toast 组件
*
* 基于项目已安装的 sonner 库封装的 Toast 通知组件。
* 提供类型安全的 API 和预设样式。
*
* @example
* ```tsx
* import { toast } from '@/design-system/feedback/toast';
*
* // 基础用法
* toast.success("操作成功!");
* toast.error("发生错误");
* toast.warning("请注意");
* toast.info("提示信息");
*
* // 自定义选项
* toast.success("操作成功!", {
* description: "您的更改已保存",
* duration: 5000,
* });
*
* // Promise Toast
* toast.promise(
* fetchData(),
* {
* loading: "加载中...",
* success: "加载成功",
* error: "加载失败",
* }
* );
* ```
*/
import { toast as sonnerToast } from "sonner";
export type ToastProps = {
description?: string;
duration?: number;
id?: string;
onDismiss?: () => void;
};
/**
* Toast 通知组件
*/
export const toast = {
success: (message: string, props?: ToastProps) => {
return sonnerToast.success(message, {
description: props?.description,
duration: props?.duration,
id: props?.id,
onDismiss: props?.onDismiss,
});
},
error: (message: string, props?: ToastProps) => {
return sonnerToast.error(message, {
description: props?.description,
duration: props?.duration,
id: props?.id,
onDismiss: props?.onDismiss,
});
},
warning: (message: string, props?: ToastProps) => {
return sonnerToast.warning(message, {
description: props?.description,
duration: props?.duration,
id: props?.id,
onDismiss: props?.onDismiss,
});
},
info: (message: string, props?: ToastProps) => {
return sonnerToast.info(message, {
description: props?.description,
duration: props?.duration,
id: props?.id,
onDismiss: props?.onDismiss,
});
},
promise: <T,>(
promise: Promise<T>,
{
loading,
success,
error,
}: {
loading: string;
success: string | ((data: T) => string);
error: string | ((error: Error) => string);
}
) => {
return sonnerToast.promise(promise, {
loading,
success,
error,
});
},
dismiss: (id?: string) => {
sonnerToast.dismiss(id);
},
};

View File

@@ -0,0 +1,52 @@
/**
* Design System 统一导出
*
* 这是 Design System 的主入口,所有组件和工具都可以从这里导入。
*
* @example
* ```tsx
* // 从主入口导入
* import { Button, Input, Card } from '@/design-system';
*
* // 或者从子路径导入(更好的 tree-shaking
* import { Button } from '@/design-system/base/button';
* import { Input } from '@/design-system/base/input';
* ```
*/
// 设计令牌
export * from './tokens';
// 工具函数
export * from './lib/utils';
// 基础组件
export * from './base/button';
export * from './base/input';
export * from './base/textarea';
export * from './base/card';
export * from './base/checkbox';
export * from './base/radio';
export * from './base/switch';
export * from './base/select';
// 反馈组件
export * from './feedback/alert';
export * from './feedback/progress';
export * from './feedback/skeleton';
export * from './feedback/toast';
// 覆盖组件
export * from './overlay/modal';
// 数据展示组件
export * from './data-display/badge';
export * from './data-display/divider';
// 布局组件
export * from './layout/container';
export * from './layout/grid';
export * from './layout/stack';
// 导航组件
export * from './navigation/tabs';

View File

@@ -0,0 +1,103 @@
"use client";
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Container 容器组件
*
* Design System 中的容器组件,用于约束内容宽度并居中。
*
* @example
* ```tsx
* // 默认容器
* <Container>
* <p>内容被居中并限制最大宽度</p>
* </Container>
*
* // 不同尺寸
* <Container size="sm">小容器</Container>
* <Container size="lg">大容器</Container>
*
* // 全宽容器
* <Container fullWidth>全宽容器</Container>
*
* // 带内边距
* <Container padding="xl">带内边距的容器</Container>
* ```
*/
/**
* Container 变体样式
*/
const containerVariants = cva(
// 基础样式
"mx-auto",
{
variants: {
size: {
xs: "max-w-xs",
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
"2xl": "max-w-2xl",
"3xl": "max-w-3xl",
"4xl": "max-w-4xl",
"5xl": "max-w-5xl",
"6xl": "max-w-6xl",
"7xl": "max-w-7xl",
full: "max-w-full",
},
padding: {
none: "",
xs: "px-2",
sm: "px-4",
md: "px-6",
lg: "px-8",
xl: "px-10",
},
fullWidth: {
true: "w-full",
false: "",
},
},
defaultVariants: {
size: "7xl",
padding: "md",
fullWidth: false,
},
}
);
export type ContainerSize = VariantProps<typeof containerVariants>["size"];
export type ContainerPadding = VariantProps<typeof containerVariants>["padding"];
export interface ContainerProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof containerVariants> {
// 子元素
children: React.ReactNode;
}
/**
* Container 容器组件
*/
export function Container({
size = "7xl",
padding = "md",
fullWidth = false,
className,
children,
...props
}: ContainerProps) {
return (
<div
className={cn(containerVariants({ size, padding, fullWidth }), className)}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './container';

View File

@@ -0,0 +1,179 @@
"use client";
import React from "react";
import { cn } from "@/design-system/lib/utils";
/**
* Grid 网格布局组件
*
* Design System 中的网格布局组件,基于 CSS Grid。
*
* @example
* ```tsx
* // 默认网格2列
* <Grid>
* <div>项目 1</div>
* <div>项目 2</div>
* </Grid>
*
* // 自定义列数
* <Grid cols={3}>
* <div>项目 1</div>
* <div>项目 2</div>
* <div>项目 3</div>
* </Grid>
*
* // 响应式网格
* <Grid cols={{ sm: 1, md: 2, lg: 3 }}>
* <div>项目 1</div>
* <div>项目 2</div>
* <div>项目 3</div>
* </Grid>
*
* // 带间距的网格
* <Grid gap={4}>
* <div>项目 1</div>
* <div>项目 2</div>
* </Grid>
* ```
*/
export interface ResponsiveValue {
sm?: number;
md?: number;
lg?: number;
xl?: number;
"2xl"?: number;
}
export interface GridProps extends React.HTMLAttributes<HTMLDivElement> {
// 列数
cols?: number | ResponsiveValue;
// 行间距
rowGap?: number | string;
// 列间距
colGap?: number | string;
// 间距(同时设置行列间距)
gap?: number | string;
// 子元素
children: React.ReactNode;
}
/**
* 生成网格类名
*/
function generateGridClass(cols?: number | ResponsiveValue): string {
if (!cols) return "grid-cols-1 md:grid-cols-2";
if (typeof cols === "number") {
return `grid-cols-${cols}`;
}
// 响应式列数
const classes = ["grid-cols-1"]; // 默认 1 列
if (cols.sm) classes.push(`sm:grid-cols-${cols.sm}`);
if (cols.md) classes.push(`md:grid-cols-${cols.md}`);
if (cols.lg) classes.push(`lg:grid-cols-${cols.lg}`);
if (cols.xl) classes.push(`xl:grid-cols-${cols.xl}`);
if (cols["2xl"]) classes.push(`"2xl":grid-cols-${cols["2xl"]}`);
// 如果没有指定 md使用默认 2 列
if (!cols.md && !cols.lg && !cols.xl && !cols["2xl"]) {
classes.push("md:grid-cols-2");
}
return classes.join(" ");
}
/**
* Grid 网格布局组件
*/
export function Grid({
cols,
rowGap,
colGap,
gap,
className,
children,
...props
}: GridProps) {
const gridClass = generateGridClass(cols);
return (
<div
className={cn(
"grid",
gridClass,
gap && `gap-${gap}`,
rowGap && `gap-y-${rowGap}`,
colGap && `gap-x-${colGap}`,
className
)}
{...props}
>
{children}
</div>
);
}
/**
* GridItem - 网格项
*/
export interface GridItemProps extends React.HTMLAttributes<HTMLDivElement> {
// 列跨度
colSpan?: number | ResponsiveValue;
// 行跨度
rowSpan?: number;
// 子元素
children: React.ReactNode;
}
/**
* 生成跨度类名
*/
function generateSpanClass(
type: "col" | "row",
span?: number | ResponsiveValue
): string {
if (!span) return "";
if (typeof span === "number") {
return `${type === "col" ? "col" : "row"}-span-${span}`;
}
// 响应式跨度
const classes: string[] = [];
if (span.sm) classes.push(`sm:${type === "col" ? "col" : "row"}-span-${span.sm}`);
if (span.md) classes.push(`md:${type === "col" ? "col" : "row"}-span-${span.md}`);
if (span.lg) classes.push(`lg:${type === "col" ? "col" : "row"}-span-${span.lg}`);
if (span.xl) classes.push(`xl:${type === "col" ? "col" : "row"}-span-${span.xl}`);
if (span["2xl"]) classes.push(`"2xl":${type === "col" ? "col" : "row"}-span-${span["2xl"]}`);
return classes.join(" ");
}
/**
* GridItem 网格项组件
*/
export function GridItem({
colSpan,
rowSpan,
className,
children,
...props
}: GridItemProps) {
return (
<div
className={cn(
generateSpanClass("col", colSpan),
generateSpanClass("row", rowSpan),
className
)}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './grid';

View File

@@ -0,0 +1 @@
export * from './stack';

View File

@@ -0,0 +1,140 @@
"use client";
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Stack 堆叠布局组件
*
* Design System 中的堆叠布局组件,用于垂直或水平排列子元素。
*
* @example
* ```tsx
* // 垂直堆叠
* <Stack>
* <div>项目 1</div>
* <div>项目 2</div>
* <div>项目 3</div>
* </Stack>
*
* // 水平堆叠
* <Stack direction="row">
* <div>项目 1</div>
* <div>项目 2</div>
* <div>项目 3</div>
* </Stack>
*
* // 自定义间距
* <Stack gap={4}>
* <div>项目 1</div>
* <div>项目 2</div>
* </Stack>
*
* // 居中对齐
* <Stack align="center">
* <div>项目 1</div>
* <div>项目 2</div>
* </Stack>
* ```
*/
/**
* Stack 变体样式
*/
const stackVariants = cva(
// 基础样式
"flex",
{
variants: {
direction: {
column: "flex-col",
row: "flex-row",
},
align: {
start: "items-start",
center: "items-center",
end: "items-end",
stretch: "items-stretch",
},
justify: {
start: "justify-start",
center: "justify-center",
end: "justify-end",
between: "justify-between",
},
wrap: {
true: "flex-wrap",
false: "flex-nowrap",
},
},
defaultVariants: {
direction: "column",
align: "start",
justify: "start",
wrap: false,
},
}
);
export type StackDirection = VariantProps<typeof stackVariants>["direction"];
export type StackAlign = VariantProps<typeof stackVariants>["align"];
export type StackJustify = VariantProps<typeof stackVariants>["justify"];
export interface StackProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof stackVariants> {
// 子元素
children: React.ReactNode;
// 间距(使用 Tailwind spacing
gap?: number | string;
// 是否为内联布局
inline?: boolean;
}
/**
* Stack 堆叠布局组件
*/
export function Stack({
direction = "column",
align = "start",
justify = "start",
wrap = false,
gap,
inline = false,
className,
children,
...props
}: StackProps) {
return (
<div
className={cn(
stackVariants({ direction, align, justify, wrap }),
gap && (typeof gap === "number" ? `gap-${gap}` : `gap-[${gap}]`),
inline && "inline-flex",
className
)}
{...props}
>
{children}
</div>
);
}
/**
* VStack - 垂直堆叠组件(快捷方式)
*/
export interface VStackProps extends Omit<StackProps, "direction"> {}
export function VStack(props: VStackProps) {
return <Stack direction="column" {...props} />;
}
/**
* HStack - 水平堆叠组件(快捷方式)
*/
export interface HStackProps extends Omit<StackProps, "direction"> {}
export function HStack(props: HStackProps) {
return <Stack direction="row" {...props} />;
}

View File

@@ -0,0 +1,23 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* 合并 Tailwind CSS 类名的工具函数
*
* 使用 clsx 处理条件类名,然后使用 tailwind-merge 解决 Tailwind 类名冲突
*
* @param inputs - 类名(字符串、对象、数组等)
* @returns 合并后的类名字符串
*
* @example
* ```tsx
* cn('px-4 py-2', isActive && 'bg-primary-500', 'text-white')
* // => 'px-4 py-2 bg-primary-500 text-white'
*
* cn('px-4 px-6') // 自动解决冲突
* // => 'px-6'
* ```
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1 @@
export * from './tabs';

View File

@@ -0,0 +1,193 @@
"use client";
import React, { useState } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/design-system/lib/utils";
/**
* Tabs 标签页组件
*
* Design System 中的标签页组件,用于内容分组和切换。
*
* @example
* ```tsx
* function MyComponent() {
* const [activeTab, setActiveTab] = useState("tab1");
*
* return (
* <Tabs value={activeTab} onValueChange={setActiveTab}>
* <Tabs.List>
* <Tabs.Trigger value="tab1">标签 1</Tabs.Trigger>
* <Tabs.Trigger value="tab2">标签 2</Tabs.Trigger>
* <Tabs.Trigger value="tab3">标签 3</Tabs.Trigger>
* </Tabs.List>
* <Tabs.Content value="tab1">
* <p>内容 1</p>
* </Tabs.Content>
* <Tabs.Content value="tab2">
* <p>内容 2</p>
* </Tabs.Content>
* <Tabs.Content value="tab3">
* <p>内容 3</p>
* </Tabs.Content>
* </Tabs>
* );
* }
* ```
*/
export interface TabsProps {
value: string;
onValueChange: (value: string) => void;
children: React.ReactNode;
className?: string;
variant?: "line" | "enclosed" | "soft";
}
/**
* Tabs 组件
*/
export function Tabs({
value,
onValueChange,
children,
className,
variant = "line",
}: TabsProps) {
return (
<div className={cn("w-full", className)} data-variant={variant}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
value,
onValueChange,
variant,
} as any);
}
return child;
})}
</div>
);
}
/**
* Tabs.List - 标签列表
*/
export interface TabsListProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
variant?: "line" | "enclosed" | "soft";
}
const listVariants = cva(
"flex",
{
variants: {
variant: {
line: "border-b border-gray-200",
enclosed: "bg-gray-100 p-1 rounded-lg gap-1",
soft: "gap-2",
},
},
defaultVariants: {
variant: "line",
},
}
);
export function TabsList({
children,
variant = "line",
className,
...props
}: TabsListProps) {
return (
<div
role="tablist"
className={cn(listVariants({ variant }), className)}
{...props}
>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
variant,
} as any);
}
return child;
})}
</div>
);
}
/**
* Tabs.Trigger - 标签触发器
*/
export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string;
children: React.ReactNode;
variant?: "line" | "enclosed" | "soft";
}
const triggerVariants = cva(
"px-4 py-2 text-sm font-medium transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
line: "border-b-2 -mb-px rounded-t-lg data-[state=active]:border-primary-500 data-[state=active]:text-primary-600 text-gray-600 hover:text-gray-900 border-transparent",
enclosed: "rounded-md data-[state=active]:bg-white data-[state=active]:shadow-sm text-gray-600 hover:text-gray-900",
soft: "rounded-lg data-[state=active]:bg-primary-50 data-[state=active]:text-primary-700 text-gray-600 hover:text-gray-900 hover:bg-gray-100",
},
},
defaultVariants: {
variant: "line",
},
}
);
export function TabsTrigger({
value,
children,
variant = "line",
className,
...props
}: TabsTriggerProps) {
return (
<button
type="button"
role="tab"
aria-selected={props["data-state"] === "active"}
data-state={props["data-state"]}
className={cn(triggerVariants({ variant }), className)}
{...props}
>
{children}
</button>
);
}
/**
* Tabs.Content - 标签内容
*/
export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
children: React.ReactNode;
}
export function TabsContent({
value,
children,
className,
...props
}: TabsContentProps) {
if (value !== props["data-state"]) return null;
return (
<div
role="tabpanel"
className={cn("mt-4 focus:outline-none", className)}
tabIndex={0}
{...props}
>
{children}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './modal';

View File

@@ -0,0 +1,238 @@
"use client";
import React, { useEffect } from "react";
import { cn } from "@/design-system/lib/utils";
/**
* Modal 模态框组件
*
* 全屏遮罩的模态对话框组件。
*
* @example
* ```tsx
* function MyComponent() {
* const [open, setOpen] = useState(false);
*
* return (
* <>
* <Button onClick={() => setOpen(true)}>打开模态框</Button>
* <Modal open={open} onClose={() => setOpen(false)}>
* <Modal.Header>
* <Modal.Title>标题</Modal.Title>
* </Modal.Header>
* <Modal.Body>
* <p>模态框内容</p>
* </Modal.Body>
* <Modal.Footer>
* <Button variant="secondary" onClick={() => setOpen(false)}>
* 取消
* </Button>
* <Button variant="primary">确定</Button>
* </Modal.Footer>
* </Modal>
* </>
* );
* }
* ```
*/
export interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
size?: "sm" | "md" | "lg" | "xl" | "full";
closeOnOverlayClick?: boolean;
closeOnEscape?: boolean;
className?: string;
}
const sizeClasses = {
sm: "max-w-md",
md: "max-w-lg",
lg: "max-w-2xl",
xl: "max-w-4xl",
full: "max-w-full mx-4",
};
/**
* Modal 组件
*/
export function Modal({
open,
onClose,
children,
size = "md",
closeOnOverlayClick = true,
closeOnEscape = true,
className,
}: ModalProps) {
// ESC 键关闭
useEffect(() => {
if (!open || !closeOnEscape) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [open, closeOnEscape, onClose]);
// 禁止背景滚动
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}
}, [open]);
if (!open) return null;
return (
<div
className="fixed inset-0 z-modal flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
>
{/* 遮罩层 */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={closeOnOverlayClick ? onClose : undefined}
/>
{/* 模态框内容 */}
<div
className={cn(
"relative z-10 w-full bg-white rounded-2xl shadow-2xl max-h-[90vh] overflow-hidden flex flex-col",
sizeClasses[size],
className
)}
>
{children}
</div>
</div>
);
}
/**
* Modal.Header - 模态框头部
*/
export interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
Modal.Header = function ModalHeader({
children,
className,
...props
}: ModalHeaderProps) {
return (
<div
className={cn("flex items-center justify-between p-6 border-b border-gray-200", className)}
{...props}
>
{children}
</div>
);
};
/**
* Modal.Title - 模态框标题
*/
export interface ModalTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
children: React.ReactNode;
}
Modal.Title = function ModalTitle({
children,
className,
...props
}: ModalTitleProps) {
return (
<h2 className={cn("text-xl font-semibold text-gray-900", className)} {...props}>
{children}
</h2>
);
};
/**
* Modal.Body - 模态框主体
*/
export interface ModalBodyProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
Modal.Body = function ModalBody({
children,
className,
...props
}: ModalBodyProps) {
return (
<div className={cn("p-6 overflow-y-auto flex-1", className)} {...props}>
{children}
</div>
);
};
/**
* Modal.Footer - 模态框底部
*/
export interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
align?: "left" | "center" | "right";
}
Modal.Footer = function ModalFooter({
children,
align = "right",
className,
...props
}: ModalFooterProps) {
const alignClasses = {
left: "justify-start",
center: "justify-center",
right: "justify-end",
};
return (
<div
className={cn(
"flex items-center gap-3 p-6 border-t border-gray-200",
alignClasses[align],
className
)}
{...props}
>
{children}
</div>
);
};
/**
* Modal.CloseButton - 关闭按钮
*/
export interface ModalCloseButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
Modal.CloseButton = function ModalCloseButton({
className,
onClick,
...props
}: ModalCloseButtonProps) {
return (
<button
type="button"
className={cn(
"rounded-lg p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors",
className
)}
onClick={onClick}
{...props}
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
);
};

View File

@@ -0,0 +1,74 @@
/**
* 边框和圆角系统设计令牌
*/
/**
* 圆角半径
*/
export const borderRadius = {
none: '0',
sm: '0.125rem', // 2px
DEFAULT: '0.25rem', // 4px
md: '0.375rem', // 6px
lg: '0.5rem', // 8px
xl: '0.75rem', // 12px
'2xl': '1rem', // 16px
'3xl': '1.5rem', // 24px
full: '9999px',
} as const;
/**
* 语义化圆角
*/
export const semanticBorderRadius = {
// 按钮
button: {
sm: borderRadius.lg,
md: borderRadius.xl,
lg: borderRadius['2xl'],
},
// 输入框
input: {
sm: borderRadius.md,
md: borderRadius.lg,
lg: borderRadius.xl,
},
// 卡片
card: {
sm: borderRadius.xl,
md: borderRadius['2xl'],
lg: borderRadius['3xl'],
},
// 模态框
modal: borderRadius['2xl'],
// 徽章/标签
badge: borderRadius.full,
// 圆形按钮/图标
circle: borderRadius.full,
} as const;
/**
* 边框宽度
*/
export const borderWidth = {
DEFAULT: '1px',
0: '0',
2: '2px',
4: '4px',
8: '8px',
} as const;
/**
* 边框样式
*/
export const borderStyle = {
solid: 'solid',
dashed: 'dashed',
dotted: 'dotted',
double: 'double',
} as const;

View File

@@ -0,0 +1,162 @@
/**
* 颜色系统设计令牌
*
* 基于 8 色阶系统50-900提供完整的颜色语义化命名
*
* 主色Teal (#35786f)
* - 用于主要操作按钮、链接、重要元素
*
* 语义色:
* - success: 成功状态
* - warning: 警告状态
* - error: 错误/危险状态
* - info: 信息提示
*/
/**
* 主色 - Teal
*/
export const primary = {
50: '#f0f9f8',
100: '#e0f2f0',
200: '#bce6e1',
300: '#8dd4cc',
400: '#5ec2b7',
500: '#35786f',
600: '#2a605b',
700: '#1f4844',
800: '#183835',
900: '#122826',
950: '#0a1413',
} as const;
/**
* 中性色 - Gray
*/
export const gray = {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
} as const;
/**
* 语义色 - Success
*/
export const success = {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
} as const;
/**
* 语义色 - Warning
*/
export const warning = {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
950: '#451a03',
} as const;
/**
* 语义色 - Error
*/
export const error = {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
} as const;
/**
* 语义色 - Info
*/
export const info = {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
} as const;
/**
* 完整的颜色令牌集合
*/
export const colors = {
primary,
gray,
success,
warning,
error,
info,
// 语义别名
semantic: {
success: success,
warning: warning,
error: error,
info: info,
},
// 通用别名
white: '#ffffff',
black: '#000000',
transparent: 'transparent',
// 前景色
foreground: gray[900],
'foreground-secondary': gray[600],
'foreground-tertiary': gray[500],
'foreground-disabled': gray[400],
// 背景色
background: gray[50],
'background-secondary': gray[100],
'background-tertiary': gray[200],
// 边框色
border: gray[300],
'border-secondary': gray[200],
'border-focus': primary[500],
// 阴影
shadow: 'rgba(0, 0, 0, 0.1)',
} as const;
export type Colors = typeof colors;

View File

@@ -0,0 +1,38 @@
/**
* Design System 设计令牌统一导出
*
* 包含所有设计系统的原始令牌
*/
export * from './colors';
export * from './spacing';
export * from './typography';
export * from './borders';
export * from './shadows';
import type { Colors } from './colors';
/**
* 完整的设计令牌类型
*/
export interface DesignTokens {
colors: Colors;
spacing: typeof import('./spacing').spacing;
semanticSpacing: typeof import('./spacing').semanticSpacing;
sizes: typeof import('./spacing').sizes;
fontFamily: typeof import('./typography').fontFamily;
fontSize: typeof import('./typography').fontSize;
fontWeight: typeof import('./typography').fontWeight;
letterSpacing: typeof import('./typography').letterSpacing;
typography: typeof import('./typography').typography;
borderRadius: typeof import('./borders').borderRadius;
borderWidth: typeof import('./borders').borderWidth;
boxShadow: typeof import('./shadows').boxShadow;
}
/**
* 设计令牌常量(供 TypeScript 类型使用)
*/
export const tokens = {
colors: {} as Colors,
} as const;

View File

@@ -0,0 +1,61 @@
/**
* 阴影系统设计令牌
*
* 提供多层次的阴影效果,用于创建深度和层次感
*/
/**
* 阴影级别
*/
export const boxShadow = {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)',
none: 'none',
} as const;
/**
* 语义化阴影
*/
export const semanticShadow = {
// 按钮
button: {
sm: boxShadow.sm,
md: boxShadow.DEFAULT,
lg: boxShadow.md,
},
// 卡片
card: {
default: boxShadow.xl,
bordered: 'none',
elevated: boxShadow['2xl'],
},
// 模态框/弹窗
modal: boxShadow['2xl'],
dropdown: boxShadow.lg,
popover: boxShadow.lg,
tooltip: boxShadow.md,
// 导航栏
navbar: boxShadow.sm,
// 输入框 focus
focus: `0 0 0 3px rgba(53, 120, 111, 0.1)`, // primary-500 的 10% 透明度
} as const;
/**
* 颜色阴影(用于特定元素的阴影)
*/
export const coloredShadow = {
primary: `0 4px 14px 0 rgba(53, 120, 111, 0.39)`,
success: `0 4px 14px 0 rgba(34, 197, 94, 0.39)`,
warning: `0 4px 14px 0 rgba(245, 158, 11, 0.39)`,
error: `0 4px 14px 0 rgba(239, 68, 68, 0.39)`,
info: `0 4px 14px 0 rgba(59, 130, 246, 0.39)`,
} as const;

View File

@@ -0,0 +1,117 @@
/**
* 间距系统设计令牌
*
* 基于 8pt 基准网格系统,提供一致的间距和尺寸
* 单位rem (1rem = 16px)
*/
/**
* 基础间距刻度
*/
export const spacing = {
0: '0',
0.5: '0.125rem', // 2px
1: '0.25rem', // 4px
1.5: '0.375rem', // 6px
2: '0.5rem', // 8px
2.5: '0.625rem', // 10px
3: '0.75rem', // 12px
3.5: '0.875rem', // 14px
4: '1rem', // 16px
5: '1.25rem', // 20px
6: '1.5rem', // 24px
7: '1.75rem', // 28px
8: '2rem', // 32px
9: '2.25rem', // 36px
10: '2.5rem', // 40px
11: '2.75rem', // 44px
12: '3rem', // 48px
14: '3.5rem', // 56px
16: '4rem', // 64px
20: '5rem', // 80px
24: '6rem', // 96px
28: '7rem', // 112px
32: '8rem', // 128px
36: '9rem', // 144px
40: '10rem', // 160px
44: '11rem', // 176px
48: '12rem', // 192px
52: '13rem', // 208px
56: '14rem', // 224px
60: '15rem', // 240px
64: '16rem', // 256px
72: '18rem', // 288px
80: '20rem', // 320px
96: '24rem', // 384px
} as const;
/**
* 语义化间距
*/
export const semanticSpacing = {
// 组件内边距
padding: {
xs: spacing[1], // 4px
sm: spacing[2], // 8px
md: spacing[4], // 16px
lg: spacing[6], // 24px
xl: spacing[8], // 32px
'2xl': spacing[12], // 48px
},
// 组件间距gap
gap: {
xs: spacing[1], // 4px
sm: spacing[2], // 8px
md: spacing[4], // 16px
lg: spacing[6], // 24px
xl: spacing[8], // 32px
},
// 布局间距
layout: {
sm: spacing[4], // 16px
md: spacing[8], // 32px
lg: spacing[12], // 48px
xl: spacing[16], // 64px
},
// 容器宽度
container: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
} as const;
/**
* 常用尺寸(用于组件大小)
*/
export const sizes = {
// 图标尺寸
icon: {
xs: '0.75rem', // 12px
sm: '1rem', // 16px
md: '1.25rem', // 20px
lg: '1.5rem', // 24px
xl: '2rem', // 32px
},
// 按钮高度
button: {
sm: '2rem', // 32px
md: '2.5rem', // 40px
lg: '3rem', // 48px
},
// 输入框高度
input: {
sm: '2rem', // 32px
md: '2.5rem', // 40px
lg: '3rem', // 48px
},
} as const;
export { spacing as default };

View File

@@ -0,0 +1,145 @@
/**
* 字体系统设计令牌
*
* 定义字体家族、字号、行高和字重
*/
/**
* 字体家族
*/
export const fontFamily = {
sans: [
'var(--font-geist-sans)',
'-apple-system',
'BlinkMacSystemFont',
'system-ui',
'sans-serif',
],
mono: [
'var(--font-geist-mono)',
'ui-monospace',
'SFMono-Regular',
'Monaco',
'Consolas',
'monospace',
],
} as const;
/**
* 字体大小和行高
*/
export const fontSize = {
xs: ['0.75rem', { lineHeight: '1rem' }], // 12px / 16px
sm: ['0.875rem', { lineHeight: '1.25rem' }], // 14px / 20px
base: ['1rem', { lineHeight: '1.5rem' }], // 16px / 24px
lg: ['1.125rem', { lineHeight: '1.75rem' }], // 18px / 28px
xl: ['1.25rem', { lineHeight: '1.75rem' }], // 20px / 28px
'2xl': ['1.5rem', { lineHeight: '2rem' }], // 24px / 32px
'3xl': ['1.875rem', { lineHeight: '2.25rem' }], // 30px / 36px
'4xl': ['2.25rem', { lineHeight: '2.5rem' }], // 36px / 40px
'5xl': ['3rem', { lineHeight: '1' }], // 48px / 48px
'6xl': ['3.75rem', { lineHeight: '1' }], // 60px / 60px
'7xl': ['4.5rem', { lineHeight: '1' }], // 72px / 72px
'8xl': ['6rem', { lineHeight: '1' }], // 96px / 96px
'9xl': ['8rem', { lineHeight: '1' }], // 128px / 128px
} as const;
/**
* 字重
*/
export const fontWeight = {
thin: '100',
extralight: '200',
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
black: '900',
} as const;
/**
* 字母间距
*/
export const letterSpacing = {
tighter: '-0.05em',
tight: '-0.025em',
normal: '0em',
wide: '0.025em',
wider: '0.05em',
widest: '0.1em',
} as const;
/**
* 语义化排版
*/
export const typography = {
// 标题
h1: {
fontSize: fontSize['3xl'][0],
fontWeight: fontWeight.bold,
lineHeight: fontSize['3xl'][1].lineHeight,
},
h2: {
fontSize: fontSize['2xl'][0],
fontWeight: fontWeight.semibold,
lineHeight: fontSize['2xl'][1].lineHeight,
},
h3: {
fontSize: fontSize.xl[0],
fontWeight: fontWeight.semibold,
lineHeight: fontSize.xl[1].lineHeight,
},
h4: {
fontSize: fontSize.lg[0],
fontWeight: fontWeight.semibold,
lineHeight: fontSize.lg[1].lineHeight,
},
h5: {
fontSize: fontSize.base[0],
fontWeight: fontWeight.medium,
lineHeight: fontSize.base[1].lineHeight,
},
h6: {
fontSize: fontSize.sm[0],
fontWeight: fontWeight.medium,
lineHeight: fontSize.sm[1].lineHeight,
},
// 正文
body: {
fontSize: fontSize.base[0],
fontWeight: fontWeight.normal,
lineHeight: fontSize.base[1].lineHeight,
},
'body-sm': {
fontSize: fontSize.sm[0],
fontWeight: fontWeight.normal,
lineHeight: fontSize.sm[1].lineHeight,
},
'body-lg': {
fontSize: fontSize.lg[0],
fontWeight: fontWeight.normal,
lineHeight: fontSize.lg[1].lineHeight,
},
// 标签/说明
label: {
fontSize: fontSize.sm[0],
fontWeight: fontWeight.medium,
lineHeight: fontSize.sm[1].lineHeight,
},
caption: {
fontSize: fontSize.xs[0],
fontWeight: fontWeight.normal,
lineHeight: fontSize.xs[1].lineHeight,
},
// 代码
code: {
fontSize: fontSize.sm[0],
fontWeight: fontWeight.normal,
fontFamily: fontFamily.mono.join(', '),
},
} as const;

233
tailwind.config.ts Normal file
View File

@@ -0,0 +1,233 @@
import type { Config } from "tailwindcss";
/**
* Tailwind CSS 配置
*
* 集成 Design System 设计令牌到 Tailwind 工具类
*/
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
// 颜色系统
colors: {
// 主色 - Teal
primary: {
50: '#f0f9f8',
100: '#e0f2f0',
200: '#bce6e1',
300: '#8dd4cc',
400: '#5ec2b7',
500: '#35786f',
600: '#2a605b',
700: '#1f4844',
800: '#183835',
900: '#122826',
950: '#0a1413',
},
// 中性色
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
// 语义色 - Success
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
// 语义色 - Warning
warning: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
950: '#451a03',
},
// 语义色 - Error
error: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
// 语义色 - Info
info: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
// 语义别名
background: 'var(--background)',
foreground: 'var(--foreground)',
},
// 间距系统(基于 8pt 网格)
spacing: {
18: '4.5rem',
88: '22rem',
128: '32rem',
},
// 字体家族
fontFamily: {
sans: [
'var(--font-geist-sans)',
'-apple-system',
'BlinkMacSystemFont',
'system-ui',
'sans-serif',
],
mono: [
'var(--font-geist-mono)',
'ui-monospace',
'SFMono-Regular',
'Monaco',
'Consolas',
'monospace',
],
},
// 字体大小和行高
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
'5xl': ['3rem', { lineHeight: '1' }],
'6xl': ['3.75rem', { lineHeight: '1' }],
'7xl': ['4.5rem', { lineHeight: '1' }],
'8xl': ['6rem', { lineHeight: '1' }],
'9xl': ['8rem', { lineHeight: '1' }],
},
// 字重
fontWeight: {
thin: '100',
extralight: '200',
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
black: '900',
},
// 圆角
borderRadius: {
sm: '0.125rem',
DEFAULT: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
},
// 阴影
boxShadow: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)',
// 语义阴影
'primary': '0 4px 14px 0 rgba(53, 120, 111, 0.39)',
'success': '0 4px 14px 0 rgba(34, 197, 94, 0.39)',
'warning': '0 4px 14px 0 rgba(245, 158, 11, 0.39)',
'error': '0 4px 14px 0 rgba(239, 68, 68, 0.39)',
'info': '0 4px 14px 0 rgba(59, 130, 246, 0.39)',
},
// 容器最大宽度
maxWidth: {
'xs': '20rem',
'sm': '24rem',
'md': '28rem',
'lg': '32rem',
'xl': '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
'7xl': '80rem',
'8xl': '88rem',
},
// Z-index 层级
zIndex: {
dropdown: 1000,
sticky: 1020,
fixed: 1030,
modalBackdrop: 1040,
modal: 1050,
popover: 1060,
tooltip: 1070,
},
// 动画时长
transitionDuration: {
'250': '250ms',
'350': '350ms',
},
// 断点
screens: {
'xs': '475px',
},
},
},
plugins: [],
};
export default config;