Design System 重构完成
This commit is contained in:
@@ -15,6 +15,8 @@
|
|||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-auth": "^1.4.10",
|
"better-auth": "^1.4.10",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
"unstorage": "^1.17.3",
|
"unstorage": "^1.17.3",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
|
|||||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -24,6 +24,12 @@ importers:
|
|||||||
better-auth:
|
better-auth:
|
||||||
specifier: ^1.4.10
|
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)
|
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:
|
dotenv:
|
||||||
specifier: ^17.2.3
|
specifier: ^17.2.3
|
||||||
version: 17.2.3
|
version: 17.2.3
|
||||||
@@ -48,6 +54,9 @@ importers:
|
|||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
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:
|
unstorage:
|
||||||
specifier: ^1.17.3
|
specifier: ^1.17.3
|
||||||
version: 1.17.3
|
version: 1.17.3
|
||||||
@@ -1480,9 +1489,16 @@ packages:
|
|||||||
citty@0.1.6:
|
citty@0.1.6:
|
||||||
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
||||||
|
|
||||||
|
class-variance-authority@0.7.1:
|
||||||
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
client-only@0.0.1:
|
client-only@0.0.1:
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||||
|
|
||||||
|
clsx@2.1.1:
|
||||||
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -3024,6 +3040,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
tailwind-merge@3.4.0:
|
||||||
|
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
||||||
|
|
||||||
tailwindcss@4.1.18:
|
tailwindcss@4.1.18:
|
||||||
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
||||||
|
|
||||||
@@ -4778,8 +4797,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
|
|
||||||
|
class-variance-authority@0.7.1:
|
||||||
|
dependencies:
|
||||||
|
clsx: 2.1.1
|
||||||
|
|
||||||
client-only@0.0.1: {}
|
client-only@0.0.1: {}
|
||||||
|
|
||||||
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -6407,6 +6432,8 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
|
tailwind-merge@3.4.0: {}
|
||||||
|
|
||||||
tailwindcss@4.1.18: {}
|
tailwindcss@4.1.18: {}
|
||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|||||||
@@ -1,20 +1,154 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design System CSS 变量
|
||||||
|
*
|
||||||
|
* 定义全局 CSS 变量用于主题切换和动态样式
|
||||||
|
*/
|
||||||
:root {
|
: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;
|
--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 {
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
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 {
|
.navbar-btn {
|
||||||
@apply border-0 bg-transparent hover:bg-black/30 shadow-none;
|
@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