Design System 重构完成
This commit is contained in:
@@ -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
27
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -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
585
src/design-system/README.md
Normal 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
|
||||
273
src/design-system/base/button/button.tsx
Normal file
273
src/design-system/base/button/button.tsx
Normal 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} />
|
||||
);
|
||||
1
src/design-system/base/button/index.ts
Normal file
1
src/design-system/base/button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './button';
|
||||
198
src/design-system/base/card/card.tsx
Normal file
198
src/design-system/base/card/card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/base/card/index.ts
Normal file
1
src/design-system/base/card/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './card';
|
||||
170
src/design-system/base/checkbox/checkbox.tsx
Normal file
170
src/design-system/base/checkbox/checkbox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/base/checkbox/index.ts
Normal file
1
src/design-system/base/checkbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './checkbox';
|
||||
1
src/design-system/base/input/index.ts
Normal file
1
src/design-system/base/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './input';
|
||||
151
src/design-system/base/input/input.tsx
Normal file
151
src/design-system/base/input/input.tsx
Normal 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";
|
||||
1
src/design-system/base/radio/index.ts
Normal file
1
src/design-system/base/radio/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './radio';
|
||||
219
src/design-system/base/radio/radio.tsx
Normal file
219
src/design-system/base/radio/radio.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/base/select/index.ts
Normal file
1
src/design-system/base/select/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './select';
|
||||
112
src/design-system/base/select/select.tsx
Normal file
112
src/design-system/base/select/select.tsx
Normal 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";
|
||||
1
src/design-system/base/switch/index.ts
Normal file
1
src/design-system/base/switch/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './switch';
|
||||
179
src/design-system/base/switch/switch.tsx
Normal file
179
src/design-system/base/switch/switch.tsx
Normal 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";
|
||||
1
src/design-system/base/textarea/index.ts
Normal file
1
src/design-system/base/textarea/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './textarea';
|
||||
104
src/design-system/base/textarea/textarea.tsx
Normal file
104
src/design-system/base/textarea/textarea.tsx
Normal 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";
|
||||
157
src/design-system/data-display/badge/badge.tsx
Normal file
157
src/design-system/data-display/badge/badge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/data-display/badge/index.ts
Normal file
1
src/design-system/data-display/badge/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './badge';
|
||||
102
src/design-system/data-display/divider/divider.tsx
Normal file
102
src/design-system/data-display/divider/divider.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/design-system/data-display/divider/index.ts
Normal file
1
src/design-system/data-display/divider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './divider';
|
||||
202
src/design-system/feedback/alert/alert.tsx
Normal file
202
src/design-system/feedback/alert/alert.tsx
Normal 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} />
|
||||
);
|
||||
1
src/design-system/feedback/alert/index.ts
Normal file
1
src/design-system/feedback/alert/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './alert';
|
||||
1
src/design-system/feedback/progress/index.ts
Normal file
1
src/design-system/feedback/progress/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './progress';
|
||||
214
src/design-system/feedback/progress/progress.tsx
Normal file
214
src/design-system/feedback/progress/progress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/feedback/skeleton/index.ts
Normal file
1
src/design-system/feedback/skeleton/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './skeleton';
|
||||
192
src/design-system/feedback/skeleton/skeleton.tsx
Normal file
192
src/design-system/feedback/skeleton/skeleton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/feedback/toast/index.ts
Normal file
1
src/design-system/feedback/toast/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './toast';
|
||||
108
src/design-system/feedback/toast/toast.tsx
Normal file
108
src/design-system/feedback/toast/toast.tsx
Normal 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);
|
||||
},
|
||||
};
|
||||
52
src/design-system/index.ts
Normal file
52
src/design-system/index.ts
Normal 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';
|
||||
103
src/design-system/layout/container/container.tsx
Normal file
103
src/design-system/layout/container/container.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/layout/container/index.ts
Normal file
1
src/design-system/layout/container/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './container';
|
||||
179
src/design-system/layout/grid/grid.tsx
Normal file
179
src/design-system/layout/grid/grid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/layout/grid/index.ts
Normal file
1
src/design-system/layout/grid/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './grid';
|
||||
1
src/design-system/layout/stack/index.ts
Normal file
1
src/design-system/layout/stack/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './stack';
|
||||
140
src/design-system/layout/stack/stack.tsx
Normal file
140
src/design-system/layout/stack/stack.tsx
Normal 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} />;
|
||||
}
|
||||
23
src/design-system/lib/utils.ts
Normal file
23
src/design-system/lib/utils.ts
Normal 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));
|
||||
}
|
||||
1
src/design-system/navigation/tabs/index.ts
Normal file
1
src/design-system/navigation/tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './tabs';
|
||||
193
src/design-system/navigation/tabs/tabs.tsx
Normal file
193
src/design-system/navigation/tabs/tabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/design-system/overlay/modal/index.ts
Normal file
1
src/design-system/overlay/modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './modal';
|
||||
238
src/design-system/overlay/modal/modal.tsx
Normal file
238
src/design-system/overlay/modal/modal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
74
src/design-system/tokens/borders.ts
Normal file
74
src/design-system/tokens/borders.ts
Normal 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;
|
||||
162
src/design-system/tokens/colors.ts
Normal file
162
src/design-system/tokens/colors.ts
Normal 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;
|
||||
38
src/design-system/tokens/index.ts
Normal file
38
src/design-system/tokens/index.ts
Normal 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;
|
||||
61
src/design-system/tokens/shadows.ts
Normal file
61
src/design-system/tokens/shadows.ts
Normal 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;
|
||||
117
src/design-system/tokens/spacing.ts
Normal file
117
src/design-system/tokens/spacing.ts
Normal 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 };
|
||||
145
src/design-system/tokens/typography.ts
Normal file
145
src/design-system/tokens/typography.ts
Normal 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
233
tailwind.config.ts
Normal 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;
|
||||
Reference in New Issue
Block a user